Skip to content

feat: schema customization with post type args#2399

Merged
jasonbahl merged 36 commits intowp-graphql:developfrom
justlevine:feat/schema-customization-with-post-type-args
Oct 6, 2022
Merged

feat: schema customization with post type args#2399
jasonbahl merged 36 commits intowp-graphql:developfrom
justlevine:feat/schema-customization-with-post-type-args

Conversation

@justlevine
Copy link
Copy Markdown
Collaborator

@justlevine justlevine commented May 28, 2022

What does this implement/fix? Explain your changes.

Adds additional WPGraphQL-specific $args to WP_Post_Type and WP_Taxonomy for granular registration/modification of PostObject and TermObject GraphQL types.

Builds off of #1486, with feedback from #2394.

What's changed

  • dev: Use register_post_type_args and register_taxonomy_args filters for setting WPGraphQL default args (instead of directly on the globals ), so they can be overruled by the user.
  • feat: adds the following WPGraphQL properties to WP_Post_Type and WP_Taxonomy:
- graphql_kind | string | (optional): Allows the type representing the post type to be added to the graph as an object type, interface type or union type. Possible values are 'object', 'interface' or 'union'. Default is 'object'.
- graphql_resolve_type | callable | (optional): The callback used to resolve the type. Only used if "graphql_kind" is set to "union" or "interface".
- graphql_interfaces | array<string> | (optional): List of Interface names the type should implement. These will be applied in addition to default interfaces such as "Node".
- graphql_exclude_interfaces | array<string> | (optional): List of Interface names the type _should not_ implement. This is applied after default and custom interfaces are added, so this can remove default interfaces. Note: Interfaces applied by other interfaces will not be excluded unless that interface is also excluded. For example a post type that supports "thumbnail" will have NodeWithFeaturedImage interface applied, which also applies the Node interface. Excluding "Node" interface will not work unless "NodeWithFeaturedImage" was also excluded.
- graphql_exclude_mutations | array<string> | (optional): Array of mutations to prevent from being registered. Possible values are "create", "update", "delete".
- graphql_fields | array<$config> | (optional): Array of fields to add to the Type. Applied if "graphql_kind" is "interface" or "object".
- graphql_exclude_fields | array<string> | (optional): Array of fields names to exclude from the type. Applies if "graphql_kind" is "interface" or "object". Note: any fields added by an interface will not be excluded with this option.
- graphql_connections | array<$config> | (optional): Array of connection configs to register to the type. Only applies if the "graphql_kind" is "object" or "interface".
- graphql_exclude_connections | array<string> | (optional): Array of connection names to exclude from the type. Only connections defined on the type will be excluded. Connections inherited from interfaces implemented on this type will remain even if "excluded" in this list.
- graphql_union_types | array<string> | (optional): Array of possible types the union can resolve to. Only used if "graphql_kind" is set to "union".
- graphql_register_root_field | boolean | (optional): Whether to register a field to the RootQuery to query a single node of this type. Default true.
- graphql_register_root_connection | boolean | (optional): Whether to register a connection to the RootQuery to query multiple nodes of this type. Default true.
- graphql_exclude_mutations | array<string> | (optional): Array of mutations to prevent from being registered. Possible values are "create", "update", "delete".
  • chore: consolidate PostObject and TermObject registration logic into WPGraphQL\Registry\Utils\PostObject and WPGraphQL\Registry\Utils\TermObject.

To do:

Does this close any currently open issues?

#1486

Any relevant logs, error output, GraphiQL screenshots, etc?

