DOC Update documentation for GraphQL v4 (#10325)

* DOC Add upgrade guidance for GraphQL v4

* Move docs around

The existing upgrading docs are for upgrading to v4, whereas the new docs are more about how to handle the new .graphql-generated directory.

* Update graphql documentation

* More updates to doc

Co-authored-by: Guy Sartorelli <guy.sartorelli@silverstripe.com>
This commit is contained in:
Maxime Rainville 2022-06-08 17:23:48 +12:00 committed by GitHub
parent 2c30438b01
commit 22d992a22b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1226 additions and 953 deletions

View File

@ -171,7 +171,8 @@ noisy, here's some pointers for auto-generated files to trigger and include in a
* `.graphql-generated/` and `public/_graphql/`: Schema and type definitions required by CMS and any GraphQL API endpoint.
Generated by
[silverstripe/graphql v4](https://github.com/silverstripe/silverstripe-graphql). See
[GraphQL Schema Build](/developer_guides/graphql/getting_started/building_the_schema).
[building the schema](/developer_guides/graphql/getting_started/building_the_schema) and
[deploying the schema](/developer_guides/graphql/getting_started/deploying_the_schema).
* Various recipes create default files in `app/` and `public/` on `composer install`
and `composer update` via
[silverstripe/recipe-plugin](https://github.com/silverstripe/recipe-plugin).

View File

@ -21,15 +21,21 @@ Docs for the current stable version (3.x) can be found
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.
(e.g. `silverstripe/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.
in the user space (e.g. `/graphql`) - i.e. your custom schema, while `admin` refers to the
GraphQL server used by CMS modules (`admin/graphql`). You can also [set up a new schema server](#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`.
[info]
The word "server" here refers to a route with its own isolated GraphQL schema. It does
not refer to a web server.
[/info]
By default, `silverstripe/graphql` 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`](api:SilverStripe\Control\Director).
```yaml
SilverStripe\Control\Director:
@ -52,10 +58,8 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\GraphQL\Controller
constructor:
schemaKey: myNewSchema
```
We'll now need to route the controller.
```yaml
@ -64,14 +68,8 @@ SilverStripe\Control\Director:
'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:
# ...
```
Now, once you have [configured](configuring_your_schema) and [built](building_the_schema) your schema, you
can access it at `/my-graphql`.
### Further reading

View File

@ -1,6 +1,7 @@
---
title: Configuring your schema
summary: Add a basic type to the schema configuration
icon: code
---
# Getting started
@ -22,10 +23,10 @@ GraphQL is a strongly-typed API layer, so having a schema behind it is essential
* 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)**
* **Fields** need to be **[resolved](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.
must be resolved.
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
@ -42,7 +43,7 @@ SilverStripe\GraphQL\Schema\Schema:
# your schemas here
```
Let's populate a schema that is pre-configured for us out of the box, `default`.
Let's populate a schema that is pre-configured for us out of the box called "default".
**app/_config/graphql.yml**
```yml
@ -54,7 +55,7 @@ SilverStripe\GraphQL\Schema\Schema:
types:
# your generic types here
models:
# your dataobjects here
# your DataObjects here
queries:
# your queries here
mutations:
@ -63,15 +64,21 @@ SilverStripe\GraphQL\Schema\Schema:
### Avoid config flushes
Because the schema YAML is only consumed at build time and never used at runtime, it doesn't
Because the schema definition 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.
increase performance and remove the need for flushing when you [build your schema](building_the_schema).
We can do this by adding a `src` key to our schema definition that maps to a directory
relative to the project root.
[notice]
This doesn't mean there is never a need to `flush=1` when building your schema. If you were to add a new
schema, make a change to the value of this `src` attribute, or create new PHP classes, those are still
standard config changes which won't take effect without a flush.
[/notice]
We can do this by adding a `src` key to our `app/_config/graphql.yml` schema definition
that maps to a directory relative to the project root.
**app/_config/graphql.yml**
```yml
@ -82,13 +89,11 @@ SilverStripe\GraphQL\Schema\Schema:
- app/_graphql
```
Your `src` must be an array. This allows further source files to be merged into your schema.
Your `src` must be an array. This allows further source files to be merged into your schema.
This feature can be use to extend the schema of third party modules.
[info]
Your directory can also be a module reference, e.g. `somevendor/somemodule: _graphql`
[/info]
Your directory can also be relative to a module reference, e.g. `somevendor/somemodule: _graphql`:
**app/_config/graphql.yml**
```yml
@ -101,17 +106,9 @@ SilverStripe\GraphQL\Schema\Schema:
# The next line would map to `vendor/somevendor/somemodule/_graphql`
- '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]
Now, in the new `app/_graphql` folder, we can create YAML file definitions.
**app/_graphql/schema.yml**
```yaml
@ -121,7 +118,7 @@ config:
types:
# your generic types here
models:
# your dataobjects here
# your DataObjects here
bulkLoad:
# your bulk loader directives here
queries:
@ -137,38 +134,24 @@ like this. We can tidy this up quite a bit by simply placing the files in direct
to the keys they populate -- e.g. `config/`, `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/config/config.yml**
```yaml
# my schema config here
```
**app/_graphql/types/types.yml**
```yaml
# my type definitions here
```
**app/_graphql/models/models.yml**
```yaml
# my type definitions here
```
**app/_graphql/bulkLoad/bulkLoad.yml**
```yaml
# my bulk loader directives 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**.
If the filename is named one of the four keywords above, it will be implicitly placed
in the corresponding section of the schema - e.g. any configuration
added to `app/_graphql/config.yml` will be implicitly added to
`SilverStripe\GraphQL\Schema\Schema.schemas.default.config`.
**This only works in the root source directory** (i.e. `app/_graphql/some-directory/config.yml`
will not work).
**app/_graphql/config.yml**
```yaml
# my config here
```
**app/_graphql/types.yml**
```yaml
@ -185,29 +168,60 @@ in the corresponding section of the schema. **This only works in the root source
# my bulk loader directives here
```
#### Going even more granular
##### 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 - e.g. any configuration
added to a `.yml` file in `app/_graphql/config/` will be implicitly added to
`SilverStripe\GraphQL\Schema\Schema.schemas.default.config`.
[hint]
The names of the actual files here do not matter. You could for example have a separate file
for each of your types, e.g. `app/_graphql/types/my-first-type.yml`.
[/hint]
**app/_graphql/config/config.yml**
```yaml
# my config here
```
**app/_graphql/types/types.yml**
```yaml
# my types here
```
**app/_graphql/models/models.yml**
```yaml
# my models here
```
**app/_graphql/bulkLoad/bulkLoad.yml**
```yaml
# my bulk loader directives 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.
_or_ the filename 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`
* `app/_graphql/config/config.yml` maps to `SilverStripe\GraphQL\Schema\Schema.schemas.default.config`
* `app/_graphql/types/allElementalBlocks.yml` maps to `SilverStripe\GraphQL\Schema\Schema.schemas.default.types`
* `app/_graphql/news-and-blog/models/blog.yml` maps to `SilverStripe\GraphQL\Schema\Schema.schemas.default.models`
* `app/_graphql/mySchema.yml` maps to `SilverStripe\GraphQL\Schema\Schema.schemas.default`
### Schema config
In addition to all the keys mentioned above, each schema can declare a generic
configuration section, `config`. This are mostly used for assigning or removing plugins
and resolvers.
Each schema can declare a generic configuration section, `config`. This is mostly used for assigning
or removing plugins and resolvers.
An important subsection of `config` is `modelConfig`, where you can configure settings for specific
models, e.g. `DataObject`.
Like the other sections, it can have its own `config.yml`, or just be added as a `config:`
Like the other sections, it can have its own `config.yml`, or just be added as a `config:`
mapping to a generic schema yaml document.
**app/_graphql/config.yml**
@ -223,11 +237,19 @@ modelConfig:
paginateList: false
```
You can learn more about plugins and resolvers in the [query plugins](../working_with_dataobjects/query_plugins),
[plugins](../plugins), [building a custom query](../working_with_generic_types/building_a_custom_query#building-a-custom-query),
and [resolver discovery](../working_with_generic_types/resolver_discovery) sections.
### Defining a basic type
Let's define a generic type for our GraphQL schema.
[info]
Generic types don't map to `DataObject` classes - they're useful for querying more 'generic' data (hence the name).
You'll learn more about adding DataObjects in [working with DataObjects](../working_with_DataObjects).
[/info]
**app/_graphql/types.yml***
```yaml
Country:
@ -238,8 +260,11 @@ Country:
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:
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
@ -253,13 +278,13 @@ 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!
Look out for the footgun, here. Make sure your bracketed type is in quotes
(i.e. `'[String]'`, not `[String]`), 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).
let's get started on [**adding some DataObjects**](../working_with_DataObjects).
### Further reading

View File

@ -1,6 +1,7 @@
---
title: Building the schema
summary: Turn your schema configuration into executable code
icon: hammer
---
# Getting started
@ -17,87 +18,107 @@ Docs for the current stable version (3.x) can be found
## 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
The primary API surface of the `silverstripe/graphql` module is the yaml configuration, along
with some [procedural configuration](using_procedural_code). 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.
(which we refer to as the "schema definition") 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.
whenever the schema definition changes, or a new schema definition is added.
### Running the build
### What triggers a GraphQL code 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.
* Any time you run the `dev/graphql/build` command to explicitly build your GraphQL schemas.
* Any time you run the `dev/build` command on your project.
* `silverstripe/graphql` will attempt to generate your schema "on-demand" on the first GraphQL request _only_ if it wasnt already generated.
`$ vendor/bin/sake dev/graphql/build schema=default`
[warning]
Relying on the "on-demand" schema generation on the first GraphQL request requires some additional consideration.
See [deploying the schema](deploying_the_schema#on-demand).
[/warning]
Keep in mind that many of your changes will be in YAML, which also requires a flush.
#### Running `dev/graphql/build`
`$ vendor/bin/sake dev/graphql/build schema=default flush=1`
The main command for generating the schema code is `dev/graphql/build`.
`vendor/bin/sake dev/graphql/build`
This command takes an optional `schema` parameter. If you only want to generate a specific schema
(e.g. generate your custom schema, but not the CMS schema), you should pass in the name of the
schema you want to build.
[info]
If you do not provide a `schema` parameter, the task will build all schemas.
If you do not provide a `schema` parameter, the command will build all schemas.
[/info]
### Building on dev/build
`vendor/bin/sake dev/graphql/build schema=default`
By default, all schemas will be built as a side-effect of `dev/build`. To disable this, change
the config:
[info]
Most of the time, the name of your custom schema is `default`. If you're editing DataObjects
that are accessed with GraphQL in the CMS, you may have to rebuild the `admin` schema as well.
[/info]
Keep in mind that some of your changes will be in YAML in the `_config/` directory, which also
requires a flush.
`vendor/bin/sake dev/graphql/build schema=default flush=1`
#### Building on dev/build
By default, all schemas will be built during `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
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.
If the type hasn't changed, it doesn't get re-built. This reduces build times to **under one second** for incremental changes.
#### Clearing the cache
#### Clearing the schema 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`.
If you want to completely re-generate your schema from scratch, you can add `clear=1` to the `dev/graphql/build` command.
`$ vendor/bin/sake dev/graphql/build schema=default 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.
Keep in mind that it's not always explicit schema definition 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: '*'`.
* Updating the `$db` array (or relationships) of a `DataObject` class that has `fields: '*'` (i.e. include all fields on that class in the schema).
* 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
* If you are using Silverstripe CMS **without the [silverstripe/assets](https://github.com/silverstripe/silverstripe-assets) module installed, the build task will leave a `.graphql` file artefact in your public directory for CMS reference.
Though it doesn't contain any highly sensitive data, we recommend you block this file from being viewed by outside
traffic.
* Adding an extension to a `DataObject` class
* Adding a new subclass of a `DataObject` class that is already exposed
### Viewing the generated code
By default, the generated code is placed in the `.graphql-generated/` directory in the root of your project.
It is not meant to be accessible through your webserver (which is ensured by dot-prefixing)
and keeping it outside of the `public/` webroot.
By default, the generated PHP code is placed in the `.graphql-generated/` directory in the root of your project.
It is not meant to be accessible through your webserver, Which is ensured by keeping it outside of the
`public/` webroot and the inclusion of a `.htaccess` file in each schema folder.
Additional files are generated for CMS operation in `public/_graphql/`, and
those are meant to be accessible through your webserver.
those _are_ meant to be accessible through your webserver.
See [Tips and Tricks: Schema Introspection](tips_and_tricks#schema-introspection)
to find out how to generate these files for your own schema.
[alert]
While it is safe for you to view these files, you should not manually alter them. If you need to make a change
to your GraphQL schema, you should [update the schema definition](configuring_your_schema) and rebuild your schema.
[/alert]
### Further reading

View File

@ -1,6 +1,7 @@
---
title: Building a schema with procedural code
summary: Use PHP code to build your schema
icon: tools
---
# Getting started
@ -22,7 +23,8 @@ may have an enum containing a list of all the languages that are configured for
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
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.
@ -35,7 +37,7 @@ the schema build.
### Adding executable code
We can use the `execute` section of the config to add an implementation of `SchemaUpdater`.
We can use the `execute` section of the config to add an implementation of [`SchemaUpdater`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater).
```yaml
SilverStripe\GraphQL\Schema\Schema:
@ -46,7 +48,7 @@ SilverStripe\GraphQL\Schema\Schema:
- 'MyProject\MySchema'
```
Now just implement the `SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater` interface.
Now just implement the [`SchemaUpdater`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater) interface.
**app/src/MySchema.php**
```php
@ -64,57 +66,56 @@ class MySchema implements SchemaUpdater
### Example code
Most the API should be self-documenting, and a good IDE should autocomplete everything you
Most of 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)`)
* types (`$schema->addType(Type $type)`)
* models (`$schema->addModel(ModelType $type)`)
* queries (`$schema->addQuery(Query $query)`)
* mutations (`$schema->addMutation(Mutation $mutation)`)
* enums (`$schema->addEnum(Enum $type)`)
* interfaces (`$schema->addInterface(InterfaceType $type)`)
* unions (`$schema->addUnion(UnionType $type)`)
```php
public static function updateSchema(Schema $schema): void
{
$myType = Type::create('Country')
$countryType = Type::create('Country')
->addField('name', 'String')
->addField('code', 'String');
$schema->addType($myType);
$schema->addType($countryType);
$myQuery = Query::create('readCountries', '[Country]')
$countriesQuery = Query::create('readCountries', '[Country]!')
->addArg('limit', 'Int');
$schema->addQuery($countriesQuery);
$myModel = $schema->createModel(MyDataObject::class)
->addAllFields()
->addAllOperations();
$schema->addModel($myModel);
}
```
#### Fluent setters
#### Chainable 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')
$countryType = Type::create('Country')
->addField('name', 'String', function (Field $field) {
// Must be a callable. No inline closures allowed!
$field->setResolver([MyClass::class, 'myResolver'])
$field->setResolver([MyResolverClass::class, 'countryResolver'])
->addArg('myArg', 'String!');
})
->addField('code', 'String');
$schema->addType($myType);
$schema->addType($countryType);
$myQuery = Query::create('readCountries', '[Country]')
$countriesQuery = Query::create('readCountries', '[Country]!')
->addArg('limit', 'Int', function (Argument $arg) {
$arg->setDefaultValue(20);
});
$schema->addQuery($countriesQuery);
```
### Further reading

View File

@ -0,0 +1,100 @@
---
title: Deploying the schema
summary: Deploy your GraphQL schema to a test or production environment
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]
## Deploying the schema
One way or another, you must get the `.graphql-generated/` and `public/_graphql/` folders into your test and production environments for Silverstripe CMS (and your own custom queries) to work as expected. There are many ways to do so. The options below are listed in order of complexity.
### Single-server hosting solutions with simple deployments {#simple-single-server}
If you host your site on a single server and you always run `dev/build` during the deployment, then assuming you have set up permissions to allow the webserver to write to the `.graphql-generated/` and `public/_graphql/` folders, your GraphQL schema will be built for you as a side-effect of running `dev/build`. You don't need to do anything further. Note that if your schema is exceptionally large you may still want to read through the rest of the options below.
### Options for any hosting solution
#### Commit the schema to version control {#commit-to-vcs}
A simplistic approach is to build the schema in your local development environment and add the `.graphql-generated/` and `public/_graphql/` folders to your version control system. With this approach you would most likely want to disable schema generation at `dev/build`.
This approach has the advantage of being very simple, but it will pollute your commits with massive diffs for the generated code.
[notice]
Make sure you set your site to `live` mode and remove any `DEBUG_SCHEMA=1` from your `.env` file if it is there before generating the schema to be committed.
[/notice]
#### Explicitly build the schema during each deployment {#build-during-deployment}
Many projects will automatically run a `dev/build` whenever they deploy a site to their production environment. If thats your case, then you can just let this process run normally and generate the `.graphql-generated/` and `public/_graphql/` folders for you. This will allow you to add these folders to your `.gitignore` file and avoid tracking the folder in your version control system.
Be aware that for this approach to work, the process executing the `dev/build` must have write access to create the folders (or you must create those folders yourself, and give write access for those folders specifically), and for multi-server environments a `dev/build` or `dev/graphql/build` must be executed on each server hosting your site after each deployment.
#### Use a CI/CD pipeline to build your schema {#using-ci-cd}
Projects with more sophisticated requirements or bigger schemas exposing more than 100 `DataObject` classes may want to consider using a continuous-integration/continuous-deployment (CI/CD) pipeline to build their GraphQL schema.
In this kind of setup, you would need to update your deployment script to run the `dev/graphql/build` command which builds the `.graphql-generated/` and `public/_graphql/` folders. In multi-server environments this must be executed on each server hosting your site.
### Multi-server hosting solutions {#multi-server}
If your site is hosted in an environment with multiple servers or configured to auto-scale with demand, there are some additional considerations. For example if you only generate the schema on one single server (i.e. via `dev/build` or `dev/graphql/build`), then the other servers wont have a `.graphql-generated/` or `public/_graphql/` folder (or those folders will be empty if you manually created them).
#### Rely on "on-demand" schema generation on the first GraphQL request {#on-demand}
When the first GraphQL schema request occurs, `silverstripe/graphql` will attempt to build the `.graphql-generated/` and `public/_graphql/` folders "on-demand" if they're not already present on the server. Similarly, if the folders are present but empty, it will build the schema "on-demand". This will impose a one-time performance hit on the first GraphQL request. If your project defines multiple schemas, only the schema that is being accessed will be generated.
For most common use cases, this process is relatively fast. For example, the GraphQL schema that is used to power the CMS can be built in about a quarter of a second.
While benchmarking schema generation performance, we measured that a schema exposing 180 DataObjects with 1600 relations could be built on-demand in less than 6 seconds on a small AWS instance.
Our expectation is that on-demand schema generation will be performant for most projects with small or medium schemas.
[warning]
Note that with this approach you will need to remove or empty the `.graphql-generated/` and `public/_graphql/` folders on each server for each deployment that includes a change to the schema definition, or you risk having an outdated GraphQL schema. The "on-demand" schema generation does not detect changes to the schema definition.
[/warning]
#### Build the schema during/before deployment and share it across your servers {#multi-server-shared-dirs}
If you have a particularly large schema, you may want to ensure it is always built before the first GraphQL request. It might make sense for you to sync your `.graphql-generated/` and `public/_graphql/` folders across all your servers using an EFS or similar mechanism. In that case you only need to run `dev/build` or `dev/graphql/build` on the server with the original folder - but bear in mind that this may have a performance impact.
### Performance considerations when building the GraphQL schema {#performance-considerations}
The main driver in the resources it takes to build a GraphQL schema is the number DataObjects and the number of exposed relations in that schema. In most cases, not all DataObjects in your database will be included in your schema - best practice is to only add classes to your schema definition if you will need to query them. DataObjects not included in your schema will not impact the time or memory needed to build it.
Silverstripe CMS defines an "admin" schema it uses for its own purpose. This schema is relatively small and has a negligible performance impact.
As an indication, benchmarks were run on a t3.micro AWS instance. The results are in the table below. These numbers may not be representative of the performance in your own environment. If you intend to build large GraphQL schemas, you should take the time to run your own benchmarks and adjust your deployment strategy accordingly.
DataObjects in schema | Build time (ms) | Memory use (MB)
-- | -- | --
5 | 290 | 26
10 | 310 | 26
40 | 1060 | 38
100 | 2160 | 58
250 | 5070 | 114
500 | 11,540 | 208
### Gotchas
#### Permissions of the `.graphql-generated/` and `public/_graphql/` folders {#gotchas-permissions}
The process that is generating these folders must have write permissions to create the folder and to update existing files. If different users are used to generate the folders, then you must make sure that each user retains write access on them.
For example, if you manually run a `dev/build` under a foobar user, the folders will be owned by foobar. If your web server is running under the www-data user and you try to call `dev/graphql/build` in your browser, you might get an error if www-data doesnt have write access.
### Further reading
[CHILDREN]

View File

@ -7,19 +7,7 @@ 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.
## Installing on silverstripe/recipe-cms < 4.11
The 4.8 - 4.10 releases of `recipe-cms` support both versions `3` and `4.0.0-alpha` versions of this module. Using the alpha (or beta) releases requires inlining the recipe and updating the `silverstripe/graphql` version.
You can inline silverstripe/recipe-cms by running this command:
```
composer update-recipe silverstripe/recipe-cms
```
Alternatively, you can remove `silverstripe/recipe-cms` from your root `composer.json` and replace it with the contents of the `composer.json` in `silverstripe/recipe-cms`.
up and running with some `DataObject` content.
[alert]
You are viewing docs for a pre-release version of silverstripe/graphql (4.x).

View File

@ -1,6 +1,6 @@
---
title: Adding DataObjects to the schema
summary: An overview of how the DataObject model can influence the creation of types, queries, and mutations
summary: An overview of how the `DataObject` model can influence the creation of types, queries, and mutations
---
# Working with DataObjects
@ -15,14 +15,15 @@ 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
## 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.
In Silverstripe CMS projects, our data tends to be contained in DataObjects almost exclusively,
and the `silverstripe/graphql` schema API is designed so that adding `DataObject` content to your
GraphQL schema definition is fast and simple.
### Using model types
While it is possible to add dataobjects to your schema as generic types under the `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_
@ -47,15 +48,16 @@ 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`,
on the `Page` class. This includes the first level of relationships, 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.
To add additional fields for those relationships you will need to add the corresponding `DataObject` model types.
[/notice]
The `*` value on `operations` tells the schema to create all available queries and mutations
for the dataobject, including:
for the DataObject, including:
* `read`
* `readOne`
@ -63,15 +65,16 @@ The `*` value on `operations` tells the schema to create all available queries a
* `update`
* `delete`
Now that we've changed our schema, we need to build it using the `build-schema` task:
Now that we've changed our schema, we need to build it using the `dev/graphql/build` command:
`$ vendor/bin/sake dev/graphql/build schema=default`
`vendor/bin/sake dev/graphql/build schema=default`
Now, we can access our schema on the default graphql endpoint, `/graphql`.
Now we can access our schema on the default graphql endpoint, `/graphql`.
Test it out!
A query:
**A query:**
```graphql
query {
readPages {
@ -90,17 +93,23 @@ query {
}
}
}
}
}
```
[info]
Note the use of the default arguments on `date`. Fields created from `DBFields`
generate their own default sets of arguments. For more information, see the
[DBFieldArgs](query_plugins#dbfieldargs) for more information.
generate their own default sets of arguments. For more information, see
[DBFieldArgs](query_plugins#dbfieldargs).
[/info]
[info]
The `... on BlogPage` syntax is called an [inline fragment](https://graphql.org/learn/queries/#inline-fragments).
You can learn more about this syntax in the [Inheritance](../inheritance) section.
[/info]
**A mutation:**
A mutation:
```graphql
mutation {
createPage(input: {
@ -112,9 +121,9 @@ mutation {
}
```
[info]
[hint]
Did you get a permissions error? Make sure you're authenticated as someone with appropriate access.
[/info]
[/hint]
### Configuring operations
@ -122,12 +131,18 @@ You may not always want to add _all_ operations with the `*` wildcard. You can a
want by setting them to `true` (or `false` to remove them).
**app/_graphql/models.yml**
```
```yaml
Page:
fields: '*'
operations:
read: true
create: true
MyProject\Models\Product:
fields: '*'
operations:
'*': true
delete: false
```
Operations are also configurable, and accept a nested map of config.
@ -144,11 +159,11 @@ Page:
#### Customising the input types
The input types, specifically in `create` and `update` can be customised with a
list of fields, which can include explicitly _disallowed_ fields.
The input types, specifically in `create` and `update`, can be customised with a
list of fields. The list can include explicitly _disallowed_ fields.
**app/_graphql/models.yml**
```
```yaml
Page:
fields: '*'
operations:
@ -159,18 +174,19 @@ Page:
update:
fields:
'*': true
sensitiveField: false
immutableField: false
```
### Adding more fields
Let's add some more dataobjects, but this time, we'll only add a subset of fields and operations.
Let's add some more DataObjects, but this time, we'll only add a subset of fields and operations.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
Page:
fields: '*'
operations: '*'
MyProject\Models\Product:
fields:
onSale: true
@ -178,6 +194,7 @@ MyProject\Models\Product:
price: true
operations:
delete: true
MyProject\Models\ProductCategory:
fields:
title: true
@ -190,20 +207,21 @@ 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
MyProject\Models\Product:
fields:
onSale:
type: Boolean
```
* The mapping of our field names to the DataObject property is case-insensitive. It is a
* 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]
### Bulk loading models
It's likely that in your application, you have a whole collection of classes you want exposed to the API, with roughly
It's likely that in your application you have a whole collection of classes you want exposed to the API with roughly
the same fields and operations exposed on them. It can be really tedious to write a new declaration for every single
dataobject in your project, and as you add new ones, there's a bit of overhead in remembering to add it to the
`DataObject` in your project, and as you add new ones, there's a bit of overhead in remembering to add it to the
GraphQL schema.
Common use cases might be:
@ -216,11 +234,10 @@ Common use cases might be:
You can create logic like this using the `bulkLoad` configuration file, which allows you to specify groups of directives
that load a bundle of classes and apply the same set of configuration to all of them.
**_graphql/bulkLoad.yml**
```yaml
elemental: # An arbitrary key to define what these directives are doing
# Load all content blocks
# Load all elemental blocks except MySecretElement
load:
inheritanceLoader:
include:
@ -235,7 +252,8 @@ elemental: # An arbitrary key to define what these directives are doing
read: true
readOne: true
app:
# Load everything in our app that has the Versioned extension
# Load everything in our MyApp\Models\ namespace that has the Versioned extension
# unless the filename ends with .secret.php
load:
namespaceLoader:
include:
@ -256,6 +274,7 @@ app:
By default, four loaders are provided to you to help gather specific classnames:
#### By namespace
* **Identifier**: `namespaceLoader`
* **Description**: Include or exclude classes based on their namespace
* **Example**: `include: [MyApp\Models\*]`
@ -275,10 +294,10 @@ By default, four loaders are provided to you to help gather specific classnames:
#### By filepath
* **Identifier**: `filepathLoader`
* **Description**: Include or exclude any classes in files matching a given glob expression, relative to the base path. Module syntax is allowed.
* **Description**: Include or exclude any classes in files matching a given glob expression, relative to the base path. Module syntax is allowed.
* **Examples**:
- `include: [ 'src/models/*.model.php' ]`
- `include: [ 'somevendor/somemodule: src/Models/*.php' ]`
* `include: [ 'src/models/*.model.php' ]`
* `include: [ 'somevendor/somemodule: src/Models/*.php' ]`
Each block starts with a collection of all classes that gets filtered as each loader runs. The primary job
of a loader is to _remove_ classes from the entire collection, not add them in.
@ -289,13 +308,13 @@ of a loader is to _remove_ classes from the entire collection, not add them in.
[info]
If you find that this paints with too big a brush, you can always override individual models explicitly in `models.yml`.
The bulk loaders run _before_ the models.yml config is loaded.
The bulk loaders run _before_ the `models.yml` config is loaded.
[/info]
#### DataObjects subclasses are the default starting point
#### `DataObject` subclasses are the default starting point
Because this is Silverstripe CMS, and it's likely that you're using dataobject models only, the bulk loaders start with an
initial filter, which is defined as follows:
Because this is Silverstripe CMS, and it's likely that you're using `DataObject` models only, the bulk loaders start with an
initial filter which is defined as follows:
```yaml
inheritanceLoader:
@ -303,8 +322,8 @@ inheritanceLoader:
- SilverStripe\ORM\DataObject
```
This ensures that at a bare minimum, you're always filtering by dataobject classes only. If, for some reason, you
have a non-dataobject class in `App\Models\*`, it will automatically be filtered out due to this default setting.
This ensures that at a bare minimum, you're always filtering by `DataObject` classes _only_. If, for some reason, you
have a non-`DataObject` class in `App\Models\*`, it will automatically be filtered out due to this default setting.
This default is configured in the `defaultBulkLoad` setting in your schema config. Should you ever want to disable
that, just set it to `false`.
@ -316,39 +335,40 @@ defaultBulkLoad: false
#### Creating your own bulk loader
Bulk loaders must extend `SilverStripe\GraphQL\Schema\BulkLoader\AbstractBulkLoader`. They need to declare an
identifier (e.g. `namespaceLoader`) to be referenced in the config, and they must provide a
`collect(Collection $collection): Collection` which returns a new `Collection` instance once the loader has done its
work parsing through the `include` and `exclude` directives.
Bulk loaders must extend [`AbstractBulkLoader`](api:SilverStripe\GraphQL\Schema\BulkLoader\AbstractBulkLoader). They
need to declare an identifier (e.g. `namespaceLoader`) to be referenced in the config, and they must implement
[`collect()`](api:SilverStripe\GraphQL\Schema\BulkLoader\AbstractBulkLoader::collect()) which returns a new `Collection`
instance once the loader has done its work parsing through the `include` and `exclude` directives.
Bulk loaders are automatically registered. Just creating the class is all you need to do to have it available for use
in your `bulkLoad.yml` file.
### 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*
**app/_graphql/models.yml**
```yaml
MyProject\Models\Product:
fields:
title:
type: String
resolver: [ 'MyProject\Resolver', 'resolveSpecialTitle' ]
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.
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
You can use `*` 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' }
const newObj = {...oldObj, someProperty: 'custom'}
```
Here's an example:
@ -383,8 +403,8 @@ This block list applies for all operations (read, update, etc).
**app/_config/graphql.yml**
```yaml
SilverStripe\CMS\Model\SiteTree:
graphql_blacklisted_fields:
myPreviewTokenField: true
graphql_blacklisted_fields:
myPreviewTokenField: true
```
### Model configuration
@ -396,10 +416,10 @@ subsection.
### Customising the type name
Most DataObject classes are namespaced, so converting them to a type name ends up
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`.
of your `DataObject` as its typename (see: [`ClassInfo::shortName()`](api:SilverStripe/Core/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
@ -420,10 +440,6 @@ when the type name is derived from the class name. The most case for this
is the `Page` class, which is both at the root namespace and often in your
app namespace, e.g. `MyApp\Models\Page`.
#### The type formatter
The `type_formatter` is a callable that can be set on the `DataObjectModel` config. It takes
@ -435,15 +451,15 @@ Let's turn `MyProject\Models\Product` into the more specific `MyProjectProduct`
```yaml
modelConfig:
DataObject:
type_formatter: ['MyProject\Formatters', 'formatType' ]
type_formatter: ['MyProject\GraphQL\Formatter', 'formatType']
```
[info]
In the above example, `DataObject` is the result of the `DataObjectModel::getIdentifier()`. Each
model class must declare one of these.
In the above example, `DataObject` is the result of [`DataObjectModel::getIdentifier()`](api:SilverStripe\GraphQL\Schema\DataObject::getIdentifier()).
Each model class must declare one of these.
[/info]
Your formatting function could look something like:
The formatting function in your `MyProject\GraphQL\Formatter` class could look something like:
```php
public static function formatType(string $className): string
@ -461,16 +477,19 @@ public static function formatType(string $className): string
#### The type prefix
You can also add prefixes to all your DataObject types. This can be a scalar value or a callable,
You can also add prefixes to all your `DataObject` types. This can be a scalar value or a callable,
using the same signature as `type_formatter`.
*app/_graphql/config.yml*
**app/_graphql/config.yml**
```yaml
modelConfig:
DataObject
DataObject:
type_prefix: 'MyProject'
```
This would automatically set the type name for your `MyProject\Models\Product` class to `MyProjectProduct`
without needing to declare a `type_formatter`.
### Further reading
[CHILDREN]

View File

@ -1,6 +1,6 @@
---
title: DataObject query plugins
summary: Learn about some of the useful goodies that come pre-packaged with DataObject queries
title: `DataObject` query plugins
summary: Learn about some of the useful goodies that come pre-packaged with `DataObject` queries
---
# Working with DataObjects
@ -15,22 +15,22 @@ Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
## DataObject query plugins
## `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:
plugins to include, and for `DataObject` queries, these include:
* filter
* sort
* dbFieldArgs
* paginateList
* inheritance
* canView (read, readOne)
* firstResult (readOne)
* `filter`
* `sort`
* `dbFieldArgs`
* `paginateList`
* `inheritance`
* `canView` (read, readOne)
* `firstResult` (readOne)
When the `silverstripe/cms` module is installed, a plugin known as `getByLink` is also added.
Other modules, such as `silverstripe-versioned` may augment that list with even more.
Other modules, such as `silverstripe/versioned` may augment that list with even more.
### The pagination plugin
@ -60,6 +60,7 @@ query {
hasNextPage
hasPrevPage
}
}
}
```
@ -67,14 +68,15 @@ query {
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.
isn't something we do in Silverstripe CMS. You can ignore `edges.node` and just use `nodes` if
you want to.
[/notice]
#### Disabling pagination
Just set it to `false` in the configuration.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Models\ProductCategory:
operations:
@ -85,33 +87,35 @@ MyProject\Models\ProductCategory:
To disable pagination globally, use `modelConfig`:
*app/_graphql/modelConfig.yml*
**app/_graphql/config.yml**
```yaml
DataObject:
operations:
read:
plugins:
paginateList: false
modelConfig:
DataObject:
operations:
read:
plugins:
paginateList: false
```
### The filter plugin
The filter plugin (`SilverStripe\GraphQL\Schema\DataObject\Plugin\QueryFilter`) adds a
The filter plugin ([`QueryFilter`](api:SilverStripe\GraphQL\Schema\DataObject\Plugin\QueryFilter)) adds a
special `filter` argument to the `read` and `readOne` operations.
```yaml
```graphql
query {
readPages(
filter: { title: { eq: "Blog" } }
) {
nodes {
title
created
nodes {
title
created
}
}
}
```
In the above example, the `eq` is known as a *comparator*. There are several of these
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)
@ -134,9 +138,10 @@ query {
created: { gt: "2020-06-01", lte: "2020-09-01" }
}
) {
nodes {
title
created
nodes {
title
created
}
}
}
```
@ -161,21 +166,22 @@ query {
}
}
) {
nodes {
title
nodes {
title
}
}
}
```
Filters are only querying against the database by default,
it is not possible to filter by fields with custom resolvers.
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
By default, all fields on the DataObject, including relationships, are included. To customise
this, just add a `fields` config to the plugin definition:
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Models\ProductCategory:
fields:
@ -193,7 +199,7 @@ MyProject\Models\ProductCategory:
Just set it to `false` in the configuration.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Models\ProductCategory:
operations:
@ -204,18 +210,19 @@ MyProject\Models\ProductCategory:
To disable filtering globally, use `modelConfig`:
*app/_graphql/modelConfig.yml*
**app/_graphql/config.yml**
```yaml
DataObject:
operations:
read:
plugins:
filter: false
modelConfig:
DataObject:
operations:
read:
plugins:
filter: false
```
### The sort plugin
The sort plugin (`SilverStripe\GraphQL\Schema\DataObject\Plugin\QuerySort`) adds a
The sort plugin ([`QuerySort`](api:SilverStripe\GraphQL\Schema\DataObject\Plugin\QuerySort)) adds a
special `sort` argument to the `read` and `readOne` operations.
```graphql
@ -223,10 +230,10 @@ query {
readPages (
sort: { created: DESC }
) {
nodes {
title
created
}
nodes {
title
created
}
}
}
```
@ -242,20 +249,19 @@ query {
}
}
) {
nodes {
title
}
nodes {
title
}
}
}
```
#### Customising the sort fields
By default, all fields on the dataobject, including `has_one` relationships, are included.
By default, all fields on the DataObject, including `has_one` relationships, are included.
To customise this, just add a `fields` config to the plugin definition:
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Models\ProductCategory:
fields:
@ -273,7 +279,7 @@ MyProject\Models\ProductCategory:
Just set it to `false` in the configuration.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Models\ProductCategory:
operations:
@ -294,11 +300,11 @@ modelConfig:
sort: false
```
### The DBFieldArgs plugin {#dbfieldargs}
### The `DBFieldArgs` plugin {#dbfieldargs}
When fields are introspected from a model and reference a `DBField` instance,
they get populated with a default set of arguments that map to methods on that
`DBField` class, for instance `->Nice()` or `->LimitSentences(4)`.
`DBField` class, for instance `$field->Nice()` or `$field->LimitSentences(4)`.
Let's have a look at this query:
@ -328,7 +334,7 @@ The primary field types that are affected by this include:
#### All available arguments
##### DBText
##### `DBText`
* `format: CONTEXT_SUMMARY` (optional "limit" arg)
* `format: FIRST_PARAGRAPH`
@ -336,7 +342,7 @@ The primary field types that are affected by this include:
* `format: SUMMARY` (optional "limit" arg)
* `parseShortcodes: Boolean` (DBHTMLText only)
##### DBDate
##### `DBDate`
* `format: TIMESTAMP`
* `format: NICE`
@ -350,29 +356,28 @@ The primary field types that are affected by this include:
* `format: FULL`
* `format: CUSTOM` (requires `customFormat: String` arg)
##### DBTime
##### `DBTime`
* `format: TIMESTAMP`
* `format: NICE`
* `format: SHORT`
* `format: CUSTOM` (requires `customFormat: String` arg)
##### DBDecimal
##### `DBDecimal`
* `format: INT`
##### DBFloat
##### `DBFloat`
* `format: NICE`
* `format: ROUND`
* `format: NICE_ROUND`
#### Enum naming strategy and deduplication
By default, auto-generated Enum types will use as generic name as possible, which is `<FieldName>Enum`, e.g.
`OrderStatusEnum`. On occasion, this may collide with other types, e.g. `OptionsEnum`. In this case, the
second enum generated will use `<TypeName><FieldName>Enum`.
By default, auto-generated Enum types will use as generic a name as possible using the convention `<FieldName>Enum` (e.g.
`OrderStatusEnum`). On occasion, this may collide with other types (e.g. `OptionsEnum` is quite generic and likely to be used already).
In this case, the second enum generated will use `<TypeName><FieldName>Enum` (e.g. `MyTypeOptionsEnum`).
If an enum already exists with the same fields and name, it will be reused. For instance, if `OptionsEnum`
is found and has exactly the same defined values (in the same order) as the Enum being generated,
@ -382,7 +387,8 @@ it will be reused rather than proceeding to the deduplication strategy.
You can specify custom enum names in the plugin config:
```
**app/_graphql/config.yml**
```yaml
modelConfig:
DataObject:
plugins:
@ -396,7 +402,8 @@ modelConfig:
You can also specify enums to be ignored. (`ClassName` does this on all DataObjects to prevent inheritance
issues)
```
**app/_graphql/config.yml**
```yaml
modelConfig:
DataObject:
plugins:
@ -410,7 +417,7 @@ modelConfig:
### The getByLink plugin
When the `silverstripe/cms` module is installed (it is in most cases), a plugin called `getByLink`
will ensure that queries that return a single DataObject model (e.g. readOne) get a new query argument
will ensure that queries that return a single `DataObject` model (e.g. `readOne`) get a new query argument
called `link` (configurable on the `field_name` property of `LinkablePlugin`).
```graphql

View File

@ -1,6 +1,6 @@
---
title: DataObject operation permissions
summary: A look at how permissions work for DataObject queries and mutations
title: `DataObject` operation permissions
summary: A look at how permissions work for `DataObject` queries and mutations
---
# Working with DataObjects
@ -15,16 +15,16 @@ Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
## DataObject operation permissions
## `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.
Please see [Model-Level Permissions](/developer_guides/model/permissions/#model-level-permissions) for more information.
### Mutation permssions
[info]
When mutations fail due to permission checks, they throw a `PermissionsException`.
When mutations fail due to permission checks, they throw a [`PermissionsException`](api:SilverStripe\GraphQL\Schema\Exception\PermissionsException).
[/info]
For `create`, if a singleton instance of the record being created doesn't pass a `canCreate($member)` check,
@ -41,19 +41,16 @@ Query permissions are a bit more complicated, because they can either be in list
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
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($member)` 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
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]
@ -61,7 +58,7 @@ for `limit` and `offset` parameters.
Though not recommended, you can disable query permissions by setting their plugins to `false`.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
Page:
operations:

View File

@ -1,6 +1,6 @@
---
title: DataObject inheritance
summary: Learn how inheritance is handled in DataObject types
title: `DataObject` inheritance
summary: Learn how inheritance is handled in `DataObject` types
---
# Working with DataObjects
@ -15,12 +15,12 @@ Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
## DataObject inheritance
## `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, and we leverage both of them when
working with dataobjects.
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, and we leverage both of them when working with DataObjects.
### Key concept: Querying types that have descendants
@ -29,12 +29,12 @@ is no guarantee that each result will be of one specific type. Take this example
```graphql
query {
readPages {
nodes {
title
content
}
readPages {
nodes {
title
content
}
}
}
```
@ -43,13 +43,13 @@ when we need the `date` field on `BlogPage`?
```graphql
query {
readPages {
nodes {
title
content
date # fails!
}
readPages {
nodes {
title
content
date # fails!
}
}
}
```
@ -57,51 +57,54 @@ To solve this problem, the graphql module will automatically change these types
```graphql
query {
readPages {
nodes { # <--- [PageInterface]
title
content
}
readPages {
nodes { # <--- [PageInterface]
title
content
}
}
}
```
But what about when we want more than `title` and `content`? In some cases, we'll want fields that are specific to
`BlogPage`. When accessing fields for a specific implementation, we need to use an [inline fragment](https://graphql.org/learn/queries/#inline-fragments) to select them.
Now, in order to query fields that are specific to `BlogPage`, we need to use an
[inline fragment](https://graphql.org/learn/queries/#inline-fragments) to select them.
In the below example, we are querying `title` and `content` on all page types, but we only query `heroImage`
on `HomePage` objects, and we query `date` and `author` only for `BlogPage` objects.
```graphql
query {
readPages {
nodes {
title # Common field
content # Common field
... on HomePage {
heroImage {
url
}
}
... on BlogPage {
date
author {
firstName
}
}
readPages {
nodes {
title # Common field
content # Common field
... on HomePage {
heroImage {
url
}
}
... on BlogPage {
date
author {
firstName
}
}
}
}
}
```
So the fields that are common to every possible type in the result set can be directly selected (with no `...on`
So the fields that are common to every possible type in the result set can be directly selected (with no `...on`
syntax), because they're part of the common interface. They're guaranteed to exist on every type. But for fields
that only appear on some types, we need to be explicit.
But let's take this a step further. What if there's another class in between? Imagine this ancestry:
Now let's take this a step further. What if there's another class in between? Imagine this ancestry:
```
Page
-> EventPage
-> ConferencePage
-> WebinarPage
-> EventPage extends Page
-> ConferencePage extends EventPage
-> WebinarPage extends EventPage
```
We can use the intermediary interface `EventPageInterface` to consolidate fields that are unique to
@ -109,40 +112,41 @@ We can use the intermediary interface `EventPageInterface` to consolidate fields
```graphql
query {
readPages {
nodes {
title # Common to all types
content # Common to all types
... on EventPageInterface {
# Common fields for WebinarPage, ConferencePage, EventPage
numberOfTickets
featuredSpeaker {
firstName
email
}
}
... on WebinarPage {
zoomLink
}
... on ConferencePage {
venueSize
}
... on BlogPage {
date
author {
firstName
}
}
readPages {
nodes {
title # Common to all types
content # Common to all types
... on EventPageInterface {
# Common fields for WebinarPage, ConferencePage, EventPage
numberOfTickets
featuredSpeaker {
firstName
email
}
}
... on WebinarPage {
zoomLink
}
... on ConferencePage {
venueSize
}
... on BlogPage {
date
author {
firstName
}
}
}
}
}
```
You can think of interfaces in this context as abstractions of *parent classes*.
You can think of interfaces in this context as abstractions of *parent classes* - and the best part is
they're generated automatically. We don't need to manually define or implement the interfaces.
[info]
A good way to determine whether you need an inline fragment is to ask,
"Can this field appear on any other types in the query?" If the answer is yes, you want to use an interface,
A good way to determine whether you need an inline fragment is to ask
"can this field appear on any other types in the query?" If the answer is yes, you want to use an interface,
which is usually the parent class with the "Interface" suffix.
[/info]
@ -165,7 +169,7 @@ Here are the rules for how inheritance affects types and fields:
* Ancestors receive any fields exposed by their descendants, if applicable.
* Exposing a type applies all of its fields to descendants only if they are explicitly exposed also.
All of this is serviced by: `SilverStripe\GraphQL\Schema\DataObject\InheritanceBuilder`
All of this is serviced by: [`InheritanceBuilder`](api:SilverStripe\GraphQL\Schema\DataObject\InheritanceBuilder)
##### Example:
@ -181,7 +185,7 @@ GalleryPage:
urlSegment: true
```
This results in these two types being exposed with the fields as shown, but also results in a `Page` type:
This results in those two types being exposed with the fields as shown, but also results in a `Page` type:
```graphql
type Page {
@ -194,13 +198,13 @@ type Page {
#### Interface generation
Any type that's part of an inheritance chain will generate interfaces. Each applicable ancestral interface is added
Any type that's part of an inheritance chain will generate interfaces. Each applicable ancestral interface is added
to the type. Like the type inheritance pattern shown above, interfaces duplicate fields from their ancestors as well.
Additionally, a **base interface** is provided for all types containing common fields across the entire DataObject
Additionally, a **base interface** is provided for all types containing common fields across the entire `DataObject`
schema.
All of this is serviced by: `SilverStripe\GraphQL\Schema\DataObject\InterfaceBuilder`
All of this is serviced by: [`InterfaceBuilder`](api:SilverStripe\GraphQL\Schema\DataObject\InterfaceBuilder)
##### Example
@ -212,10 +216,11 @@ Page
-> WebinarPage extends EventsPage
```
This will create the following interfaces:
This will create the following interfaces (assuming the fields below are exposed):
```graphql
interface PageInterface {
id: ID!
title: String
content: String
}
@ -279,16 +284,16 @@ type Page implements PageInterface & DataObjectInterface {}
#### Interface assignment to queries
Queries, both at the root, and nested as fields on types, will have their types
Queries, both at the root and nested as fields on types, will have their types
updated if they refer to a type that has had any generated interfaces added to it.
```graphql
type Query {
readPages: [Page]
readPages: [Page]
}
type BlogPage {
download: File
download: File
}
```
@ -296,19 +301,20 @@ Becomes:
```graphql
type Query {
readPages: [PageInterface]
readPages: [PageInterface]
}
type BlogPage {
download: FileInterface
download: FileInterface
}
```
All of this is serviced by: `SilverStripe\GraphQL\Schema\DataObject\InterfaceBuilder`
All of this is serviced by: [`InterfaceBuilder`](api:SilverStripe\GraphQL\Schema\DataObject\InterfaceBuilder)
#### Elemental
This section refers to types added via `dnadesign/silverstripe-elemental`.
Almost by definition, content blocks are always abstractions. You're never going to query for a `BaseElement` type
Almost by definition, content blocks are always abstractions. You're never going to query for a `BaseElement` type
specifically. You're always asking for an assortment of its descendants, which adds a lot of polymorphism to
the query.
@ -336,6 +342,10 @@ query {
}
```
[info]
The above example shows a query for elements on all elemental pages - but for most situations you will
probably only want to query the elements on one page at a time.
[/info]
### Optional: Use unions instead of interfaces
@ -346,24 +356,21 @@ typically only be done for conceptual purposes.
To use unions, turn on the `useUnionQueries` setting.
**app/_graphql/config.yml**
```yaml
SilverStripe\GraphQL\Schema\Schema:
schemas:
default:
config:
modelConfig:
DataObject:
plugins:
inheritance:
useUnionQueries: true
modelConfig:
DataObject:
plugins:
inheritance:
useUnionQueries: true
```
This means that models that have descendants will create unions that include themselves and all of their descendants.
For queries that return those models, a union is put in its place.
Serviced by: `SilverStripe\GraphQL\Schema\DataObject\InheritanceUnionBuilder`
Serviced by: [`InheritanceUnionBuilder`](api:SilverStripe\GraphQL\Schema\DataObject\InheritanceUnionBuilder)
##### Example
#### Example
```
type Page implements PageInterface {}
@ -380,7 +387,7 @@ union PageInheritanceUnion = Page | BlogPage | EventsPage | ConferencePage | Web
union EventsPageInheritanceUnion = EventsPage | ConferencePage | WebinarPage
```
"Leaf" models like `BlogPage`, `ConferencePage`, and `WebinarPage` that have no exposed descendants will not create
"Leaf" models like `BlogPage`, `ConferencePage`, and `WebinarPage` that have no exposed descendants will not create
unions, as they are functionally useless.
This means that queries for `readPages` and `readEventsPages` will now return unions.
@ -426,11 +433,11 @@ We might query this with:
```graphql
query {
readProducts {
nodes {
price
}
readProducts {
nodes {
price
}
}
}
```
@ -445,35 +452,35 @@ class DigitalProduct extends Product
Now our query breaks:
```
```graphql
query {
readProducts {
nodes {
price # Error: Field "price" not found on ProductInheritanceUnion
}
readProducts {
nodes {
price # Error: Field "price" not found on ProductInheritanceUnion
}
}
}
```
We need to revise it:
```
```graphql
query {
readProducts {
nodes {
... on ProductInterface {
price
}
... on DigitalProduct {
downloadUrl
}
}
readProducts {
nodes {
... on ProductInterface {
price
}
... on DigitalProduct {
downloadUrl
}
}
}
}
```
Had we used interfaces, this wouldn't have broken, because the `price` field would have been on `ProductInterface`
and directly queryable (without the inline fragment).
If we use interfaces, this won't break because the `price` field will be on `ProductInterface`
which makes it directly queryable (without requiring the inline fragment).
### Further reading

View File

@ -17,16 +17,18 @@ Docs for the current stable version (3.x) can be found
## 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.
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.
You can also [disable](#disable) versioning for your schema if you don't need it.
See [model versioning](/developer_guides/model/versioning) for general information about versioning your DataObjects.
### Versioned plugins
There are several plugins provided by the `silverstripe-versioned` module that affect how versioned DataObjects
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 `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
@ -34,12 +36,12 @@ 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:
Defined in the [`VersionedDataObject`](api: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:
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)
@ -47,6 +49,12 @@ The `version` field on your DataObject will include the following fields:
* `liveVersion`: Boolean (True if the version is the one that is currently live)
* `latestDraftVersion`: Boolean (True if the version is the latest draft version)
[info]
Note that `author` and `publisher` are in relation to the given _version_ of the object - these are
not necessarily the same as the author and publisher of the _original_ record (i.e. the author may not
be the person who created the object, they're the person who saved a specific version of it).
[/info]
Let's look at it in context:
```graphql
@ -65,7 +73,7 @@ query readPages {
##### The `versions` field
The `versions` field on your DataObject will return a list of the `version` objects described above.
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
@ -95,8 +103,7 @@ fields:
The query will automatically apply the settings from the `versioning` input type to the query and affect
the resulting `DataList`.
#### The "unpublishOnDelete" plugin
#### The `unpublishOnDelete` plugin
This is mostly for internal use. It's an escape hatch for tidying up after a delete.
@ -111,9 +118,9 @@ by default. They include:
* `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: '*'`.
`DataObject`. They will all be included if you use `operations: '*'`.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Models\MyObject:
fields: '*'

View File

@ -3,7 +3,6 @@ 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]
@ -18,10 +17,13 @@ Docs for the current stable version (3.x) can be found
## 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:
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*
In this example, we are taking a property `content` (which will be defined as `Content` in php) and defining it
as `pageContent` for GraphQL queries and mutations.
**app/_graphql/models.yml**
```yaml
Page:
fields:
@ -35,12 +37,11 @@ When using explicit property mapping, you must also define an explicit type, as
no longer be inferred.
[/notice]
### Dot-separated accessors
Property mapping is particularly useful when using **dot syntax** to access fields.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Pages\Blog:
fields:
@ -50,9 +51,9 @@ MyProject\Pages\Blog:
property: 'Author.FirstName'
```
Fields on plural relationships will automatically convert to a `column` array:
Fields on `has_many` or `many_many` relationships will automatically convert to a `column` array:
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Pages\Blog:
fields:
@ -67,7 +68,7 @@ MyProject\Pages\Blog:
We can even use a small subset of **aggregates**, including `Count()`, `Max()`, `Min()` and `Avg()`.
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Models\ProductCategory:
fields:

View File

@ -20,7 +20,7 @@ For readability and ergonomics, you can take advantage of nested type definition
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*
**app/_graphql/models.yml**
```yaml
MyProject\Pages\Blog:
fields:
@ -36,7 +36,7 @@ MyProject\Pages\Blog:
Alternatively, we could flatten that out:
*app/_graphql/models.yml*
**app/_graphql/models.yml**
```yaml
MyProject\Pages\Blog:
fields:

View File

@ -1,22 +1,22 @@
---
title: Working with DataObjects
summary: Add DataObjects to your schema, expose their fields, read/write operations, and more
summary: Add DataObjects to your schema, expose their fields, add 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
and adding read/write operations. We'll also look at 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).
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

@ -20,7 +20,7 @@ Docs for the current stable version (3.x) can be found
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*
**app/_graphql/types.yml**
```yaml
Country:
fields:
@ -31,15 +31,15 @@ Country:
We've defined a type called `Country` that has two fields: `code` and `name`. An example record
could be something like:
```
```php
[
'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.
That's all we have to do for now! We'll need to tell GraphQL how to get this data, but first
we need to [building a custom query](building_a_custom_query) to see how we can use it.
### Further reading

View File

@ -16,26 +16,25 @@ Docs for the current stable version (3.x) can be found
## 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've defined the shape of our data, now we need 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*
**app/_graphql/schema.yml**
```yaml
types:
Country:
fields:
code: String!
name: String!
queries:
readCountries: '[Country]'
```
### Resolving fields
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.
need a **resolver** to tell the query where to get the data from. For this, we're going to
have to break out of the configuration layer and write some PHP code.
**app/src/Resolvers/MyResolver.php**
```php
namespace MyProject\Resolvers;
class MyResolver
{
public static function resolveCountries(): array
@ -57,24 +56,29 @@ class MyResolver
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
**app/_graphql/schema.yml**
```yaml
types:
Country:
fields:
code: String!
name: String!
queries:
readCountries:
type: '[Country]'
resolver: [ 'MyResolver', 'resolveCountries' ]
queries:
readCountries:
type: '[Country]'
resolver: [ 'MyProject\Resolvers\MyResolver', 'resolveCountries' ]
```
[notice]
Note the difference in syntax here between the `type` and the `resolver` - the type declaration
_must_ have quotes around it, because we are saying "this is a list of `Country` objects". The value
of this must be a yaml _string_. But the resolver must _not_ be surrounded in quotes. It is explicitly
a yaml array, so that PHP recognises it as a `callable`.
[/notice]
Now, we just have to build the schema:
`$ vendor/bin/sake dev/graphql/build schema=default`
`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.
### Testing the query
Let's test this out in our GraphQL IDE. If you have the [silverstripe/graphql-devtools](https://github.com/silverstripe/silverstripe-graphql-devtools)
module installed, just go to `/dev/graphql/ide` in your browser.
As you start typing, it should autocomplete for you.
@ -109,44 +113,46 @@ And the expected response:
```
[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.
Keep in mind that [plugins](../working_with_DataObjects/query_plugins)
don't apply in this context - at least without updating the resolver
to account for them. Most importantly this means you need to
implement your own `canView()` checks. It also means you need
to add your own filter functionality, such as [pagination](adding_pagination).
[/notice]
## Resolver Method Arguments
A resolver is executed in a particular query context,
which is passed into the method as arguments.
A resolver is executed in a particular query context, which is passed into the method as arguments.
* `$value`: An optional `mixed` value of the parent in your data graph.
Defaults to `null` on the root level, but can be useful to retrieve the object
when writing field-specific resolvers (see [Resolver Discovery](resolver_discovery))
* `$args`: An array of optional arguments for this field (which is different from the [Query Variables](https://graphql.org/learn/queries/#variables))
* `$context`: An arbitrary array which holds information shared between resolvers.
Use implementors of `SilverStripe\GraphQL\Schema\Interfaces\ContextProvider` to get and set
data, rather than relying on the array keys directly.
* `$info`: Data structure containing useful information for the resolving process (e.g. the field name).
See [Fetching Data](http://webonyx.github.io/graphql-php/data-fetching/) in the underlying PHP library for details.
* `mixed $value`: An optional value of the parent in your data graph.
Defaults to `null` on the root level, but can be useful to retrieve the object
when writing field-specific resolvers (see [Resolver Discovery](resolver_discovery)).
* `array $args`: An array of optional arguments for this field (which is different from the [Query Variables](https://graphql.org/learn/queries/#variables))
* `array $context`: An arbitrary array which holds information shared between resolvers.
Use implementors of [`ContextProvider`](api:SilverStripe\GraphQL\Schema\Interfaces\ContextProvider) to get and set
data, rather than relying on the array keys directly.
* [`?ResolveInfo`](api:GraphQL\Type\Definition\ResolveInfo)` $info`: Data structure containing useful information for the resolving process (e.g. the field name).
See [Fetching Data](http://webonyx.github.io/graphql-php/data-fetching/) in the underlying PHP library for details.
## Using Context Providers
The `$context` array can be useful to get access to the HTTP request,
retrieve the current member, or find out details about the schema.
You can use it through implementors of the `SilverStripe\GraphQL\Schema\Interfaces\ContextProvider` interface.
You can use it through implementors of the `ContextProvider` interface.
In the example below, we'll demonstrate how you could limit viewing the country code to
users with ADMIN permissions.
**app/src/Resolvers/MyResolver.php**
```php
namespace MyProject\Resolvers;
use GraphQL\Type\Definition\ResolveInfo;
use SilverStripe\GraphQL\QueryHandler\UserContextProvider;
use SilverStripe\Security\Permission;
class MyResolver
{
public static function resolveCountries($value = null, array $args = [], array $context = [], ?ResolveInfo $info = null): array
public static function resolveCountries(mixed $value = null, array $args = [], array $context = [], ?ResolveInfo $info = null): array
{
$member = UserContextProvider::get($context);
$canViewCode = ($member && Permission::checkMember($member, 'ADMIN'));
@ -171,7 +177,6 @@ it's going to get awfully laborious mapping all these resolvers. Let's clean thi
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

@ -17,10 +17,13 @@ Docs for the current stable version (3.x) can be found
## The resolver discovery pattern
When you define a query mutation, or any other field on a type, you can opt out of providing
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.
Let's start by registering a resolver class where we can define a bunch of these methods.
You can register as many classes as makes sense - and each resolver class can have multiple
resolver methods.
**app/_graphql/config.yml**
```yaml
@ -34,31 +37,36 @@ or many fields. How those functions will be discovered relies on the _resolver s
### Resolver strategy
Each schema config accepts a `resolverStrategy` property. This should map to a callable that will return
a method name given a class name, type name, and `Field` instance.
a method name given a class name, type name, and [`Field`](api:SilverStripe\GraphQL\Schema\Field\Field) instance.
```php
public static function getResolverMethod(string $className, ?string $typeName = null, ?Field $field = null): ?string;
class Strategy
{
public static function getResolverMethod(string $className, ?string $typeName = null, ?Field $field = null): ?string
{
// strategy logic here
}
}
```
#### The default resolver strategy
By default, all schemas use `SilverStripe\GraphQL\Schema\Resolver\DefaultResolverStrategy::getResolerMethod`
By default, all schemas use [`DefaultResolverStrategy::getResolverMethod()`](api:SilverStripe\GraphQL\Schema\Resolver\DefaultResolverStrategy::getResolverMethod())
to discover resolver functions. The logic works like this:
* Does `resolve<TypeName><FieldName>` exist?
* Yes? Invoke
* Yes? Return that method name
* No? Continue
* Does `resolve<TypeName>` exist?
* Yes? Invoke
* Yes? Return that method name
* No? Continue
* Does `resolve<FieldName>` exist?
* Yes? Invoke
* Yes? Return that method name
* No? Continue
* Does `resolve` exist?
* Yes? Invoke
* Yes? Return that method name
* No? Return null. This resolver cannot be discovered
Let's look at our query again:
```graphql
@ -69,19 +77,26 @@ query {
}
```
Imagine we have two classes registered under `resolvers` -- `ClassA` and `ClassB`
Imagine we have two classes registered under `resolvers` - `ClassA` and `ClassB`
The logic will go like this:
**app/_graphql/config.yml**
```yaml
resolvers:
- ClassA
- ClassB
```
* `ClassA::resolveCountryName`
* `ClassA::resolveCountry`
* `ClassA::resolveName`
* `ClassA::resolve`
* `ClassB::resolveCountryName`
* `ClassB::resolveCountry`
* `ClassB::resolveName`
* `ClassB::resolve`
* Return null.
The `DefaultResolverStrategy` will check for methods in this order:
* `ClassA::resolveCountryName()`
* `ClassA::resolveCountry()`
* `ClassA::resolveName()`
* `ClassA::resolve()`
* `ClassB::resolveCountryName()`
* `ClassB::resolveCountry()`
* `ClassB::resolveName()`
* `ClassB::resolve()`
* Return `null`.
You can implement whatever strategy you like in your schema. Just register it to `resolverStrategy` in the config.
@ -94,6 +109,7 @@ Let's add a resolver method to our resolver provider:
**app/src/Resolvers/MyResolvers.php**
```php
namespace MyApp\Resolvers;
class MyResolvers
{
@ -113,8 +129,14 @@ class MyResolvers
}
```
Now that we're using logic to discover our resolver, we can remove our resolver method declarations from the individual
queries and instead just register the resolver class.
Now that we're using logic to discover our resolver, we can clean up the config a bit.
**app/_graphql/config.yml**
```yaml
resolvers:
- MyApp\Resolvers\MyResolvers
```
**app/_graphql/schema.yml**
```yml
@ -122,16 +144,15 @@ Now that we're using logic to discover our resolver, we can clean up the config
readCountries: '[Country]'
```
Re-run the schema build, with a flush, and let's go!
`$ vendor/bin/sake dev/graphql/build schema=default flush=1`
Re-run the schema build, with a flush (because we created a new PHP class), 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.
ground between the rigor of hard coding everything at a query level, and the opacity of discovery logic.
**app/_graphql/schema.yml**
```yml
@ -140,13 +161,14 @@ ground between the rigor of hard coding everything and the opacity of discovery
fields:
name: String
code: String
fieldResolver: [ 'MyProject\MyResolver', 'resolveCountryField' ]
fieldResolver: [ 'MyProject\MyResolver', 'resolveCountryFields' ]
```
You'll need to do explicit checks for the `fieldName` in your resolver to make this work.
In this case the registered resolver method will be used to resolve any number of fields.
You'll need to do explicit checks for the field name in your resolver to make this work.
```php
public static function resolveCountryField($obj, $args, $context, ResolveInfo $info)
public static function resolveCountryFields($obj, $args, $context, ResolveInfo $info)
{
$fieldName = $info->fieldName;
if ($fieldName === 'image') {

View File

@ -26,6 +26,12 @@ way of influencing our query response:
'readCountries(limit: Int!)': '[Country]'
```
[hint]
In the above example, the `limit` argument is _required_ by making it non-nullable. If you want to be able
to get an un-filtered list, you can instead allow the argument to be nullable by removing the `!`:
`'readCountries(limit: Int)': '[Country]'`
[/hint]
We've provided the required argument `limit` to the query, which will allow us to truncate the results.
Let's update the resolver accordingly.
@ -45,10 +51,10 @@ Let's update the resolver accordingly.
return $results;
}
```
Now let's try our query again. This time, notice that the IDE is telling us we're missing a required argument.
We need to add the argument to our query:
```graphql
query {
@ -59,7 +65,7 @@ query {
}
```
This works pretty well, but maybe it's a bit over the top to *require* the `limit` argument. We want to optimise
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**
@ -68,7 +74,8 @@ performance, but we also don't want to burden the developer with tedium like thi
'readCountries(limit: Int = 20)': '[Country]'
```
Rebuild the schema, and notice that the IDE is no longer yelling at you for a `limit` argument.
Rebuild the schema and try the query again without adding a limit in the query. Notice that the IDE is no longer
yelling at you for a `limit` argument, but the result list is limited to the first 20 items.
Let's take this a step further by turning this in to a proper [paginated result](adding_pagination).

View File

@ -16,7 +16,8 @@ Docs for the current stable version (3.x) can be found
## Adding pagination
We've created a simple generic query for our `Country` type called `readCounties` that takes a `limit` argument.
So far in this section we've created a simple generic query for a `Country` type called `readCountries` that takes a
`limit` argument.
```graphql
query {
@ -29,14 +30,16 @@ query {
Let's take this a step further and paginate it using a plugin.
### The "paginate" 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.
If you're paginating a `DataList`, you might want to consider using models with read operations (instead of declaring
them as generic types with generic queries), which paginate by default using the `paginateList` plugin.
You can use generic typing and follow the below instructions too but it requires code that, for `DataObject` models,
you get for free.
[/notice]
Let's add the plugin to our query:
@ -48,10 +51,9 @@ Let's add the plugin to our query:
type: '[Country]'
plugins:
paginate: {}
```
Right now the plugin will add the necessary arguments to the query, build and update the return types. But
Right now the plugin will add the necessary arguments to the query, 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**
@ -88,13 +90,15 @@ public static function paginateCountries(array $context): Closure
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`
* Notice the new design pattern of a **context-aware resolver**. Since the plugin is 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`](api:SilverStripe\GraphQL\Schema\Plugin\PaginationPlugin)
can handle the rest. It will return an array including `edges`, `nodes`, and `pageInfo`.
* As long as we can do the work of counting and limiting the array, the `PaginationPlugin` can handle the rest. It will return an array including `edges`, `nodes`, and `pageInfo`.
Rebuild the schema and test it out:
Rebuild the schema, and test it out:
`vendor/bin/sake dev/graphql/build schema=default`
```graphql
query {

View File

@ -16,8 +16,12 @@ Docs for the current stable version (3.x) can be found
## 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
One of the great features of a schema-backed API is that it is self-documenting. If you use
the [silverstripe/graphql-devtools](https://github.com/silverstripe/silverstripe-graphql-devtools)
module you can see the documentation by navigating to /dev/graphql/ide in your browser anc clicking
on "DOCS" on the right.
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.
@ -26,16 +30,16 @@ 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
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
@ -43,14 +47,14 @@ 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
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

View File

@ -19,11 +19,10 @@ Docs for the current stable version (3.x) can be found
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 }`.
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.
@ -36,28 +35,28 @@ It's very easy to add enum types to your schema. Just use the `enums` section of
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.
`age: Int`. The types `Actor` and `Chef` would implement the `Person` interface, and therefore
querying for `Person` types can also give you `Actor` and `Chef` types in the result. Actors and
chefs must also have the `firstName`, `surname`, and `age` fields for such a query to work.
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:
Person:
fields:
firstName: String!
surname: String!
age: Int!
resolveType: [ 'MyProject\MyResolver', 'resolvePersonType' ]
```
Interfaces must define a `resolveType` resolver method to inform the interface
Interfaces must define a `resolve[Typename]Type` 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.
@ -94,7 +93,7 @@ non-discoverable and must be applied explicitly.
```php
public static function resolveArticleUnion(Article $object): string
{
if ($object->category === 'blogs')
if ($object->category === 'blogs') {
return 'Blog';
}
if ($object->category === 'news') {
@ -106,4 +105,3 @@ non-discoverable and must be applied explicitly.
### Further reading
[CHILDREN]

View File

@ -1,21 +1,22 @@
---
title: Working with generic types
summary: Break away from the magic of DataObject models and build types and queries from scratch.
summary: Break away from the magic of `DataObject` models and build types and queries from scratch.
icon: clipboard
---
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
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.
This is useful for situations where your data doesn't come from a `DataObject`, or where you have very specific
requirements for your GraphQL API that don't easily map to the schema of your `DataObject` classes.
[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.
Just because we won't be using DataObjects in this example doesn't mean you can't do it - you can absolutely
declare `DataObject` classes as generic types. You would lose a lot of the benefits of the `DataObject` model
in doing so, but this lower level API may suit your needs for very 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/),
@ -23,3 +24,5 @@ and report any issues at [github.com/silverstripe/silverstripe-graphql](https://
Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
[CHILDREN]

View File

@ -22,7 +22,7 @@ Some Silverstripe CMS resources have permission requirements to perform CRUD ope
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
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.
@ -34,22 +34,24 @@ resources are accessed.
### Default authentication
The `MemberAuthenticator` class is configured as the default option for authentication,
The [`MemberAuthenticator`](api:SilverStripe\GraphQL\Auth\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).**
not disabled the [CSRF Middleware](csrf_protection). (It is enabled 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 can be configured for GraphQL that
There is a [`BasicAuthAuthenticator`](api:SilverStripe\GraphQL\Auth\BasicAuthAuthenticator)
which can be configured for GraphQL that
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. Note that basic auth will bypass MFA authentication
so if MFA is enabled it is not recommended that you also use basic auth for graphql.
for CMS permission filtering, whereas the global [`BasicAuth`](api:SilverStripe\Security\BasicAuth) does not log the
member in or use it for model security. Note that basic auth will bypass MFA authentication
so if MFA is enabled it is not recommended that you also use basic auth for GraphQL.
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.
@ -60,7 +62,9 @@ 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
this can be done from "Edit HTTP Headers".
The `/dev/graphql/ide` endpoint in [silverstripe/graphql-devtools](https://github.com/silverstripe/silverstripe-graphql-devtools)
does not support custom HTTP headers at this point.
Your custom header should follow the following format:
@ -87,17 +91,18 @@ php -r 'echo base64_encode("hello:world");'
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 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
Authenticator classes need to implement the [`AuthenticatorInterface`](api:SilverStripe\GraphQL\Auth\AuthenticatorInterface)
interface, which requires you to define an `authenticate()` method to return a `Member` or `false`, and
and an `isApplicable()` method which tells the [`Handler`](api:SilverStripe\GraphQL\Auth\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 authentication for graphql will bypass Multi-Factor Authentication (MFA) if that's enabled. Using basic authentication for graphql is considered insecure if you are using MFA .
Note that basic authentication for GraphQL will bypass Multi-Factor Authentication (MFA) if that's enabled. Using basic authentication for GraphQL is considered insecure if you are using MFA.
[/notice]
```yaml

View File

@ -29,100 +29,94 @@ Once you have enabled CORS you can then control four new headers in the HTTP Res
1. **Access-Control-Allow-Origin.**
This lets you define which domains are allowed to access your GraphQL API. There are
4 options:
This lets you define which domains are allowed to access your GraphQL API. There are
4 options:
* **Blank**:
Deny all domains (except localhost)
* **Blank**: Deny all domains (except localhost)
```yaml
Allow-Origin:
```
```yaml
Allow-Origin:
```
* **'\*'**:
Allow requests from all domains.
* **'\*'**: Allow requests from all domains.
```yaml
Allow-Origin: '*'
```
```yaml
Allow-Origin: '*'
```
* **Single Domain**:
* **Single Domain**: Allow requests from one specific external domain.
Allow requests from one specific external domain.
```yaml
Allow-Origin: 'https://my.domain.com'
```
```yaml
Allow-Origin: 'https://my.domain.com'
```
* **Multiple Domains**: Allow requests from multiple specified external domains.
* **Multiple Domains**:
Allow requests from multiple specified external domains.
```yaml
Allow-Origin:
- 'https://my.domain.com'
- 'https://your.domain.org'
```
```yaml
Allow-Origin:
- 'https://my.domain.com'
- 'https://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:
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'
```
```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]
[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:
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'
```
```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:
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
```
```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.
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.
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:
This is set to empty by default but can be changed in YAML as in this example:
```yaml
Allow-Credentials: 'true'
```
```yaml
Allow-Credentials: 'true'
```
### Apply a CORS config to all GraphQL endpoints
@ -135,7 +129,7 @@ SilverStripe\GraphQL\Controller:
Allow-Headers: 'Authorization, Content-Type'
Allow-Methods: 'GET, POST, OPTIONS'
Allow-Credentials: 'true'
Max-Age: 600 # 600 seconds = 10 minutes.
Max-Age: 600 # 600 seconds = 10 minutes.
```
### Apply a CORS config to a single GraphQL endpoint

View File

@ -16,7 +16,7 @@ Docs for the current stable version (3.x) can be found
## CSRF tokens (required for mutations)
Even if your graphql endpoints are behind authentication, it is still possible for unauthorised
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.
@ -24,11 +24,12 @@ 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`.
By default, this module comes with a [`CSRFMiddleware`](api:SilverStripe\GraphQL\Middleware\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 CMS, CSRF tokens are most commonly stored in the session as `SecurityID`, or accessed through
the `SecurityToken` API, using `SecurityToken::inst()->getValue()`.
the [`SecurityToken`](api:SilverStripe\Security\SecurityToken) API, using `SecurityToken::inst()->getValue()`.
Queries do not require CSRF tokens.
@ -38,7 +39,6 @@ If you are using HTTP basic authentication or a token-based system like OAuth or
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\Core\Injector\Injector:
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:

View File

@ -20,15 +20,15 @@ Docs for the current stable version (3.x) can be found
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.
To disable that requirement, you can remove the [`HTTPMethodMiddleware`](api:SilverStripe\GraphQL\Middleware\HTTPMethodMiddleware)
from the [`QueryHandler`](api:SilverStripe\GraphQL\QueryHandler\QueryHandler).
```yaml
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
properties:
Middlewares:
httpMethod: false
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
properties:
Middlewares:
httpMethod: false
```
### Further reading

View File

@ -1,7 +1,7 @@
---
title: Security & best practices
icon: user-lock
summary: A guide to keeping your GraphQL API secure and accessible
icon: user-lock
---
# Security and best practices
@ -9,8 +9,6 @@ summary: A guide to keeping your GraphQL API secure and accessible
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/),
@ -18,3 +16,5 @@ and report any issues at [github.com/silverstripe/silverstripe-graphql](https://
Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
[CHILDREN]

View File

@ -20,7 +20,7 @@ Docs for the current stable version (3.x) can be found
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 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...
@ -29,46 +29,47 @@ Plugins are used to distribute reusable functionality across your schema. Some e
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 interfaces, unions, and merges ancestral fields.
* An `inheritedPlugins` plugin (a bit meta!) that merges plugins from ancestral types into descendants.
* The `DataObject` model (i.e. any `DataObject` based type) has:
* An `inheritance` plugin that builds the interfaces, unions, 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`)
* 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:
plugins, including:
* A `versioning` plugin that adds `version` fields to the dataobject type (if `silverstripe/versioned` is installed)
* 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 `admin` schema.
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.
#### 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
```
**app/_graphql/config.yml**
```yaml
modelConfig:
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.
You can override default plugins on your specific `DataObject` type and these changes will be inherited by descendants.
**app/_graphql/models.yml**
```yaml
**app/_graphql/models.yml**
```yaml
Page:
plugins:
inheritance: false
@ -77,8 +78,8 @@ MyProject\MyCustomPage: {} # now has no inheritance plugin
Likewise, you can do the same for operations:
**app/_graphql/models.yml**
```yaml
**app/_graphql/models.yml**
```yaml
Page:
operations:
read:
@ -89,7 +90,6 @@ MyProject\MyCustomPage:
read: true # has no readVersion plugin
```
### What plugins must do
There isn't a huge API surface to a plugin. They just have to:
@ -97,22 +97,21 @@ 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`
* Be registered with the [`PluginRegistry`](api:SilverStripe\GraphQL\Schema\Registry\PluginRegistry)
### Available plugin interfaces
Plugin interfaces are all found in the namespace `SilverStripe\GraphQL\Schema\Interfaces`
Plugin interfaces are all found in the `SilverStripe\GraphQL\Schema\Interfaces` namespace
* `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
* [`SchemaUpdater`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater): Make a one-off, context-free update to the schema
* [`QueryPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\QueryPlugin): Update a generic query
* [`MutationPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\MutationPlugin): Update a generic mutation
* [`TypePlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\TypePlugin): Update a generic type
* [`FieldPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\FieldPlugin): Update a field on a generic type
* [`ModelQueryPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\ModelQueryPlugin): Update queries generated by a model, e.g. `readPages`
* [`ModelMutationPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\ModelMutationPlugin): Update mutations generated by a model, e.g. `createPage`
* [`ModelTypePlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\ModelTypePlugin): Update types that are generated by a model
* [`ModelFieldPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\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,
@ -120,7 +119,7 @@ just for different types. It's pretty easy to navigate if you know what you want
### Registering plugins
Plugins have to be registered with Injector.
Plugins have to be registered to the `PluginRegistry` via the `Injector`.
```yaml
SilverStripe\Core\Injector\Injector:
@ -129,10 +128,6 @@ SilverStripe\Core\Injector\Injector:
- '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

View File

@ -20,9 +20,12 @@ Docs for the current stable version (3.x) can be found
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.
Because it applies to fields, we'll want to implement the [`FieldPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\FieldPlugin)
interface for this.
```php
namespace MyProject\Plugins;
class Truncator implements FieldPlugin
{
public function getIdentifier(): string
@ -37,10 +40,10 @@ class Truncator implements FieldPlugin
}
```
Now we've added an argument to any field that implements the `truncate` plugin. This is good, but it really
Now we've added an argument to any field that uses 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_.
For that, we'll need to augment our plugin with some _afterware_.
```php
public function apply(Field $field, Schema $schema, array $config = [])

View File

@ -17,14 +17,14 @@ Docs for the current stable version (3.x) can be found
## 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
`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.
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
* DataObjects that are geocodable should always expose their lat/lon fields for GraphQL queries
* `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.
@ -33,9 +33,13 @@ Let's get started.
### Step 1: Ensure DataObjects expose lat/lon fields
Since we're dealing with DataObjects, we'll need a `ModelTypePlugin`.
Since we're dealing with DataObjects, we'll need to implement a [`ModelTypePlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\ModelTypePlugin).
```php
namespace MyProject\Plugins;
//...
class GeocodableDataObject implements ModelTypePlugin
{
public function getIdentifier(): string
@ -47,7 +51,7 @@ class GeocodableDataObject implements ModelTypePlugin
{
$class = $type->getModel()->getSourceClass();
// sanity check that this is a dataobject
// 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',
@ -66,14 +70,28 @@ class GeocodableDataObject implements ModelTypePlugin
}
```
Now all dataobjects that have the extension will be forced to expose their lat/lon fields.
And register the plugin:
```yaml
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\Schema\Registry\PluginRegistry:
constructor:
- 'MyProject\Plugins\GeocodableDataObject'
```
Once we've [applied the plugin](#step-5-apply-the-plugins), all DataObjects that have the `Geocodable` 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.
We want any `readXXX` query to include a `within` parameter if it's for a geocodable `DataObject`.
For this, we're going to implement [`ModelQueryPlugin`](api:SilverStripe\GraphQL\Schema\Interfaces\ModelQueryPlugin),
because this is for queries generated by a model.
```php
namespace MyProject\Plugins;
//...
class GeocodableQuery implements ModelQueryPlugin
{
public function getIdentifier(): string
@ -95,7 +113,17 @@ class GeocodableQuery implements ModelQueryPlugin
}
```
Now our read queries will have a new parameter:
Register the new plugin
```yaml
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\Schema\Registry\PluginRegistry:
constructor:
- 'MyProject\Plugins\GeocodableDataObject'
- 'MyProject\Plugins\GeocodableQuery'
```
Now after we [apply the plugins](#step-5-apply-the-plugins), our read queries will have a new parameter:
```graphql
query readEvents(within: ...)
@ -119,15 +147,16 @@ query {
}
}
}
```
### 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.
We'll need this `SearchRadiusInput` to be shared across queries. It's not specific to any `DataObject`.
For this, we can implement [`SchemaUpdater`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater).
For tidiness, let's just to this in the same `GeocodeableQuery` class, since they share concerns.
```php
class GeocodableQuery implements ModelQueryPlugin, SchemUpdater
class GeocodableQuery implements ModelQueryPlugin, SchemaUpdater
{
//...
@ -167,12 +196,11 @@ their functionality without a massive hack. This is where the idea of **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`
In this case, we're going to be filtering our `DataList` procedurally and transforming it into an array.
We need to know that things like filters and sort have already been applied, because they expect a `DataList`
instance. So we'll need to do this fairly late in the chain. Afterware makes the most sense.
```php
```php
public function apply(ModelQuery $query, Schema $schema, array $config = []): void
{
$class = $query->getModel()->getSourceClass();
@ -200,7 +228,6 @@ public static function applyRadius($result, array $args): array
}
```
Looking good!
But there's still one little gotcha. This is likely to be run after pagination has been executed, so our
@ -234,25 +261,13 @@ 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,
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
### Step 5: Apply the plugins
Back to Injector:
```yaml
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\Schema\Registry\PluginRegistry:
constructor:
- 'MyProject\Plugins\GeocodableQuery'
- 'MyProject\Plugins\GeocodableDataObject'
```
### Step 6: Apply the plugins
We can apply the plugins to queries and dataobjects one of two ways:
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.
@ -275,12 +290,13 @@ MyProject\Models\Event:
```
This can get pretty verbose, so you might just want to register them as default plugins for all DataObjects
and their `read` operations.
and their `read` operations. In this case we've already added logic within the plugin itself to skip
DataObjects that don't have the `GeoCodable` extension.
#### Apply by default
```yaml
# apply the DataObject plugin
# apply the `DataObject` plugin
SilverStripe\GraphQL\Schema\DataObject\DataObjectModel:
default_plugins:
geocode: true

View File

@ -1,6 +1,7 @@
---
title: Plugins
summary: Learn what plugins are and how you can use them to extend your schema
icon: file-code
---
# Plugins
@ -9,10 +10,6 @@ Plugins play a critical role in distributing reusable functionality across your
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/),
@ -20,3 +17,5 @@ and report any issues at [github.com/silverstripe/silverstripe-graphql](https://
Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
[CHILDREN]

View File

@ -16,21 +16,21 @@ Docs for the current stable version (3.x) can be found
## Adding a custom model
The only point of contact the `silverstripe-graphql` schema has with
Silverstripe specifically is through the `DataObjectModel` adapter
The only point of contact the `silverstripe/graphql` schema has with
the Silverstripe ORM 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
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.
of what a non-`DataObject` model might be.
### SchemaModelInterface
Models must implement the `SchemaModelInterface`, which has some
hefty requirements. Let's walk through them:
Models must implement the [`SchemaModelInterface`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaModelInterface),
which has a lot of methods to implement. Let's walk through them:
* `getIdentifier(): string`: A unique identifier for this model type,
e.g. 'DataObject'
@ -42,7 +42,7 @@ infer the type. If the field doesn't exist, return `null`
the source class)
* `getDefaultResolver(?array $context = []): ResolverReference`:
Get the generic resolver that should be used for types that are built
with this model.
with this model.
* `getSourceClass(): string`: Get the name of the class that builds
the type, e.g. `MyDataObject`
* `getAllFields(): array`: Get all available fields on the object
@ -52,60 +52,33 @@ model type.
In addition, models may want to implement:
* `OperationProvider` (if your model creates operations, like
* [`OperationProvider`](api:SilverStripe\GraphQL\Schema\Interfaces\) (if your model creates operations, like
read, create, etc)
* `DefaultFieldsProvider` (if your model provides a default list
* [`DefaultFieldsProvider`](api:SilverStripe\GraphQL\Schema\Interfaces\) (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.
to look through how [`DataObjectModel`](api:SilverStripe\GraphQL\Schema\DataObject\DataObjectModel) implements all these methods.
### SchemaModelCreatorInterface
Given a class name, create an instance of `SchemaModelInterface`.
Given a class name, create an instance of [`SchemaModelCreatorInterface`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaModelCreatorInterface).
This layer of abstraction is necessary because we can't assume that
all `SchemaModelInterfaces` will accept a class name in their
all implementations of `SchemaModelCreatorInterface` 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);
}
}
```
Look at the [`ModelCreator`](api:SilverStripe\GraphQL\Schema\DataObject\ModelCreator) implementation
for a good example of how this works.
### Registering your model creator
Just add it to the registry:
**app/_graphql/config.yml
**app/_graphql/config.yml**
```yaml
modelCreators:
- 'SilverStripe\GraphQL\Schema\DataObject\ModelCreator'

View File

@ -18,7 +18,7 @@ Docs for the current stable version (3.x) can be found
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.
an implementation of [`OperationProvider`](api:SilverStripe\GraphQL\Schema\Interfaces\OperationProvider) and registering it.
Let's build a new operation that **duplicates** DataObjects.
@ -41,6 +41,7 @@ class DuplicateCreator implements OperationCreator
'dataClass' => $model->getSourceClass(),
]);
}
}
```
We add **resolver context** to the mutation because we need to know
@ -50,7 +51,7 @@ static function.
The signature for resolvers with context is:
```php
public static function (array $context): Closure;
public static function (array $context): Closure
```
We use the context to pass to a function that we'll create dynamically.
@ -65,13 +66,13 @@ public static function resolve(array $resolverContext = []): Closure
return null;
}
return DataObject::get_by_id($dataClass, $args['id'])
->duplicate();
->duplicate();
};
}
```
Now, just add the operation to the `DataObjectModel` configuration
to make it available to all DataObject types.
to make it available to all `DataObject` types.
**app/_graphql/config.yml**
```yaml

View File

@ -21,44 +21,50 @@ 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 accommodates
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.
The middleware API in the `silverstripe/graphql` module is separate from other common middleware
APIs in Silverstripe CMS, such as `HTTPMiddleware`. The two are not interchangable.
[/notice]
The signature for middleware looks like this:
The signature for middleware (defined in [`QueryMiddleware`](api:SilverStripe\GraphQL\Middleware\QueryMiddleware)) looks like this:
```php
public function process(Schema $schema, $query, $context, $vars, callable $next)
public function process(Schema $schema, string $query, array $context, array $vars, callable $next)
```
* `$schema`: The underlying [Schema](http://webonyx.github.io/graphql-php/type-system/schema/) object.
Useful to inspect whether types are defined in a schema.
* `$query`: The raw query string.
* `$context`: An arbitrary array which holds information shared between resolvers.
Use implementors of `SilverStripe\GraphQL\Schema\Interfaces\ContextProvider` to get and set
data, rather than relying on the array keys directly.
* `$vars`: An array of (optional) [Query Variables](https://graphql.org/learn/queries/#variables).
* `$next`: A callable referring to the next middleware in the chain
The return value should be [`ExecutionResult`](api:GraphQL\Executor\ExecutionResult) or an `array`.
* `$schema`: The underlying [Schema](http://webonyx.github.io/graphql-php/type-system/schema/) object.
Useful to inspect whether types are defined in a schema.
* `$query`: The raw query string.
* `$context`: An arbitrary array which holds information shared between resolvers.
Use implementors of [`ContextProvider`](api:SilverStripe\GraphQL\Schema\Interfaces\ContextProvider) to get and set
data, rather than relying on the array keys directly.
* `$vars`: An array of (optional) [Query Variables](https://graphql.org/learn/queries/#variables).
* `$next`: A callable referring to the next middleware in the chain
Let's write a simple middleware that logs our queries as they come in.
```php
namespace MyProject\Middleware;
use SilverStripe\GraphQL\Middleware\QueryMiddleware;
use SilverStripe\GraphQL\QueryHandler\UserContextProvider;
use GraphQL\Type\Schema;
//etc
class LoggingMiddleware implements Middleware
class LoggingMiddleware implements QueryMiddleware
{
public function process(Schema $schema, $query, $context, $vars, callable $next)
public function process(Schema $schema, string $query, array $context, array $vars, callable $next)
{
$member = UserContextProvider::get($context);
Injector::inst()->get(LoggerInterface::class)
->info(sprintf(
->info(sprintf(
'Query executed: %s by %s',
$query,
$member ? $member->Title : '<anonymous>';
@ -72,7 +78,6 @@ class LoggingMiddleware implements Middleware
Now we can register the middleware with our query handler:
```yaml
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
@ -80,4 +85,3 @@ Now we can register the middleware with our query handler:
Middlewares:
logging: '%$MyProject\Middleware\LoggingMiddleware'
```

View File

@ -1,14 +1,13 @@
---
title: Extending the schema
summary: Add new functionality to the schema
icon: code
---
In this section of the documentation, we'll look at some advanced
features for developers who want to extend their GraphQL server
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/),
@ -16,3 +15,5 @@ and report any issues at [github.com/silverstripe/silverstripe-graphql](https://
Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
[CHILDREN]

View File

@ -17,29 +17,34 @@ Docs for the current stable version (3.x) can be found
## Debugging the generated code
By default, the generated PHP code is put into obfuscated classnames and filenames to prevent poisoning the search
tools within IDEs. Without this, you can search for something like "Page" in your IDE and get both a generated GraphQL type (probably not what you want) and a DataObject (more likely what you want) in the results and have no easy way of differentiating between the two.
tools within IDEs. Without this, you can search for something like "Page" in your IDE and get both a generated GraphQL
type (probably not what you want) and a `SiteTree` subclass (more likely what you want) in the results and have no easy way
of differentiating between the two.
When debugging, however, it's much easier if these classnames are human-readable. To turn on debug mode, add `DEBUG_SCHEMA=1` to your environment file and the classnames and filenames in the generated code directory will match their type names.
When debugging, however, it's much easier if these classnames are human-readable. To turn on debug mode, add `DEBUG_SCHEMA=1`
to your environment file. The classnames and filenames in the generated code directory will then match their type names.
[warning]
Take care not to use `DEBUG_SCHEMA=1` as an inline environment variable to your build command, e.g. `DEBUG_SCHEMA=1 vendor/bin/sake dev/graphql/build` because any activity that happens at run time, e.g. querying the schema will fail, since the environment variable is no longer set.
Take care not to use `DEBUG_SCHEMA=1` as an inline environment variable to your build command, e.g.
`DEBUG_SCHEMA=1 vendor/bin/sake dev/graphql/build` because any activity that happens at run time, e.g. querying the schema
will fail, since the environment variable is no longer set.
[/warning]
In live mode, full obfuscation kicks in and the filenames become unreadable. You can only determine the type they map
to by looking at the generated classes and finding the `// @type:<typename>` inline comment, e.g. `// @type:Page`.
This obfuscation is handled by the `NameObfuscator` interface. See the `config.yml` file in the GraphQL module for
the various implementations, which include:
This obfuscation is handled by the [`NameObfuscator`](api:SilverStripe\GraphQL\Schema\Storage\NameObfuscator) interface.
* `NaiveNameObfuscator`: Filename/Classname === Type name
* `HybridNameObfuscator`: Filename/Classname is a mix of the typename and a hash (default).
* `HashNameObfuscator`: Filename/Classname is a md5 hash of the type name (non-dev only).
There are various implementations:
* [`NaiveNameObfuscator`](api:SilverStripe\GraphQL\Schema\Storage\NaiveNameObfuscator): Filename/Classname === Type name (debug only)
* [`HybridNameObfuscator`](api:SilverStripe\GraphQL\Schema\Storage\HybridNameObfuscator): Filename/Classname is a mix of the typename and a md5 hash (default).
* [`HashNameObfuscator`](api:SilverStripe\GraphQL\Schema\Storage\HashNameObfuscator): Filename/Classname is a md5 hash of the type name (non-dev only).
## Getting the type name for a model class
Often times, you'll need to know the name of the type given a class name. There's a bit of context to this.
### Getting the type name from within your app
If you need the type name during normal execution of your app, e.g. to display in your UI, you can rely
@ -52,24 +57,24 @@ SchemaBuilder::singleton()->read('default')->getTypeNameForClass($className);
## 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
on bandwidth, as the client doesn't need to put a fully expressed query in the request body - they can use 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:
[`PersistedQueryMappingProvider`](api: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
* [`FileProvider`](api:SilverStripe\GraphQL\PersistedQuery\FileProvider): Store your queries in a flat JSON file on the local filesystem.
* [`HTTPProvider`](api:SilverStripe\GraphQL\PersistedQuery\HTTPProvider): Store your queries on a remote server and reference a JSON file by URL.
* [`JSONStringProvider`](api:SilverStripe\GraphQL\PersistedQuery\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.
Note that each schema gets its own set of persisted queries. In these examples, we're using the `default` schema.
[/notice]
#### FileProvider
@ -83,7 +88,6 @@ SilverStripe\Core\Injector\Injector:
default: '/var/www/project/query-mapping.json'
```
A flat file in the path `/var/www/project/query-mapping.json` should contain something like:
```json
@ -136,11 +140,12 @@ Note that if you pass `query` along with `id`, an exception will be thrown.
## 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.
The [`QueryCachingMiddleware`](api:SilverStripe\GraphQL\Middleware\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`
To implement query caching, add the middleware to your `QueryHandler`
```yaml
SilverStripe\Core\Injector\Injector:
@ -151,7 +156,7 @@ SilverStripe\Core\Injector\Injector:
cache: '%$SilverStripe\GraphQL\Middleware\QueryCachingMiddleware'
```
And you will also need to apply an extension to all DataObjects:
And you will also need to apply the [QueryRecorderExtension](api:SilverStripe\GraphQL\Extensions\QueryRecorderExtension) extension to all DataObjects:
```yaml
SilverStripe\ORM\DataObject:
@ -163,16 +168,15 @@ SilverStripe\ORM\DataObject:
This feature is experimental, and has not been thoroughly evaluated for security. Use at your own risk.
[/warning]
## Schema introspection {#schema-introspection}
Some GraphQL clients such as [Apollo](http://apollographql.com) require some level of introspection
into the schema. The `SchemaTranscriber` class will persist this data to a static file in an event
into the schema. The [`SchemaTranscriber`](api:SilverStripe\GraphQL\Schema\Services\SchemaTranscriber)
class will persist this data to a static file in an event
that is fired on completion of the schema build. This file can then be consumed by a client side library
like Apollo. The `silverstripe/admin` module is built to consume this data and expects it to be in a
web-accessible location.
```json
{
"data":{
@ -187,9 +191,10 @@ web-accessible location.
]
}
}
}
```
By default, the file will be stored in `public/_graphql`. Files are only generated for the `silverstripe/admin` module.
By default, the file will be stored in `public/_graphql/`. Files are only generated for the `silverstripe/admin` module.
If you need these types for your own uses, add a new handler:

View File

@ -1,16 +1,16 @@
---
title: Architecture Diagrams
summary: A visual overview of the architecture and design of GraphQL 4
icon: sitemap
---
## GraphQL 3 vs GraphQL 4
![](../../_images/graphql/graphql3_vs_4.png)
![A high-level view of the differences between GraphQL v3 and v4 lifecycle](../../_images/graphql/graphql3_vs_4.png)
In GraphQL 3, the schema is built at request time, adding significant overhead to every API request. As the schema grows, this "Generate Schema" process becomes slower, making scalability a real problem.
In GraphQL 4, the schema is generated in a separate task, which generates code generation artefacts. These artefacts are executed at request time, meaning the schema itself imposes no penalty on the response time.
In GraphQL 4, the schema is generated during a build step, which generates code generation artefacts. These artefacts are executed at request time, meaning the schema itself imposes no penalty on the response time.
[info]
A useful analog to these two different approaches is a dynamic website versus a static website. In the case of the former, the PHP process is doing work on every single request. In the case of the latter, it does a lot of work once, in a separate context, in exchange for doing zero work on every page request.
@ -18,15 +18,15 @@ A useful analog to these two different approaches is a dynamic website versus a
## The build process
![](../../_images/graphql/build_process.png)
![A high-level view of the GraphQL v4 build process](../../_images/graphql/build_process.png)
* **dev/graphql/build**: This is the task that builds the schema. It also runs as a side effect of `dev/build` as a fallback. It accepts a `schema` parameter if you only want to build one schema.
* **dev/graphql/build**: This is the command that builds the schema. It also runs as a side effect of `dev/build` as a fallback. It accepts a `schema` parameter if you only want to build one schema.
* **Schema Factory**: This class is responsible for rebuilding a schema or fetching an existing one (i.e. as cached generated code)
* **Schema**: The most central class that governs the composition of your GraphQL schema and all of the connected services. It is largely a value object hydrated by config files and executable PHP code.
* **Plugins**: Plugins are the primary input for mutating the schema through thirdparty code. They can also be used in app code to augment core features, e.g. default resolvers for dataobjects.
* **Plugins**: Plugins are the primary input for mutating the schema through thirdparty code. They can also be used in app code to augment core features, e.g. default resolvers for DataObjects.
* **Storable Schema**: A value object that is agnostic of domain-specific entities like plugins and models, and just contains the finalised set of types, queries, mutations, interfaces, unions, and scalars. It cannot be mutated once created.
@ -34,7 +34,7 @@ A useful analog to these two different approaches is a dynamic website versus a
## The request process
![](../../_images/graphql/request_process.png)
![A high-level view of what happens during a GraphQL request](../../_images/graphql/request_process.png)
There are two key processes that happen at request time. Although they're run in serial, they rely on state, so they're depicted above as separate paths.
@ -46,11 +46,11 @@ The concept of the "resolver stack" is illustrated later in this document.
## Schema composition
![](../../_images/graphql/schema_composition.png)
![A diagram of what makes up a GraphQL schema](../../_images/graphql/schema_composition.png)
The `Schema` class is largely a value object that serves as the air traffic controller for the creation of a storable schema (i.e. generated code). Most of what it contains will be familiar to anyone with a basic understanding of GraphQL -- types, mutations, queries, etc. The magic, however, is in its nonstandard components: models and config.
The `Schema` class is largely a value object that serves as the air traffic controller for the creation of a storable schema (i.e. generated code). Most of what it contains will be familiar to anyone with a basic understanding of GraphQL - types, mutations, queries, etc. The magic, however, is in its nonstandard components: models and config.
Models are the layers of abstraction that create plain types and queries based on dataobjects. Imagine these few lines of config:
Models are the layers of abstraction that create plain types and queries based on DataObjects. Imagine these few lines of config:
```yaml
App\Models\MyModel:
@ -60,17 +60,17 @@ App\Models\MyModel:
read: true
```
It is the model's job to interpret what `*` or "all fields" means in that context (e.g. looking at `$db`, `$has_one`, etc). It also can create a read query for that dataobject with the simple `read: true` directive, and adding something `query readMyDataObjects` to the schema for you. Models are described in more detail below. There is also a lot more to learn about the model layer in the [Working with DataObjects](../working_with_dataobjects) section.
It is the model's job to interpret what `*` or "all fields" means in that context (e.g. looking at `$db`, `$has_one`, etc). It also can create a read query for that `DataObject` with the simple `read: true` directive, and adding something `query readMyDataObjects` to the schema for you. Models are described in more detail below. There is also a lot more to learn about the model layer in the [Working with DataObjects](../working_with_DataObjects) section.
The nonstandard "config" component here contains arbitrary directives, most of which influence the behaviour of models -- for instance, adding plugins, and influencing how resolvers operate.
The nonstandard "config" component here contains arbitrary directives, most of which influence the behaviour of models - for instance adding plugins and influencing how resolvers operate.
The primary role of the `Schema` class is to create a "storable schema" -- a readonly object that contains only standard GraphQL components. That is, all models have been transformed into plain types, queries, mutations, interfaces, etc., and the schema is ready to encode.
The primary role of the `Schema` class is to create a "storable schema" - a readonly object that contains only standard GraphQL components. That is, all models have been transformed into plain types, queries, mutations, interfaces, etc., and the schema is ready to encode.
## Models and model types
![](../../_images/graphql/models.png)
![A diagram showing how GraphQL type schema for DataObjects is generated](../../_images/graphql/models.png)
Model types are created by providing a class name to the schema. From there, it asks the `Model Creator` service to create a model for that class name. This may seem like an unnessary layer of abstraction, but in theory, models could be based on classes that are not dataobjects, and in such a case a new model creator would be required.
Model types are created by providing a class name to the schema. From there, it asks the `Model Creator` service to create a model for that class name. This may seem like an unnessary layer of abstraction, but in theory, models could be based on classes that are not DataObjects, and in such a case a new model creator would be required.
The model type composes itself by interrogating the model, an implementation of `SchemaModelInterface`. This will almost always be `DataObjectModel`. The model is responsible for solving domain-specific problems pertaining to a Silverstripe project, including:
@ -83,15 +83,15 @@ All model types eventually become plain GraphQL types when the `Schema` class cr
## Plugins
![](../../_images/graphql/plugins.png)
![A diagram showing a few of the plugin interfaces and what they are used for](../../_images/graphql/plugins.png)
The plugin layer is likely to be refactored before stablisation, but the current architecture is presented above.
As explained in [available plugin interfaces](../plugins/overview#available-plugin-interfaces), there are a lot of interfaces available for implementing a plugin. This is because you will need different types passed into the `apply()` method depending on what the plugin is for. This is illustrated in the diagram above.
## Resolver composition
![](../../_images/graphql/resolver_composition.png)
![A diagram showing how multiple resolvers can be used to get data for a single query](../../_images/graphql/resolver_composition.png)
Injecting behaviour into resolvers is one of the main ways the schema can be customised. For instance, if you add a new argument to a query, the standard dataobject resolver will not know about it, so you'll want to write your own code to handle that argument. You could overwrite the entire resolver, but then you would lose key functionality from other plugins, like pagination, sort, and filtering.
Injecting behaviour into resolvers is one of the main ways the schema can be customised. For instance, if you add a new argument to a query, the standard `DataObject` resolver will not know about it, so you'll want to write your own code to handle that argument. You could overwrite the entire resolver, but then you would lose key functionality from other plugins, like pagination, sort, and filtering.
To this end, resolvers are a product of composition. Each bit of functionality is just another resolver in the "stack." The stack passes the result of the previous resolver to the next resolver, while the other three parameters, `$args, $context, $info` are immutable.
@ -99,9 +99,9 @@ This pattern allows, for instance, filter plugin to run `$obj = $obj->filter(...
### Resolver context
![](../../_images/graphql/resolver_context.png)
![A diagram showing how some resolvers will be passed contextual information about the query, while others will not](../../_images/graphql/resolver_context.png)
Sometimes, a resolver needs to be used in multiple contexts, for instance, a generic "read" resolver for a dataobject that simply runs `DataList::create($className)`. That `$className` parameter needs to come from somewhere. Normally we would use some kind of state on an instance, but because resolver methods must be static, we don't have that option. This gets really tricky.
Sometimes, a resolver needs to be used in multiple contexts, for instance, a generic "read" resolver for a `DataObject` that simply runs `DataList::create($className)`. That `$className` parameter needs to come from somewhere. Normally we would use some kind of state on an instance, but because resolver methods must be static, we don't have that option. This gets really tricky.
To solve this problem, we can use "resolver context".
@ -123,10 +123,3 @@ public static function resolve(array $resolverContext)
```
As illustrated above, some resolvers in the stack can be provided context, while others may not.

View File

@ -4,9 +4,7 @@ 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.
For more information on GraphQL, visit its [documentation site](https://graphql.org).
[CHILDREN includeFolders]
For more information on GraphQL in general, visit its [documentation site](https://graphql.org).
[alert]
You are viewing docs for a pre-release version of silverstripe/graphql (4.x).
@ -15,3 +13,5 @@ and report any issues at [github.com/silverstripe/silverstripe-graphql](https://
Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
[CHILDREN includeFolders]

View File

@ -1,9 +1,9 @@
---
title: Upgrading from GraphQL 3
summary: A high-level view of what you'll need to change when upgrading to GraphQL 4
title: Upgrading to GraphQL 4
summary: Upgrade your Silverstripe CMS project to use graphQL version 4
---
# Upgrading from GraphQL 3
# Upgrading to GraphQL 4
[alert]
You are viewing docs for a pre-release version of silverstripe/graphql (4.x).
@ -13,47 +13,39 @@ 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
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.
Note that this document is aimed towards developers who have a custom graphql schema. If you are updating from graphql
3 to 4 but do not have a custom schema, you should familiarise yourself with the
[building the schema](/developer_guides/graphql/getting_started/building_the_schema) documentation, but you do not need to read this document.
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.
runtime. This didn't scale. As the GraphQL schema grew, API response latency increased.
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=default`
`$ vendor/bin/sake dev/graphql/build schema=mySchema`
Check the [building the schema](/developer_guides/graphql/getting_started/building_the_schema) documentation to learn about the
different ways to build your schema.
You can also run the task in the browser:
## The `Manager` class, the godfather of GraphQL 3, is gone
`http://example.com/dev/graphql/build?schema=mySchema`
`silverstripe/graphql` 3.x relied heavily on the `Manager` class. This became a catch-all that handled
scaffolding, registration of types, running queries and middleware, error handling, and more. This
class has been broken up into two separate concerns:
[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, applies middlewares and context.
* [`Schema`](api:SilverStripe\GraphQL\Schema\Schema) <- register your stuff here
* [`QueryHandlerInterface`](api:SilverSTripe\GraphQL\QueryHandler\QueryHandlerInterface) <- Handles GraphQL queries, applies middlewares and context.
You'll probably never have to touch it.
### Upgrading
@ -78,7 +70,7 @@ SilverStripe\GraphQL\Schema\Schema:
```
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.
the [configuring your schema](/developer_guides/graphql/getting_started/configuring_your_schema) section.
```
app/_graphql
@ -91,15 +83,14 @@ app/_graphql
unions.yml
```
## TypeCreator, QueryCreator, and MutationCreator are gone
## `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
as value objects that effectively 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
@ -133,8 +124,9 @@ group:
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.
That's a simple type, and obviously there's a lot more to it than that. Have a look at the
[working with generic types](/developer_guides/graphql/getting_started/working_with_generic_types) section of the documentation
to learn more.
## Resolvers must be static callables
@ -144,7 +136,9 @@ and moved into a class.
### Upgrading
Move your resolvers into one or many classes, and register them.
Move your resolvers into one or many classes, and register them. Notice that the name of the method
determines what is being resolved, where previously that would be the name of a resolver class. Now,
multiple resolver methods can exist within a single resolver class.
**before**
```php
@ -162,12 +156,12 @@ class LatestPostResolver implements OperationResolver
**app/_graphql/config.yml**
```yaml
resolvers:
- MyProject\Resolvers\MyResolverA
- MyProject\Resolvers\MyResolverB
- MyProject\Resolvers\MyResolverClassA
- MyProject\Resolvers\MyResolverClassB
```
```php
class MyResolverA
class MyResolverClassA
{
public static function resolveLatestPost($object, array $args, $context, ResolveInfo $info)
{
@ -176,22 +170,23 @@ class MyResolverA
}
```
This method relies on [resolver discovery](getting_started/working_with_generic_types/resolver_discovery),
This method relies on [resolver discovery](/developer_guides/graphql/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:
Alternatively, you can [hardcode the resolver into your config](/developer_guides/graphql/getting_started/working_with_generic_types/resolver_discovery#field-resolvers).
**app/_graphql/queries.yml**
```yaml
latestPost:
type: Post
resolver: ['MyResolvers', 'latestPost' ]
resolver: ['MyApp\Resolvers\MyResolvers', 'latestPost']
```
## ScaffoldingProviders are now SchemaUpdaters
## `ScaffoldingProvider`s are now `SchemaUpdater`s
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.
The `ScaffoldingProvider` interface has been replaced with [`SchemaUpdater`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater).
If you were updating your schema with procedural code, you'll need to implement `SchemaUpdater`
and implement the [`updateSchema()`](api:SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater::updateSchema()) method.
### Upgrading
@ -235,21 +230,23 @@ class MyProvider implements SchemaUpdater
}
```
[alert]
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.
[using procedural code](/developer_guides/graphql/getting_started/using_procedural_code) documentation.
[/alert]
## Goodbye, scaffolding, hello models
## 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**.
In `silverstripe/graphql` 3.x, 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 replaced with a 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!).
A model type is just a type that is backed by a class that has 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 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?
@ -257,7 +254,7 @@ At a high-level, it needs to answer questions like:
### 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.
a lot like the old scaffolding API. It's largely the same syntax, but a lot easier to read.
**before**
```yaml
@ -285,6 +282,7 @@ SilverStripe\GraphQL\Manager:
SilverStripe\Security\Member:
fields: '*'
operations: '*'
SilverStripe\CMS\Model\SiteTree:
fields:
title: true
@ -293,12 +291,12 @@ SilverStripe\CMS\Model\SiteTree:
read: true
```
## DataObject field names are lowerCamelCase by default
## `DataObject` GraphQL 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.
map directly 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
@ -343,11 +341,10 @@ query readPages {
}
```
## DataObject type names are simpler
## `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>`.
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.
@ -362,46 +359,49 @@ Change any references to DataObject type names in your queries
**after**
`query SiteTrees {}`
[info]
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.
control over how types are named. You can use the `type_formatter` and `type_prefix` on
[`DataObjectModel`](api:SilverStripe\GraphQL\Schema\DataObject\DataObjectModel) to influence the naming computation.
Read more about this in the [DataObject model type](/developer_guides/graphql/getting_started/working_with_dataobjects/dataobject_model_type#customising-the-type-name)
docs.
[/info]
## The Connection class has been moved to plugins
## The `Connection` class has been replaced with 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).
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](/developer_guides/graphql/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.
`SS_List` results. Again, this plugin is applied to all `DataObject` classes by default, and will include all of their
sortable fields by default - though this is configurable. See the [query plugins](/developer_guides/graphql/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
## Query filtering has been replaced with 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.
read queries by default, and should include all filterable fields including nested relationships - though
this is configurable. See the [query plugins](/developer_guides/graphql/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
## Query permissions have been replaced with 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).
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](/developer_guides/graphql/getting_started/working_with_dataobjects/permissions).
## Enums are first-class citizens
@ -421,3 +421,6 @@ Status:
CANCELLED: Cancelled
PENDING: Pending
```
See the [Enums, unions, and interfaces](/developer_guides/graphql/working_with_generic_types/enums_unions_and_interfaces/#enum-types)
documentation for more information.

View File

@ -8,6 +8,7 @@ title: 4.11.0 (unreleased)
- [Regression test and Security audit](#audit)
- [Dropping support for PHP 7.3](#phpeol)
- [GraphQL v4 major release](#graphqlv4)
- [Features and enhancements](#features-and-enhancements)
- [Upload and use WebP images in the CMS](#webp)
- [Preview any DataObject in any admin section](#cms-preview)
@ -28,6 +29,71 @@ While it is still advised that you perform your own due diligence when upgrading
In accordance with our [PHP support policy](/Getting_Started/Server_Requirements), Silverstripe CMS Recipe release 4.11.0 drops support for PHP 7.3. We expect to drop support for PHP 7 altogether around January 2023.
## GraphQL 4 major release {#graphqlv4}
Silverstripe CMS Recipe 4.11 defaults to installing `silverstripe/graphql` version 4, which has just had a stable major release. Previous releases installed version 3.
### What does `silverstripe/graphql` do and why are you changing this?
GraphQL is a query language for APIs. It was initially designed by Facebook but it is now used widely across the internet by all sorts of organisations including GitHub, AirBnB, Lyft, PayPal, Shopify and Silverstripe CMS … to name just a few.
`silverstripe/graphql` is an implementation of GraphQL specific to Silverstripe CMS. It is used to power some aspects of the CMS UI. It can also be used by developers to create APIs that other web services can use to read or update data in your CMS sites. This opens a lot of use cases like using Silverstripe CMS as “headless” CMS.
Up until CMS Recipe 4.11, Silverstripe CMS would default to using `silverstripe/graphql` version 3. While `silverstripe/graphql` v3 was sufficient to support the basic CMS use cases it was being used for, it was not performant enough to build more complex applications.
`silverstripe/graphql` v4 is a complete rewrite and provides substantial performance improvements.
`silverstripe/graphql` v4 provides developers a first class tool for building APIs and allowing third party services to integrate with their Silverstripe CMS websites.
### What do I need to know to get started?
Part of the reason why `silverstripe/graphql` v4 is so much faster than v3 is that it has a “code generation” step. Silverstripe CMS will generate PHP classes for your GraphQL schemas and stores them in a `.graphql-generated` folder in the root of your project.
If you do not have a custom schema, all you need to know is:
- There are two new folders that your web server user will need write access to: `.graphql-generated` and `public/_graphql`. These are now mentioned in the [Server Requirements](/getting_started/server_requirements/) documentation.
- If these folders do not exist when `silverstripe/graphql` needs them, the module will try to create them.
- The GraphQL schema for the CMS will need to be generated. For the most common hosting scenarios you will be fine letting this happen during dev/build, but read the [building the schema](/developer_guides/graphql/getting_started/building_the_schema) documentation to know what your options are - especially if you have a multi-server hosting solution.
- You will need to deploy the generated schema to your test or production environment. There are several ways to do this depending on your hosting situation - see the [deploying the schema](/developer_guides/graphql/getting_started/deploying_the_schema) documentation.
If you were already using GraphQL v3 for your own custom schema and queries and want to upgrade to v4, you will also need to read the [Upgrading to GraphQL 4](/upgrading/upgrading_to_graphql_4) documentation, and are encouraged to read the [GraphQL documentation](/developer_guides/graphql/) generally to make sure your existing knowledge carries over to the new major release.
### That sounds risky, do I absolutely have to use version 4?
Silverstripe CMS has been shipping with dual support for `silverstripe/graphql` v3 and v4 since the 4.8 release. Until now `silverstripe/graphql` v4 had been in alpha and you had to explicitly opt-in to get it. At Silverstripe, we are already using `silverstripe/graphql` v4 in production on several projects.
All the supported Silverstripe CMS modules that use `silverstripe/graphql` have dual-support. If you wish to stay on `silverstripe/graphql` v3, you can do so and it will not block you from upgrading to Silverstripe CMS 4.11.
#### Opting out of `silverstripe/graphql` version 4 and sticking to version 3
If your project composer.json file already explicitly requires silverstripe/graphql, you dont need to do anything.
If your project uses silverstripe/recipe-cms, it will install silverstripe/graphql:^4.0 when you upgrade to the 4.11 release. To stay on silverstripe/graphql:^3, you'll need to "inline" the `silverstripe/recipe-cms` requirements in your root composer.json and change `silverstripe/graphql` to `^3`.
You can inline `silverstripe/recipe-cms` by running this command:
```
composer update-recipe silverstripe/recipe-cms
```
If your project does not directly require `silverstripe/recipe-cms` or `silverstripe/graphql`, you may still be getting `silverstripe/graphql` installed if you require other modules that depend on it. To fix your `silverstripe/graphql` to version 3, run this composer command:
`composer require silverstripe/graphql:^3`
To validate which version of `silverstripe/graphql` your project is using, run this composer command:
`composer show silverstripe/graphql`
To view which dependencies require `silverstripe/graphql`, run this composer command:
`composer why silverstripe/graphql`
### Tracking or ignoring the `.graphql-generated` and `public/_graphql` folders
Existing projects will not have an entry in their `.gitignore` file for `.graphql-generated` or `public/_graphql`. It is best practice for most situations to not track these folders in version control. Youll have to manually add this entry to your `.gitignore`.
The `.gitignore` file in `silverstripe/installer` 4.11 has been updated to ignore both of these folders. If you start a new project from `silverstripe/installer` 4.11.0 and want to track the new folders, youll have to update your `.gitignore` file.
## Features and enhancements {#features-and-enhancements}
### Upload and use WebP images {#webp}