DOCS: Document new schema config, change to resolver discovery pattern (#9781)

This commit is contained in:
Aaron Carlino 2020-12-01 21:27:52 +13:00 committed by GitHub
parent 91c441103b
commit fe972d62d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 83 deletions

View File

@ -49,6 +49,8 @@ Let's populate a schema that is pre-configured for us out of the box, `default`.
SilverStripe\GraphQL\Schema\Schema:
schemas:
default:
config:
# general schema config here
types:
# your generic types here
models:
@ -79,7 +81,10 @@ SilverStripe\GraphQL\Schema\Schema:
src: app/_graphql
```
It can also be an array of directories.
[info]
It is recommended that you define your sources as an array so that further source files are merged.
Otherwise, another config file could completely override part of your schema definition.
[/info]
**app/_config/graphql.yml**
```yml
@ -87,8 +92,8 @@ SilverStripe\GraphQL\Schema\Schema:
schemas:
default:
src:
myDir: app/_graphql
myOtherDir: module/_graphql
- app/_graphql
- module/_graphql
```
[info]
@ -107,6 +112,8 @@ This doesn't mean there is never a need to flush your schema config. If you were
**app/_graphql/schema.yml**
```yaml
# no schema key needed. it's implied!
config:
# your schema config here
types:
# your generic types here
models:
@ -121,7 +128,7 @@ mutations:
Your schema YAML file will get quite bloated if it's just used as a monolithic source of truth
like this. We can tidy this up quite a bit by simply placing the files in directories that map
to the keys they populate -- e.g. `types/`, `models/`, `queries/`, `mutations/`, etc.
to the keys they populate -- e.g. `config/`, `types/`, `models/`, `queries/`, `mutations/`, etc.
There are two approaches to namespacing:
* By filename
@ -132,6 +139,11 @@ There are two approaches to namespacing:
If you use a parent directory name (at any depth) of one of the four keywords above, it will
be implicitly placed in the corresponding section of the schema.
**app/_graphql/types/config.yml**
```yaml
# my schema config here
```
**app/_graphql/types/types.yml**
```yaml
# my type definitions here
@ -169,30 +181,29 @@ The following are perfectly valid:
* `app/_graphql/news-and-blog/models/blog.yml`
* `app/_graphql/mySchema.yml`
### Changing schema defaults
### Schema config
In addition to all the keys mentioned above, each schema can declare a couple of generic
configuration files, `defaults` and `modelConfig`. These are
mostly used for assigning or removing default plugins to models and operations.
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.
[info]
As of now, the only one of these being used
is `modelConfig`, but `defaults` could some day apply non-model configuration to the schema.
[/info]
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 `modelConfig.yml`, or just be added as a `modelConfig:`
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/modelConfig.yml**
**app/_graphql/config.yml**
```yaml
DataObject:
plugins:
inheritance: true
operations:
read:
plugins:
readVersion: false
paginateList: false
modelConfig:
DataObject:
plugins:
inheritance: true
operations:
read:
plugins:
readVersion: false
paginateList: false
```

View File

@ -84,8 +84,7 @@ tangential changes such as:
### Viewing the generated code
TODO, once we figure out where it will go
By default, the generated code is placed in the `.graphql/` directory in the root of your project.
### Further reading

View File

@ -33,16 +33,17 @@ on the fly as closures. Resolvers must be static methods on a class, and are eva
the schema build.
[/notice]
### Adding a schema builder
### Adding executable code
We can use the `builders` 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`.
```yaml
SilverStripe\GraphQL\Schema\Schema:
schemas:
default:
builders:
- 'MyProject\MySchema'
config:
execute:
- 'MyProject\MySchema'
```
Now just implement the `SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater` interface.

View File

@ -224,8 +224,8 @@ Page:
There are several settings you can apply to your model class (typically `DataObjectModel`),
but because they can have distinct values _per schema_, the standard `_config` layer is not
an option. Model configuration has to be done within the schema definition in the `modelConfig`
section.
an option. Model configuration has to be done within the schema config in the `modelConfig`
subsection.
### Customising the type name
@ -245,10 +245,11 @@ the `$className` as a parameter.
Let's turn `MyProject\Models\Product` into the more specific `MyProjectProduct`
*app/_graphql/modelConfig.yml*
*app/_graphql/config.yml*
```yaml
DataObject:
type_formatter: ['MyProject\Formatters', 'formatType' ]
modelConfig:
DataObject:
type_formatter: ['MyProject\Formatters', 'formatType' ]
```
[info]
@ -277,10 +278,11 @@ public static function formatType(string $className): string
You can also add prefixes to all your DataObject types. This can be a scalar value or a callable,
using the same signature as `type_formatter`.
*app/_graphql/modelConfig.yml*
*app/_graphql/config.yml*
```yaml
DataObject
type_prefix: 'MyProject'
modelConfig:
DataObject
type_prefix: 'MyProject'
```
### Further reading

View File

@ -283,13 +283,14 @@ MyProject\Models\ProductCategory:
To disable sort globally, use `modelConfig`:
*app/_graphql/modelConfig.yml*
*app/_graphql/config.yml*
```yaml
DataObject:
operations:
read:
plugins:
sort: false
modelConfig:
DataObject:
operations:
read:
plugins:
sort: false
```
### The getByLink plugin

View File

@ -22,22 +22,43 @@ an explicit resolver and allow the system to discover one for you based on namin
Let's start by registering a resolver class(es) where we can define a bunch of these functions.
**app/_graphql/config.yml**
```yaml
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\Schema\Registry\ResolverRegistry:
constructor:
myResolver: '%$MyProject\Resolvers\MyResolvers'
resolvers:
- MyProject\Resolvers\MyResolvers
```
What we're registering here is called a `ResolverProvider`, and it must implement that interface.
The only thing this class is obliged to do is return a method name for a resolver given a type name and
`Field` object. If the class does not contain a resolver for that combination, it may return null and
defer to other resolver providers, or ultimately fallback on the global default resolver.
What we're registering here is a generic class that should contain one or more static functions that resolve one
or many fields. How those functions will be discovered relies on the _resolver strategy_.
### 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.
```php
public static function getResolverMethod(?string $typeName = null, ?Field $field = null): ?string;
public static function getResolverMethod(string $className, ?string $typeName = null, ?Field $field = null): ?string;
```
#### The default resolver strategy
By default, all schemas use `SilverStripe\GraphQL\Schema\Resolver\DefaultResolverStrategy::getResolerMethod`
to discover resolver functions. The logic works like this:
* Does `resolve<TypeName><FieldName>` exist?
* Yes? Invoke
* No? Continue
* Does `resolve<TypeName>` exist?
* Yes? Invoke
* No? Continue
* Does `resolve<FieldName>` exist?
* Yes? Invoke
* No? Continue
* Does `resolve` exist?
* Yes? Invoke
* No? Return null. This resolver cannot be discovered
Let's look at our query again:
```graphql
@ -48,31 +69,33 @@ query {
}
```
An example implementation of this method for our example might be:
Imagine we have two classes registered under `resolvers` -- `ClassA` and `ClassB`
* Does `resolveCountryName` exist?
* Yes? Invoke
* No? Continue
* Does `resolveCountry` exist?
* Yes? Invoke
* No? Continue
* Does `resolveName` exist?
* Yes? Invoke
* No? Continue
* Return null. Maybe someone else knows how to deal with this.
The logic will go like this:
You can implement whatever logic you like to help the resolver provider discover which of its methods
it appropriate for the given type/field combination, but since the above pattern seems like a pretty common
implementation, the module ships an abstract `DefaultResolverProvider` that applies this logic. You can just
write the resolver methods!
* `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.
**app/_graphql/config.yml**
```yaml
resolverStrategy: [ 'MyApp\Resolvers\Strategy', 'getResolverMethod' ]
```
Let's add a resolver method to our resolver provider:
**app/src/Resolvers/MyResolvers.php**
```php
use SilverStripe\GraphQL\Schema\Resolver\DefaultResolverProvider;
class MyResolvers extends DefaultResolverProvider
class MyResolvers
{
public static function resolveReadCountries()
{
@ -90,14 +113,6 @@ class MyResolvers extends DefaultResolverProvider
}
```
To recap, the `DefaultResolverProvider` will follow this workflow to locate a resolver
for this query:
* `resolveQueryReadCountries()` (<typeName><fieldName>)
* `resolveQuery()` (<typeName>)
* `resolveReadCountries()` (<fieldName>)
* `resolve` (catch-all)
Now that we're using logic to discover our resolver, we can clean up the config a bit.

View File

@ -105,11 +105,10 @@ class ModelCreator implements SchemaModelCreatorInterface
Just add it to the registry:
**app/_graphql/config.yml
```yaml
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\Schema\Registry\SchemaModelCreatorRegistry:
constructor:
dataobject: '%$SilverStripe\GraphQL\Schema\DataObject\ModelCreator'
modelCreators:
- 'SilverStripe\GraphQL\Schema\DataObject\ModelCreator'
```
### Further reading

View File

@ -73,10 +73,13 @@ public static function resolve(array $resolverContext = []): Closure
Now, just add the operation to the `DataObjectModel` configuration
to make it available to all DataObject types.
**app/_graphql/config.yml**
```yaml
SilverStripe\GraphQL\Schema\DataObject\DataObjectModel:
operations:
duplicate: 'MyProject\Operations\DuplicateCreator'
modelConfig:
DataObject:
operations:
duplicate:
class: 'MyProject\Operations\DuplicateCreator'
```
And use it:

View File

@ -13,6 +13,43 @@ Docs for the current stable version (3.x) can be found
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
[/alert]
## 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 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
If you need the type name during normal execution of your app, e.g. to display in your UI, you can rely
on the cached typenames, which are persisted alongside your generated schema code.
```php
Schema::create('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
A common pattern in GraphQL APIs is to store queries on the server by an identifier. This helps save