-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(MOVED) NEW: Replace _extends in favour of Unions/Interfaces #374
(MOVED) NEW: Replace _extends in favour of Unions/Interfaces #374
Conversation
I don't have large scale public APIs running, but the potential side-effects of breaking the API seem like a rather big downside to this approach. Not sure if I understand correctly, but is it mainly a problem when you add a Subclass to a class that was a "leaf-model" beforehand? How will the audit system work? Does the developer add queries that are important to the API? Or is this also auto-generated? This might also generate a lot of warnings, whenever you install a module that exposes models via GraphQL, or am I missing something here? |
fe33277
to
49b4d25
Compare
Yeah, this has always been at the core of the struggle with how to handle inheritance. Unions are the right thing to do, but the determination to use one or not is driven by circumstances that are only tangentially unrelated to the schema, which creates some instability in your API. The early results from our experiment with "pseudo-unions" to mitigate this problem haven't been promising. It's very confusing and cumbersome, and it's just a bad look for Silverstripe to have a content API that doesn't adhere to basic conventions. The question is really over what cost we're willing to pay for the guarantee of not breaking the API when you add a subclass. While not rare, it's still pretty edge-casey, and I reckon we can gain a lot of ground through good documentation, and through notices in the build. My idea of an auditor was to simply invoke something like
|
Whats the nature of the breakage you get when adding a subclass? do queries break due to names changing on unions/interfaces? I'm just wondering if it could be mitigated by producing a fragment that you could the import via the |
Yeah, so when a query changes from a list of a single concrete type to a union, every field needs to be in an query {
readPages {
title
content
... on BlogPage {
date
}
}
} We assumed that would work (after all, title and content are on all types in the union) and would therefore allow your queries to be backward compatible. But unfortunately, that's not he case. query {
readPages {
... on PageInterface {
title
content
}
... on BlogPage {
date
}
}
} Therefore, adding a subclass to an exposed type, and exposing that subclass, will break things. Notably, this upgrade does not break elemental. You would think But if you do this: ... Schema:
schemas:
admin:
models:
My\Block:
fields: '*' You just broke the CMS. |
possibly bad idea - default every dataobject in the CMS/admin schema to a union from the get go - so no single types to break when they change/are subclassed. I feel this as less of a problem in front-facing schemas as code that consumes those is under the control of the one adding the config. |
Well here we go again. tl;dr, we probably don't need the unions in this PR, and all the chatter above regarding breaking APIs is probably not a thing. |
We're using this branch so much now (e.g. silverstripe/headless module) that it's a pain to keep it on a fork, so I've moved the branch to the main repo and have issued a new pull request using that. |
Overview
This pull request does away with the idiosyncratic and verbose
_extends
field we used to navigate the inheritance pattern of ORM-generated types and instead leverage some of the native abstractions provided by the GraphQL spec that are better suited for this purpose.Relevant: #209
Docs: silverstripe/silverstripe-framework#9912
To be merged after
* [ ] silverstripe/silverstripe-asset-admin#1201* [ ] silverstripe/silverstripe-versioned#331Tests:
There are several components to this revision.
Part 1: Inherited fields / implicit exposures
Serviced by:
SilverStripe\GraphQL\Schema\DataObject\InheritanceBuilder
Example:
This results in these two types being exposed with the fields as shown, but also results in a
Page
type:Part 2: Interfaces
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 schema.
Serviced by:
SilverStripe\GraphQL\Schema\DataObject\InterfaceBuilder
Example
This will create the following interfaces:
Types then get these interfaces applied, like so:
Lastly, for good measure, we create a
DataObjectInterface
that applies to everything.Part 3: Unions and Union Queries
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
Example
Creates the following unions:
"Leaf" models like
BlogPage
,ConferencePage
, andWebinarPage
that have no exposed descendants will not create unions, as they are functionally useless.This means that queries for
readPages
andreadEventsPages
will now return unions.This works fine for leaf models, but what if we want the
title
field? One option is to add it to each... on
clause, as we know its been propagated down through the inheritance builder in part 1, above. But a better way to do this is to leverage the common interface.A good way of negotiating whether to use interfaces or types in the
... on
block is to ask the question "Could this field appear on more than one type?" If the answer is yes, you want an interface.Elemental
One of the primary motivators for this refactor was the awkwardness of querying for content blocks. 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.To consider
This has the undesirable effect of breaking GraphQL APIs when a subclass is added. We've always thought of this as having dangerous consequences for the use of GraphQL in the CMS, but now that we're using a build step to generate the schema, it's conceivable that we could mitigate that problem at build time with static query generation for CMS consumption.
For users consuming GraphQL in their own app code, this does indeed break their APIs, and we should make sure they're aware of it. There are an increasing number of these "heads up" cases that we want to include in the build script, and I'm proposing a
SchemaAuditInterface
API, where you could add your own set of auditors that would compare the previous schema to the next one and throw warnings or fatals when something isn't right.A simple auditor that checks if any query has changed its return type would make a great auditor to start with.
Also in this PR
className
allowed by default for DataObject typesbase_fields
setting for fields that are required to be exposed on each modelSchemaContextProvider
is now more appropriately namedSchemaConfigProvider
QueryHandlerInterface
QueryStateProvider
for exposing shared query state to resolvers