(If it’s long, please paste to https://ghostbin.com/ and insert the link here.)

Any other comments?

Where has this been tested?

Operating System: Ubuntu 20.04 ( WSL2 + devilbox + PHP 8.0.15 )

WordPress Version: 6.0

@justlevine justlevine requested review from jasonbahl and kidunot89 May 28, 2022 19:07
*
* @return array
*/
public static function setup_default_taxonomies( $args, $taxonomy ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

*
* @return array
*/
public static function setup_default_post_types( $args, $post_type ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

@justlevine justlevine added type: enhancement Improvements to existing functionality status: in progress Currently being worked on needs: discussion Requires a discussion to proceed scope: api Issues related to access functions, actions, and filters needs: tests Tests should be added to be sure this works as expected needs: reviewer response This needs the attention of a codeowner or maintainer labels May 28, 2022
@coveralls
Copy link
Copy Markdown

coveralls commented May 28, 2022

Coverage Status

Coverage increased (+0.4%) to 82.214% when pulling e8af672 on justlevine:feat/schema-customization-with-post-type-args into a2098ea on wp-graphql:develop.

@justlevine justlevine force-pushed the feat/schema-customization-with-post-type-args branch from c43ca34 to 1390d13 Compare May 28, 2022 21:23
@@ -419,50 +421,112 @@ public function init_admin() {
* @return void
*/
public static function show_in_graphql() {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole file is getting pretty large. Might be time to break the post type / taxonomy registration stuff to a separate class.

If so what should we name it? (Extensions use WPGraphQL/CoreSchemaFilters, but that doesn't really mesh with our file structure.

Copy link
Copy Markdown
Collaborator

@jasonbahl jasonbahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did a quick scan and left some comments. will come back and look in more depth shortly

@justlevine
Copy link
Copy Markdown
Collaborator Author

Apologies for the status spam. commit justlevine@2d38d1f broke my git history.
Had to reset and reapply the commits manually.

On the bright side, its now rebased on the latest develop

@justlevine justlevine changed the title Feat: schema customization with post type args feat: schema customization with post type args Aug 4, 2022
@justlevine
Copy link
Copy Markdown
Collaborator Author

@jasonbahl I dont see a clear path towards model/dataloader support as those are usually hard-coded and called devoid of the WP_Post_Type / WP_Taxonomy object.

I'm open to suggestions, but it might make sense to polish this off with the remaining tests, and leave that part for a future PR.

@jasonbahl
Copy link
Copy Markdown
Collaborator

@jasonbahl I dont see a clear path towards model/dataloader support as those are usually hard-coded and called devoid of the WP_Post_Type / WP_Taxonomy object.

I'm open to suggestions, but it might make sense to polish this off with the remaining tests, and leave that part for a future PR.

@justlevine ya, let's hold off on that. I have some ideas for sure, but we can tackle it separately.

@jasonbahl jasonbahl closed this Aug 4, 2022
@jasonbahl jasonbahl reopened this Aug 4, 2022
@jasonbahl
Copy link
Copy Markdown
Collaborator

Ooops. Accidentally clicked "Close with comment" instead of "Comment" 🤦🏻‍♂️

@justlevine justlevine added the status: in review Awaiting review before merging or closing label Sep 18, 2022
@justlevine justlevine mentioned this pull request Sep 19, 2022
@jasonbahl
Copy link
Copy Markdown
Collaborator

@justlevine ok, so I'm running through some scenarios and finding some issues.

I'll document.

We'll start with this basic post_type registration:

add_action( 'init', function() {

  $args = [
    'label' => 'Test Type',
    'public' => true,
    'supports' => ['comments', 'title', 'author', 'editor', 'thumbnail', 'custom-fields' ],
    'show_in_graphql' => true,
    'graphql_single_name' => 'TestType',
    'graphql_plural_name' => 'TestTypes',
  ];

  register_post_type( 'test_type', $args );

} );

graphql_exclude_connection

Because the post type has support for "comments" and "author", this gives us connections: TestType -> Author and TestType -> Comments

I can query like so now:

{
  testTypes {
    nodes {
      title
      author {
        node {
          name
        }
      }
      comments {
        nodes {
          content
        }
      }
    }
  }
}

Let's say I want to "exclude" the Author and Comments connection.

According to this PR I should be able to add: 'graphql_exclude_connections' => [ 'author', 'comments' ]

If I do that, the comments connection is properly not exposed to the Schema, but the author connection is still present.

We can see that in the screenshot below, that the TestType does not have the comments field any longer, but does still have the author field:

CleanShot 2022-09-20 at 14 00 59

I've not investigated the code, but I suspect this has something to do with one-to-one connections vs. plural connections?

graphql_exclude_interfaces

Using the registration above for the test_type post type, the TestType will apply the ContentNode interface (as expected).

CleanShot 2022-09-20 at 14 19 41

If we want this type to not apply the ContentNode interface, we should be able to add:

'graphql_exclude_interfaces' => [ 'ContentNode' ],

If we do this, the TestType still implements the ContentNode interface:

CleanShot 2022-09-20 at 14 32 16

(full registration here):

$args = [
	'label' => 'Test Type',
	'public' => true,
	'show_in_graphql' => true,
	'graphql_single_name' => 'TestType',
	'graphql_plural_name' => 'TestTypes',
	'supports' => ['comments', 'title', 'author', 'editor', 'thumbnail', 'custom-fields' ],
	'graphql_exclude_interfaces' => [ 'ContentNode' ],
];

register_post_type( 'test-type', $args );

If I remove the supports line altogether, then the ContentNode Interface is not applied.

Copy link
Copy Markdown
Collaborator

@jasonbahl jasonbahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discovered some issues while testing: #2399 (comment)

I haven't looked yet at the code causing these issues.

@justlevine
Copy link
Copy Markdown
Collaborator Author

If I do that, the comments connection is properly not exposed to the Schema, but the author connection is still present.

That's because the author isn't registered on the type, but on the NodeWithAuthor interface. If you exclude that interface it should work.

If we want this type to not apply the ContentNode interface, we should be able to add:
'graphql_exclude_interfaces' => [ 'ContentNode' ],
If we do this, the TestType still implements the ContentNode interface:

This should work, will investigate 🧐

@jasonbahl
Copy link
Copy Markdown
Collaborator

That's because the author isn't registered on the type, but on the NodeWithAuthor interface. If [you exclude that interface]

Gah!

Yes, this is a documentation thing, not a code thing. 🤦🏻‍♂️

Thanks for reminding me 😄

@justlevine
Copy link
Copy Markdown
Collaborator Author

If we want this type to not apply the ContentNode interface, we should be able to add:
'graphql_exclude_interfaces' => [ 'ContentNode' ],
If we do this, the TestType still implements the ContentNode interface:

This should work, will investigate 🧐

Not by my computer to test, but pretty sure the issue is that NodeWiithFeaturedImage includes ContentNode (and some others) as inherited interfaces.

Not sure why this inherits those interfaces when other NodeWith* types don't. An oversight, maybe?

@jasonbahl
Copy link
Copy Markdown
Collaborator

jasonbahl commented Sep 20, 2022

Not by my computer to test, but pretty sure the issue is that NodeWiithFeaturedImage includes ContentNode (and some others) as inherited interfaces.

Not sure why this inherits those interfaces when other NodeWith* types don't. An oversight, maybe?

Ah! Yes! 💪🏻

This seems right.

And this is likely due to my identity crisis of "what is a content node?" (i.e. #2355)

I think sometimes in my mind ContentNode is synonomous with "Post of any post type" and then other times "ContentNode" is synonomous with "Posts of public post types" 🤔

I think we need to still solve that problem. 🤔

That said, I think this could be a documentation thing, then, too.

If you exclude_interfaces, but another interface that is not excluded includes it, it will still be included. As is the case here.

I excluded ContentNode, but because I did not exclude NodeWithFeaturedImage it was still included because NodeWithFeaturedImage included it.

Now. . .the question is, do we remove ContentNode from NodeWithFeaturedImage? My instinct is yes, we should remove that declaration 🤔 (although technically a breaking change at this point if we did, so perhaps leave it and cover in documentation?)

@justlevine
Copy link
Copy Markdown
Collaborator Author

Now. . .the question is, do we remove ContentNode from NodeWithFeaturedImage? My instinct is yes, we should remove that declaration 🤔 (although technically a breaking change at this point if we did, so perhaps leave it and cover in documentation?)

I think I agree with your instincts.

  • In WPGraphQL's codebase, NodeWithFeaturedImage is only used on PostObject types, all of which are required by default to have the 'Node', 'ContentNode', 'DatabaseIdentifier', and 'NodeWithTemplate' interfaces by default. So the GraphQL object will still have the same fields if we remove any of those from NodeWithFeaturedImage.
  • Because PostObject GraphQL types are usually autogenerated, its unlikely someone is manually applying this interface to a GraphQL type with the intention to inherit those particular interfaces. It's possibly theyre applying NodeWithFeaturedImage to a TermNode, but if thats they case, they'd need to be filtering off ContentNode anyway, and the other extraneous interfaces are similarly included de facto in TermObject types.
  • As I mentioned, none of the of the other NodeWith* interfaces inherit these additional types. Not just does this imply that the inclusion on NodeWithFeaturedImage is a bug, but it also means that even those schemas that are moving to a nodeByUri pattern of building up generic queries with interfaces are likely not including the fields in their ... on NodeWithFeaturedImage fragments. Definitely fewer than e.g. those that could have been using broken pagination as a feature (although to be fair, the necessity of this change is less severe).

True, we could address the extra interfaces on NodeWithFeaturedImage solely in the docs, as part of whatever disclaimer that says the API can only remove direct interfaces/connections/fields, but as with deregister_graphql_field() doesnt affect inherited types, but IMO educating users about the differences between the two is hard enough without throwing curveballs in the form of unnecessary inheritance.

- correct typos / copy pasta in some code comments
}

// Remove excluded connections.
foreach ( $tax_object->graphql_exclude_connections as $connection_name ) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to support graphql_exclude_connections at a lower level?

For example, in the WPObjectType and WPInterfaceType?

For example, we could then do something like this:

apply_filters( 'graphql_wp_object_type_config', function( $config ) { 
  $config['graphql_exclude_connections'] = [ 'someConnectionToExclude'];
  return $config;
});

And this functionality could then work for all types, not just types that were added to the graph by register_post_type and register_taxonomy?

Like, I know some folks using WPGraphQL to get data from external data sources, like shopify, etc.

If they had some connections registered, and a plugin author wanted to quickly exclude them, they could use the filter above, and it would work for any type, not just Post/Term types. 🤔

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose even in the situation I brought up, we could just use apply_filters( 'graphql_wp_object_type_config', ... ); and unset the connections manually as well.

Maybe not a big deal 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a whole slew of lower-level filters, along with some additional (de)register_graphql_* access methods would be great.

This PR though is about making it easier to for devs to handle auto registering CPTs/Terms without the use of multiple actions/filters.

We can always adjust the underlying logic used to handle the WP_Type class properties in a future PR, and I think that by handling it separately/incrementally we can make a more informed design decision about where these new filters should go, instead of implementing them now and possibly needing to adjust/deprecate them once we have a concrete list of additional use cases/considerations.

…pe and register_taxonomy

- update stubs to include graphql_exclude_mutations
- update TypeRegistry.php to respect graphql_exclude_mutations
- add tests to ensure graphql_exclude_mutations is respected by register_post_type and register_taxonomy
… consistent with the other `NodeBy*` interfaces
* @return array
* @todo make protected after \Type\ObjectType\PostObject::get_fields() is removed.
*/
public static function get_fields( WP_Post_Type $post_type_object ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method get_fields has 60 lines of code (exceeds 40 allowed). Consider refactoring.

*
* @return array
*/
protected static function get_connections( WP_Post_Type $post_type_object ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method get_connections has 102 lines of code (exceeds 40 allowed). Consider refactoring.

*
* @return array
*/
protected static function get_connections( WP_Taxonomy $tax_object ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method get_connections has 114 lines of code (exceeds 40 allowed). Consider refactoring.

*
* @return array
*/
protected static function get_connections( WP_Post_Type $post_type_object ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function get_connections has a Cognitive Complexity of 28 (exceeds 5 allowed). Consider refactoring.

@@ -0,0 +1,578 @@
<?php
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File PostObject.php has 419 lines of code (exceeds 250 allowed). Consider refactoring.

*
* @return array
*/
protected static function get_connections( WP_Taxonomy $tax_object ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function get_connections has a Cognitive Complexity of 19 (exceeds 5 allowed). Consider refactoring.

*
* @return array
*/
protected static function get_fields( WP_Taxonomy $tax_object ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function get_fields has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.

* @return array
* @todo make protected after \Type\ObjectType\PostObject::get_fields() is removed.
*/
public static function get_fields( WP_Post_Type $post_type_object ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function get_fields has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.

- add tests for graphql_fields / graphql_exclude_fields for post types and taxonomies
@qlty-cloud-legacy
Copy link
Copy Markdown

Code Climate has analyzed commit e8af672 and detected 43 issues on this pull request.

Here's the issue category breakdown:

Category Count
Complexity 32
Duplication 11

View more on Code Climate.

@jasonbahl
Copy link
Copy Markdown
Collaborator

jasonbahl commented Oct 6, 2022

UPGRADE NOTICE:

This PR removes the ContentNode and DatabaseIdentifier interfaces from the NodeWithFeaturedImage interface.

This is considered a breaking change (see: https://github.com/wp-graphql/wp-graphql/actions/runs/3200436057/jobs/5227330295) affecting any queries that are using a ...on NodeWithFeaturedImage fragment referencing fields added by the ContentNode or DatabaseIdentifier interface.

The following filter can be used to add back the ContentNode and DatabaseIdentifier interface to the NodeWithFeaturedImage interface:

add_filter( 'graphql_wp_interface_type_config', function( $config ) {
	if ( $config['name'] === 'NodeWithFeaturedImage' ) {
		$config['interfaces'][] = 'ContentNode';
		$config['interfaces'][] = 'DatabaseIdentifier';
	}
	return $config;
}, 10, 1 );

@jasonbahl jasonbahl added the compat: possible break There is a possibility that this might lead to breaking changes, but not confirmed yet label Oct 6, 2022
@jasonbahl jasonbahl merged commit 2741140 into wp-graphql:develop Oct 6, 2022
@jasonbahl jasonbahl mentioned this pull request Oct 6, 2022
@justlevine justlevine deleted the feat/schema-customization-with-post-type-args branch November 29, 2022 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compat: possible break There is a possibility that this might lead to breaking changes, but not confirmed yet needs: reviewer response This needs the attention of a codeowner or maintainer scope: api Issues related to access functions, actions, and filters status: in review Awaiting review before merging or closing type: enhancement Improvements to existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants