Skip to content
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

DX: Can't narrow down interface subtypes on an implementing object with register_graphql_field() #3096

Open
2 of 3 tasks
justlevine opened this issue Apr 8, 2024 · 2 comments
Labels
Needs: Reviewer Response This needs the attention of a codeowner or maintainer. scope: API Issues related to access functions, actions, and filters Status: Discussion Requires a discussion to proceed Type: Bug Something isn't working

Comments

@justlevine
Copy link
Collaborator

Description

When using register_graphql_field(), a DUPLICATE_FIELD error is thrown even if the field being registered is compatible with the existing one.

For example, let's say a NodeWithSeo interface registers the seo: SeoInterface field. I should be able to overwrite PostObject.seo to be type PostObjectSeo implements SeoInterface. However, because at this point in the lifecycle, the interface fields are already merged with the fields in WPObjectType, the blind key-check sees that seo already exists and bails.

Ideally, if the field already exists, the new object type would be compared to see if it is compatible with the interface instead of throwing an error. (However I'm unaware of any internal methods for getting a list of implementing interfaces for a GraphQL object name).

Steps to reproduce

  1. Register your custom interface, e.g. SeoInterface
  2. Register your custom object that implements that interface. e.g. PostObjectSeo.
  3. Create a new interface to add a seo: SeoInterface to all ContentNodes.
  4. Register a custom field on PostObject to override the type of your custom field: seo: PostObjectSeo.
  5. Enable GraphQL Debugging and run the query.

image

Additional context

  • The workaround is manually using the 'graphql_{$type_name}_fields' filter, but that's bad DX.
  • This affects the schema usability for both WPGraphQL for Gravity Forms and WPGraphQL for RankMath Seo.

WPGraphQL Version

1.22.1

WordPress Version

6.5.0

PHP Version

8.1.15

Additional environment details

Ubuntu 20.04 ( wsl2 + devilbox).

Please confirm that you have searched existing issues in the repo.

  • Yes

Please confirm that you have disabled ALL plugins except for WPGraphQL.

  • Yes
  • My issue is with compatibility with a specific WordPress plugin, and I have listed all my installed plugins (and version info) above.
@justlevine
Copy link
Collaborator Author

justlevine commented May 11, 2024

Basic Reproduction:

The following code registers the NodeWithMyCustomContentNode interface to all content node types.
The interface contains two fields, childFieldOverloaded and childFieldFiltered, both of which are type ContentNode and simply return the parent.

The field types are changed on the Post object to return the narrowed Post type, using two methods:

  • graphql_register_fields(): fails due to a Duplicate Type error.
  • add_filter( "graphql_${post_type}_fields" ): passes since there is no duplication check.
add_action( 'graphql_register_types', static function() {
    // The graphql interface whose fields we'll narrow.
    register_graphql_interface_type(
      'NodeWithMyCustomContentNode',
      [
        'fields' => [
          'childFieldOverloaded' => [
            'type'        => 'ContentNode',
            'description' => __( 'This will be narrowed with graphql_register_field()', 'my-text-domain' ),
            'resolveType' => static fn ( $source ) => isset( $source->post_type ) ? graphql_format_type_name( $source->post_type ) : null,
            'resolve'     => static fn ( $source ) => $source,
          ],
          'childFieldFiltered'   => [
            'type'        => 'ContentNode',
            'description' => __( 'This will be narrowed with the filter directly', 'my-text-domain' ),
            'resolveType' => static fn ( $source ) => isset( $source->post_type ) ? graphql_format_type_name( $source->post_type ) : null,
            'resolve'     => static fn ( $source ) => $source,
          ],
        ],
      ]
    );

    // Register the interface to all ContentNodes types.
    register_graphql_interfaces_to_types( 'NodeWithMyCustomContentNode', [ 'ContentNode' ] );

    // ✖️ Narrow down the type for Post Objects using `register_graphql_field()`
    register_graphql_field(
      'Post',
      'childFieldOverloaded',
      [
        'type' => 'Post',
      ]
    );

    // ✔️ Use the `graphql_$type_name_fields` filter instead
    add_filter(
      'graphql_post_fields',
      static function ( array $fields ) {
        if ( isset( $fields['childFieldFiltered'] ) ) {
          $fields['childFieldFiltered']['type'] = 'Post';
        }

        return $fields;
      },
      10,
      2
    );
  },
);

Then inspect the types or try the following query:

{
  posts {
    nodes {
      childFieldOverloaded {
        title # Doesnt exist on ContentNode				
        ... on Post {
          title
        }
      }
      childFieldFiltered {
        title # Type type is a post so it works.
        ... on Post {
          title
        }
      }
    }
  }
}

Results

As you can see below, childFieldOverLoaded remains a ContentNode and a duplicate field message is output. However, childFieldFiltered correctly becomes the narrowed Post type and can be used in the query without an ...on Post{} wrapper.

image

@justlevine justlevine added Type: Bug Something isn't working Needs: Reviewer Response This needs the attention of a codeowner or maintainer. scope: API Issues related to access functions, actions, and filters Status: Discussion Requires a discussion to proceed and removed Needs: Reproduction This issue needs to be reproduced independently. labels May 11, 2024
@justlevine
Copy link
Collaborator Author

(Fun quirk with the filter workaround: seems to be a lifecycle issue where narrowing a field on an interface ( E.g. Node.myfield: MyInterface > ContentNode.myField: MyInterfaceOnContentNode won't affect other interfaces that implement ContentNode, e.g. HierarchicalContentNode ).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs: Reviewer Response This needs the attention of a codeowner or maintainer. scope: API Issues related to access functions, actions, and filters Status: Discussion Requires a discussion to proceed Type: Bug Something isn't working
Projects
Status: 🆕 New
Development

No branches or pull requests

3 participants