Merge pull request #9835 from unclecheese/pulls/4/nuclear-refactor

DOCS: Document GraphQL 4 BuildState changes
This commit is contained in:
Ingo Schommer 2021-02-17 16:20:16 +13:00 committed by GitHub
commit c6d6358e45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 76 deletions

View File

@ -106,6 +106,26 @@ app/
Don't forget to include this additional folder in any syncing and backup processes! Don't forget to include this additional folder in any syncing and backup processes!
### Building, Packaging and Deployment
It is common to build a SilverStripe application into a package on one environment (e.g. a CI server),
and then deploy the package to a (separate) webserver environment(s).
This approach relies on all auto-generated files required by SilverStripe to
be included in the package, or generated on the fly on each webserver environment.
The easiest way to ensure this is to commit auto generated files to source control.
If those changes are considered too noisy, here's some pointers for auto-generated files
to trigger and include in a deployment package:
* `public/_resources/`: Frontend assets copied from the (inaccessible) `vendor/` folder
via [silverstripe/vendor-plugin](https://github.com/silverstripe/vendor-plugin).
See [Templates: Requirements](/developer_guides/templates/requirements#exposing-assets-webroot).
* `.graphql/` and `public/_graphql/`: Schema and type definitions required by CMS and any GraphQL API endpoint. Generated through
[silverstripe/graphql v4](https://github.com/silverstripe/silverstripe-graphql).
Triggered by `dev/build`, or [GraphQL Schema Build](/developer_guides/graphql/getting_started/building_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).
### Web Worker Concurrency ### Web Worker Concurrency
@ -215,7 +235,7 @@ SilverStripe's PHP support has changed over time and if you are looking to upgra
| 3.6 | 5.3 - 7.1 | | | 3.6 | 5.3 - 7.1 | |
| 3.7 | 5.3 - 7.3 | [changelog](https://docs.silverstripe.org/en/3/changelogs/3.7.0/) | | 3.7 | 5.3 - 7.3 | [changelog](https://docs.silverstripe.org/en/3/changelogs/3.7.0/) |
| 4.0 - 4.4 | 5.6+ | | | 4.0 - 4.4 | 5.6+ | |
| 4.5+ (unreleased) | 7.1+ | [blog post](https://www.silverstripe.org/blog/our-plan-for-ending-php-5-6-support-in-silverstripe-4/) | | 4.5+ | 7.1+ | [blog post](https://www.silverstripe.org/blog/our-plan-for-ending-php-5-6-support-in-silverstripe-4/) |
## CMS browser requirements ## CMS browser requirements

View File

@ -47,7 +47,7 @@ Files contained inside the `app/client/dist` and `app/images` will be made publi
SilverStripe projects should not track the "resources" directory in their source control system. SilverStripe projects should not track the "resources" directory in their source control system.
### Exposing assets in the web root ### Exposing assets in the web root {#exposing-assets-webroot}
SilverStripe projects ship with [silverstripe/vendor-plugin](https://github.com/silverstripe/vendor-plugin). SilverStripe projects ship with [silverstripe/vendor-plugin](https://github.com/silverstripe/vendor-plugin).
This Composer plugin automatically tries to expose assets from your project and installed modules after installation, or after an update. This Composer plugin automatically tries to expose assets from your project and installed modules after installation, or after an update.

View File

@ -81,10 +81,23 @@ tangential changes such as:
* Adding a new resolver for a type that uses [resolver discovery](../working_with_generic_types/resolver_discovery) * Adding a new resolver for a type that uses [resolver discovery](../working_with_generic_types/resolver_discovery)
* Adding an extension to a DataObject * Adding an extension to a DataObject
* Adding a new subclass to a DataObject that is already exposed * 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.
### Viewing the generated code ### Viewing the generated code
By default, the generated code is placed in the `.graphql/` directory in the root of your project. By default, the generated code is placed in the `.graphql/` 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.
Additional files are generated for CMS operation in `public/_graphql/`, and
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.
### Further reading ### Further reading

View File

@ -114,6 +114,58 @@ don't apply in this context. Most importantly, this means you need to
implement your own `canView()` checks. implement your own `canView()` checks.
[/notice] [/notice]
## Resolver Method 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.
## 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.
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
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
{
$member = UserContextProvider::get($context);
$canViewCode = ($member && Permission::checkMember($member, 'ADMIN'));
$results = [];
$countries = Injector::inst()->get(Locales::class)->getCountries();
foreach ($countries as $code => $name) {
$results[] = [
'code' => $canViewCode ? $code : '',
'name' => $name
];
}
return $results;
}
}
```
## Resolver Discovery
This is great, but as we write more and more queries for types with more and more fields, This is great, but as we write more and more queries for types with more and more fields,
it's going to get awfully laborious mapping all these resolvers. Let's clean this up a bit by it's going to get awfully laborious mapping all these resolvers. Let's clean this up a bit by
adding a bit of convention over configuration, and save ourselves a lot of time to boot. We can do adding a bit of convention over configuration, and save ourselves a lot of time to boot. We can do

View File

@ -30,26 +30,39 @@ The middleware API in the silverstripe-graphql module is separate from other com
APIs in Silverstripe CMS, such as HTTPMiddleware. APIs in Silverstripe CMS, such as HTTPMiddleware.
[/notice] [/notice]
The signature for middleware is pretty simple: The signature for middleware looks like this:
```php ```php
public function process(array $params, callable $next) public function process(Schema $schema, $query, $context, $vars, callable $next)
``` ```
`$params` is an arbitrary array of data, much like an event object * `$schema`: The underlying [Schema](http://webonyx.github.io/graphql-php/type-system/schema/) object.
passed to an event handler. The `$next` parameter refers to the next Useful to inspect whether types are defined in a schema.
middleware in the chain. * `$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
Let's write a simple middleware that logs our queries as they come in. Let's write a simple middleware that logs our queries as they come in.
```php ```php
use SilverStripe\GraphQL\QueryHandler\UserContextProvider;
use GraphQL\Type\Schema;
class LoggingMiddleware implements Middleware class LoggingMiddleware implements Middleware
{ {
public function process(array $params, callable $next) public function process(Schema $schema, $query, $context, $vars, callable $next)
{ {
$query = $params['query']; $member = UserContextProvider::get($context);
Injector::inst()->get(LoggerInterface::class) Injector::inst()->get(LoggerInterface::class)
->info('Query executed: ' . $query); ->info(sprintf(
'Query executed: %s by %s',
$query,
$member ? $member->Title : '<anonymous>';
));
// Hand off execution to the next middleware // Hand off execution to the next middleware
return $next($params); return $next($params);
@ -67,3 +80,4 @@ Now we can register the middleware with our query handler:
Middlewares: Middlewares:
logging: '%$MyProject\Middleware\LoggingMiddleware' logging: '%$MyProject\Middleware\LoggingMiddleware'
``` ```

View File

@ -17,16 +17,6 @@ Docs for the current stable version (3.x) can be found
Often times, you'll need to know the name of the type given a class name. There's a bit of context to this. 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 at build time
If you need to know the name of the type _during the build_, e.g. creating the name of an operation, field, query, etc,
you should use the `Build::requireActiveBuild()` accessor. This will get you the schema that is currently being built,
and throw if no build is active. A more tolerant method is `getActiveBuild()` which will return null if no schema
is being built.
```php
Build::requireActiveBuild()->findOrMakeModel($className)->getName();
```
### Getting the type name from within your app ### Getting the type name from within your app
@ -34,20 +24,7 @@ If you need the type name during normal execution of your app, e.g. to display i
on the cached typenames, which are persisted alongside your generated schema code. on the cached typenames, which are persisted alongside your generated schema code.
```php ```php
Schema::create('default')->getTypeNameForClass($className); SchemaBuilder::singleton()->read('default')->getTypeNameForClass($className);
```
### Why is there a difference?
It is expensive to load all of the schema config. The `getTypeNameForClass` function avoids the need to
load the config, and reads directly from the cache. To be clear, the following is functionally equivalent,
but slow:
```php
Schema::create('default')
->loadFromConfig()
->findOrMakeModel($className)
->getName();
``` ```
## Persisting queries ## Persisting queries
@ -165,15 +142,16 @@ This feature is experimental, and has not been thoroughly evaluated for security
[/warning] [/warning]
## Schema introspection ## Schema introspection {#schema-introspection}
Some GraphQL clients such as [Apollo](http://apollographql.com) require some level of introspection Some GraphQL clients such as [Apollo](http://apollographql.com) require some level of introspection
into the schema. While introspection is [part of the GraphQL spec](http://graphql.org/learn/introspection/), into the schema. The `SchemaTranscriber` class will persist this data to a static file in an event
this module provides a limited API for fetching it via non-graphql endpoints. By default, the `graphql/` that is fired on completion of the schema build. This file can then be consumed by a client side library
controller provides a `types` action that will return the type schema (serialised as JSON) dynamically. like Apollo. The `silverstripe/admin` module is built to consume this data and expects it to be in a
web-accessible location.
*GET http://example.com/graphql/types*
```js ```json
{ {
"data":{ "data":{
"__schema":{ "__schema":{
@ -189,19 +167,18 @@ controller provides a `types` action that will return the type schema (serialise
} }
``` ```
By default, the file will be stored in `public/_graphql`. Files are only generated for the `silverstripe/admin` module.
As your schema grows, introspecting it dynamically may have a performance hit. Alternatively, If you need these types for your own uses, add a new handler:
if you have the `silverstripe/assets` module installed (as it is in the default SilverStripe installation),
GraphQL can cache your schema as a flat file in the `assets/` directory. To enable this, simply
set the `cache_types_in_filesystem` setting to `true` on `SilverStripe\GraphQL\Controller`. Once enabled,
a `types.graphql` file will be written to your `assets/` directory on `flush`.
When `cache_types_in_filesystem` is enabled, it is recommended that you remove the extension that ```yml
provides the dynamic introspection endpoint. SilverStripe\Core\Injector\Injector:
SilverStripe\EventDispatcher\Dispatch\Dispatcher:
```php properties:
use SilverStripe\GraphQL\Controller; handlers:
use SilverStripe\GraphQL\Extensions\IntrospectionProvider; graphqlTranscribe:
on: [ graphqlSchemaBuild.mySchema ]
Controller::remove_extension(IntrospectionProvider::class); handler: '%$SilverStripe\GraphQL\Schema\Services\SchemaTranscribeHandler'
``` ```
This handler will only apply to events fired in the `mySchema` context.

View File

@ -53,7 +53,8 @@ registration of types, execution of scaffolding, running queries and middleware,
class has been broken up into separate concerns: class has been broken up into separate concerns:
* `Schema` <- register your stuff here * `Schema` <- register your stuff here
* `QueryHandlerInterface` <- Handles GraphQL queries. You'll probably never have to touch it. * `QueryHandlerInterface` <- Handles GraphQL queries, applies middlewares and context.
You'll probably never have to touch it.
### Upgrading ### Upgrading
@ -72,7 +73,8 @@ SilverStripe\GraphQL\Manager:
SilverStripe\GraphQL\Schema\Schema: SilverStripe\GraphQL\Schema\Schema:
schemas: schemas:
default: default:
src: app/_graphql # A directory of your choice src:
- app/_graphql # A directory of your choice
``` ```
Add the appropriate yaml files to the directory. For more information on this pattern, see Add the appropriate yaml files to the directory. For more information on this pattern, see
@ -142,7 +144,7 @@ and moved into a class.
### Upgrading ### Upgrading
Move your resolvers into one or many `ResolverProvider` implementations, register them. Move your resolvers into one or many classes, and register them.
**before** **before**
```php ```php
@ -156,15 +158,16 @@ class LatestPostResolver implements OperationResolver
``` ```
**after** **after**
**app/_graphql/config.yml**
```yaml ```yaml
SilverStripe\Core\Injector\Injector: resolvers:
SilverStripe\GraphQL\Schema\Registry\ResolverRegistry: - MyProject\Resolvers\MyResolverA
constructor: - MyProject\Resolvers\MyResolverB
myResolver: '%$MyProject\Resolvers\MyResolvers'
``` ```
```php ```php
class MyResolvers extends DefaultResolverProvider class MyResolverA
{ {
public static function resolveLatestPost($object, array $args, $context, ResolveInfo $info) public static function resolveLatestPost($object, array $args, $context, ResolveInfo $info)
{ {
@ -218,7 +221,7 @@ class MyProvider implements ScaffoldingProvider
SilverStripe\GraphQL\Schema\Schema: SilverStripe\GraphQL\Schema\Schema:
schemas: schemas:
default: default:
builders: execute:
- 'MyProject\MyProvider' - 'MyProject\MyProvider'
``` ```
@ -246,7 +249,7 @@ A model type is just a type that is backed by a class that express awareness of
At a high-level, it needs to answer questions like: At a high-level, it needs to answer questions like:
* Do you have field X? * Do you have field X?
What type is field Y? What type is field Y?
* What are all the fields you offer? * What are all the fields you offer?
* What operations do you provide? * What operations do you provide?
* Do you require any extra types to be added to the schema? * Do you require any extra types to be added to the schema?
@ -304,14 +307,33 @@ Change the casing in your queries.
**before** **before**
```graphql ```graphql
query readPages { query readPages {
nodes { edges {
Title nodes {
ShowInMenus Title
ShowInMenus
}
} }
} }
``` ```
**after** **after**
```graphql
query readPages {
edges {
node {
title
showInMenus
}
}
}
```
### `edges` no longer required
We don't have [cursor-based pagination](https://graphql.org/learn/pagination/) in Silverstripe CMS, so
the use of `edges` is merely for convention. You can eliminate a layer here and just use `nodes`, but `edges`
still exists for backward compatibility.
```graphql ```graphql
query readPages { query readPages {
nodes { nodes {
@ -321,6 +343,7 @@ 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 To avoid naming collisions, the 3.x release of the module used a pretty aggressive approach to ensuring
@ -398,10 +421,3 @@ Status:
CANCELLED: Cancelled CANCELLED: Cancelled
PENDING: Pending PENDING: Pending
``` ```
## Middleware signature is more loosely typed
In the 3.x release, `QueryMiddleware` was a very specific implementation that took parameters that were unique
to queries. The middleware pattern is now more generic and accepts a loosely-typed `params` array that can consist
of anything -- more like an `event` parameter for an event handler. If you've defined custom middleware, you'll
need to update it. Check out the [adding middleware](extending/adding_middleware) section for more information.