fix: Connections need to connect to Types that implement the Node interface (#675)

* - implement Node on different Types

* - update class-customers to return arrays for edges and nodes
- move id resolver from product-attribute interface to the specific types

* - return empty arrays for edges/nodes for coupon connections
- update test to check for falsy vs null

* - remove manual registration of the `Product` Interface
- register the fields to the Product Interface

* - fix code style

* - fix code style

* - no longer pass type registry to Product::register_interface()

* -  use post type registry to register the Product type as an Interface

* WPGraphQL v1.13.x support added.

* WPGraphQL v1.13.x support added.

* fix: filter corrected.

* fix: Needed code restored

* fix: Needed code restored

* parent field no longer overwritten

* chore: WPCS compliance met.

Co-authored-by: Geoff Taylor <geoff@axistaylor.com>
This commit is contained in:
Jason Bahl
2022-12-07 17:04:39 -05:00
committed by GitHub
co-authored by Geoff Taylor
parent 25a5734386
commit 0b8ce4f634
23 changed files with 118 additions and 116 deletions
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
View File
+22 -5
View File
@@ -8,6 +8,7 @@
namespace WPGraphQL\WooCommerce;
use GraphQL\Error\UserError;
use WPGraphQL\WooCommerce\Data\Loader\WC_Customer_Loader;
use WPGraphQL\WooCommerce\Data\Loader\WC_CPT_Loader;
use WPGraphQL\WooCommerce\Data\Loader\WC_Db_Loader;
@@ -134,11 +135,27 @@ class Core_Schema_Filters {
*/
public static function register_post_types( $args, $post_type ) {
if ( 'product' === $post_type ) {
$args['show_in_graphql'] = true;
$args['graphql_single_name'] = 'Product';
$args['graphql_plural_name'] = 'Products';
$args['skip_graphql_type_registry'] = true;
}
$args['show_in_graphql'] = true;
$args['graphql_single_name'] = 'Product';
$args['graphql_plural_name'] = 'Products';
$args['graphql_kind'] = 'interface';
$args['graphql_register_root_field'] = false;
$args['graphql_register_root_connection'] = false;
$args['graphql_resolve_type'] = static function( $value ) {
$type_registry = \WPGraphQL::get_type_registry();
$possible_types = WP_GraphQL_WooCommerce::get_enabled_product_types();
if ( isset( $possible_types[ $value->type ] ) ) {
return $type_registry->get_type( $possible_types[ $value->type ] );
}
throw new UserError(
sprintf(
/* translators: %s: Product type */
__( 'The "%s" product type is not supported by the core WPGraphQL WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ),
$value->type
)
);
};
}//end if
if ( 'product_variation' === $post_type ) {
$args['show_in_graphql'] = true;
$args['graphql_single_name'] = 'ProductVariation';
+1 -1
View File
@@ -59,7 +59,7 @@ class Type_Registry {
Type\WPInputObject\Orderby_Inputs::register();
// Interfaces.
Type\WPInterface\Product::register_interface( $type_registry );
Type\WPInterface\Product::register_interface();
Type\WPInterface\Attribute::register_interface( $type_registry );
Type\WPInterface\Product_Attribute::register_interface( $type_registry );
Type\WPInterface\Cart_Error::register_interface( $type_registry );
+4 -1
View File
@@ -45,7 +45,10 @@ class Coupons {
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'shop_coupon' );
if ( ! self::should_execute() ) {
return [];
return [
'edges' => [],
'nodes' => [],
];
}
return $resolver->get_connection();
+4 -1
View File
@@ -60,7 +60,10 @@ class Customers {
$resolver->set_query_arg( 'role', 'customer' );
if ( ! self::should_execute() ) {
return [];
return [
'nodes' => [],
'edges' => [],
];
}
return $resolver->get_connection();
+48 -40
View File
@@ -32,7 +32,9 @@ class Products {
[
'fromType' => 'Coupon',
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver->set_query_arg( 'post__in', $source->product_ids );
@@ -52,7 +54,9 @@ class Products {
'fromType' => 'Coupon',
'fromFieldName' => 'excludedProducts',
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver->set_query_arg( 'post__in', $source->excluded_product_ids );
@@ -83,7 +87,9 @@ class Products {
]
),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
// Bypass randomization by default for pagination support.
if ( empty( $args['where']['shuffle'] ) ) {
@@ -114,7 +120,9 @@ class Products {
'fromType' => 'Product',
'fromFieldName' => 'upsell',
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver->set_query_arg( 'post__in', $source->upsell_ids );
@@ -135,7 +143,9 @@ class Products {
[
'fromType' => 'GroupProduct',
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver->set_query_arg( 'post__in', $source->grouped_ids );
@@ -154,7 +164,9 @@ class Products {
$cross_sell_config = [
'fromFieldName' => 'crossSell',
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver->set_query_arg( 'post__in', $source->cross_sell_ids );
@@ -185,7 +197,9 @@ class Products {
'toType' => 'ProductVariation',
'fromFieldName' => 'variations',
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver->set_query_arg( 'post_parent', $source->ID );
$resolver->set_query_arg( 'post_type', 'product_variation' );
@@ -216,7 +230,10 @@ class Products {
return null;
}
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver->set_query_arg( 'p', $source->parent_id );
$resolver = self::set_ordering_query_args( $resolver, $args );
@@ -228,14 +245,16 @@ class Products {
// Taxonomy To Product resolver.
$resolve_product_from_taxonomy = function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$tax_query = [
[
// WPCS: slow query ok.
'taxonomy' => $source->taxonomyName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'field' => 'term_id',
'terms' => $source->term_id,
'taxonomy' => $source->taxonomyName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'field' => 'term_id',
'terms' => $source->term_id,
],
];
$resolver->set_query_arg( 'tax_query', $tax_query );
@@ -245,37 +264,9 @@ class Products {
return $resolver->get_connection();
};
// From ProductCategory.
register_graphql_connection(
self::get_connection_config(
[
'fromType' => 'ProductCategory',
'resolve' => $resolve_product_from_taxonomy,
]
)
);
// From ProductTag.
register_graphql_connection(
self::get_connection_config(
[
'fromType' => 'ProductTag',
'resolve' => $resolve_product_from_taxonomy,
]
)
);
// From WooCommerce product attributes.
$attributes = WP_GraphQL_WooCommerce::get_product_attribute_taxonomies();
foreach ( $attributes as $attribute ) {
register_graphql_connection(
self::get_connection_config(
[
'fromType' => ucfirst( graphql_format_field_name( $attribute ) ),
'resolve' => $resolve_product_from_taxonomy,
]
)
);
register_graphql_connection(
self::get_connection_config(
[
@@ -329,7 +320,9 @@ class Products {
'queryClass' => '\WC_Product_Query',
'connectionArgs' => self::get_connection_args(),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
add_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' );
remove_filter( 'graphql_post_object_connection_args', [ __CLASS__, 'bypass_get_args_sanitization' ], 10, 3 );
$resolver = self::set_ordering_query_args( $resolver, $args );
@@ -340,6 +333,19 @@ class Products {
);
}
/**
* Bypass arg sanization in Post Object Connection Resolver.
*
* @param array $args Sanitized GraphQL args passed to the resolver.
* @param PostObjectConnectionResolver $connection_resolver Instance of the ConnectionResolver.
* @param array $all_args array of arguments input in the field as part of the GraphQL query.
* @return array
*/
public static function bypass_get_args_sanitization( $args, $connection_resolver, $all_args ) {
return $all_args;
}
/**
* Undocumented function
*
@@ -527,20 +533,21 @@ class Products {
* from a GraphQL Query to the WP_Query
*
* @param array $query_args The mapped query arguments.
* @param array $where_args Query "where" args.
* @param array $args Query "where" args.
* @param mixed $source The query results for a query calling this.
* @param array $args All of the arguments for the query (not just the "where" args).
* @param array $all_args All of the arguments for the query (not just the "where" args).
* @param AppContext $context The AppContext object.
* @param ResolveInfo $info The ResolveInfo object.
* @param mixed|string|array $post_type The post type for the query.
*
* @return array Query arguments.
*/
public static function map_input_fields_to_wp_query( $query_args, $where_args, $source, $args, $context, $info, $post_type ) {
public static function map_input_fields_to_wp_query( $query_args, $args, $source, $all_args, $context, $info, $post_type ) {
if ( ! in_array( 'product', $post_type, true ) && ! in_array( 'product_variation', $post_type, true ) ) {
return $query_args;
}
$where_args = $all_args['where'];
$query_args = array_merge(
$query_args,
map_shared_input_fields_to_wp_query( $where_args )
@@ -646,11 +653,11 @@ class Products {
if ( empty( $where_args['type'] ) && empty( $where_args['typeIn'] ) && ! empty( $where_args['supportedTypesOnly'] )
&& true === $where_args['supportedTypesOnly'] ) {
$supported_types = array_keys( WP_GraphQL_WooCommerce::get_enabled_product_types() );
$terms = ! empty( $where_args['typeNotIn'] )
? array_diff( $supported_types, $where_args['typeNotIn'] )
: $supported_types;
$tax_query[] = [
$supported_types = array_keys( WP_GraphQL_WooCommerce::get_enabled_product_types() );
$terms = ! empty( $where_args['typeNotIn'] )
? array_diff( $supported_types, $where_args['typeNotIn'] )
: $supported_types;
$tax_query[] = [
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $terms,
@@ -787,7 +794,7 @@ class Products {
: PHP_INT_MAX;
$meta_query[] = apply_filters(
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
'woocommerce_get_min_max_price_meta_query',
[
'key' => '_price',
@@ -850,4 +857,5 @@ class Products {
return $query_args;
}
}
+4
View File
@@ -39,6 +39,10 @@ class WC_Terms extends TermObjects {
// Registers the connections between each allowed PostObjectType and it's TermObjects.
if ( ! empty( $wc_post_types ) && is_array( $wc_post_types ) ) {
foreach ( $wc_post_types as $post_type ) {
if ( 'product' === $post_type ) {
continue;
}
if ( in_array( $post_type, $tax_object->object_type, true ) ) {
$post_type_object = get_post_type_object( $post_type );
register_graphql_connection(
@@ -23,6 +23,7 @@ class Attribute {
'Attribute',
[
'description' => __( 'Attribute object', 'wp-graphql-woocommerce' ),
'interfaces' => [ 'Node' ],
'fields' => [
'name' => [
'type' => 'String',
@@ -23,6 +23,7 @@ class Product_Attribute {
'ProductAttribute',
[
'description' => __( 'Product attribute object', 'wp-graphql-woocommerce' ),
'interfaces' => [ 'Node' ],
'fields' => self::get_fields(),
'resolveType' => function( $value ) use ( &$type_registry ) {
if ( $value->is_taxonomy() ) {
@@ -45,9 +46,6 @@ class Product_Attribute {
'id' => [
'type' => [ 'non_null' => 'ID' ],
'description' => __( 'Attribute Global ID', 'wp-graphql-woocommerce' ),
'resolve' => function ( $attribute ) {
return ! empty( $attribute->_relay_id ) ? $attribute->_relay_id : null;
},
],
'attributeId' => [
'type' => [ 'non_null' => 'Int' ],
+5 -62
View File
@@ -24,30 +24,12 @@ class Product {
/**
* Registers the "Product" interface.
*
* @param \WPGraphQL\Registry\TypeRegistry $type_registry Instance of the WPGraphQL TypeRegistry.
*/
public static function register_interface( &$type_registry ) {
register_graphql_interface_type(
'Product',
[
'description' => __( 'Product object', 'wp-graphql-woocommerce' ),
'fields' => self::get_fields(),
'resolveType' => function( $value ) use ( &$type_registry ) {
$possible_types = WP_GraphQL_WooCommerce::get_enabled_product_types();
if ( isset( $possible_types[ $value->type ] ) ) {
return $type_registry->get_type( $possible_types[ $value->type ] );
}
throw new UserError(
sprintf(
/* translators: %s: Product type */
__( 'The "%s" product type is not supported by the core WPGraphQL WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ),
$value->type
)
);
},
]
);
public static function register_interface() {
// Register the fields to the Product Interface
// the product interface is defined by the post_type registration.
register_graphql_fields( 'Product', self::get_fields() );
register_graphql_field(
'RootQuery',
@@ -112,26 +94,6 @@ class Product {
*/
public static function get_fields() {
return [
'id' => [
'type' => [ 'non_null' => 'ID' ],
'description' => __( 'The globally unique identifier for the product', 'wp-graphql-woocommerce' ),
],
'databaseId' => [
'type' => [ 'non_null' => 'Int' ],
'description' => __( 'The ID of the product in the database', 'wp-graphql-woocommerce' ),
],
'slug' => [
'type' => 'String',
'description' => __( 'Product slug', 'wp-graphql-woocommerce' ),
],
'date' => [
'type' => 'String',
'description' => __( 'Date product created', 'wp-graphql-woocommerce' ),
],
'modified' => [
'type' => 'String',
'description' => __( 'Date product last updated', 'wp-graphql-woocommerce' ),
],
'type' => [
'type' => 'ProductTypesEnum',
'description' => __( 'Product type', 'wp-graphql-woocommerce' ),
@@ -140,10 +102,6 @@ class Product {
'type' => 'String',
'description' => __( 'Product name', 'wp-graphql-woocommerce' ),
],
'status' => [
'type' => 'String',
'description' => __( 'Product status', 'wp-graphql-woocommerce' ),
],
'featured' => [
'type' => 'Boolean',
'description' => __( 'If the product is featured', 'wp-graphql-woocommerce' ),
@@ -223,13 +181,6 @@ class Product {
'type' => 'Int',
'description' => __( 'Product review count', 'wp-graphql-woocommerce' ),
],
'parent' => [
'type' => 'Product',
'description' => __( 'Parent product', 'wp-graphql-woocommerce' ),
'resolve' => function( $source, array $args, AppContext $context ) {
return Factory::resolve_crud_object( $source->parent_id, $context );
},
],
'image' => [
'type' => 'MediaItem',
'description' => __( 'Main image', 'wp-graphql-woocommerce' ),
@@ -249,14 +200,6 @@ class Product {
'type' => 'Boolean',
'description' => __( 'Can product be purchased?', 'wp-graphql-woocommerce' ),
],
'link' => [
'type' => 'String',
'description' => __( 'The permalink of the post', 'wp-graphql-woocommerce' ),
'resolve' => function( $source ) {
$permalink = get_post_permalink( $source->ID );
return ! empty( $permalink ) ? $permalink : null;
},
],
'metaData' => \WPGraphQL\WooCommerce\Type\WPObject\Meta_Data_Type::get_metadata_field_definition(),
];
}
+1
View File
@@ -429,6 +429,7 @@ class Cart_Type {
'CartItem',
[
'description' => __( 'A item in the cart', 'wp-graphql-woocommerce' ),
'interfaces' => [ 'Node' ],
'fields' => [
'key' => [
'type' => [ 'non_null' => 'ID' ],
@@ -27,6 +27,7 @@ class Downloadable_Item_Type {
'DownloadableItem',
[
'description' => __( 'A downloadable item', 'wp-graphql-woocommerce' ),
'interfaces' => [ 'Node' ],
'fields' => [
'downloadId' => [
'type' => [ 'non_null' => 'String' ],
@@ -247,6 +247,7 @@ class Order_Item_Type {
'description' => $config[0],
'fields' => self::get_fields( $config[1] ),
'connections' => ! empty( $config[2] ) ? $config[2] : null,
'interfaces' => [ 'Node' ],
]
);
}
@@ -23,9 +23,10 @@ class Payment_Gateway_Type {
'PaymentGateway',
[
'description' => __( 'A payment gateway object', 'wp-graphql-woocommerce' ),
'interfaces' => [ 'Node' ],
'fields' => [
'id' => [
'type' => [ 'non_null' => 'String' ],
'type' => [ 'non_null' => 'ID' ],
'description' => __( 'gateway\'s title', 'wp-graphql-woocommerce' ),
'resolve' => function( $source ) {
return ! empty( $source->id ) ? $source->id : null;
@@ -8,6 +8,8 @@
namespace WPGraphQL\WooCommerce\Type\WPObject;
use GraphQLRelay\Relay;
/**
* Class Product_Attribute_Types
*/
@@ -24,6 +26,13 @@ class Product_Attribute_Types {
'description' => __( 'A product attribute object', 'wp-graphql-woocommerce' ),
'interfaces' => [ 'ProductAttribute' ],
'fields' => [
'id' => [
'type' => [ 'non_null' => 'ID' ],
'description' => __( 'Attribute Global ID', 'wp-graphql-woocommerce' ),
'resolve' => function ( $attribute ) {
return ! empty( $attribute->_relay_id ) ? $attribute->_relay_id : Relay::toGlobalId( 'LocalProductAttribute', $attribute->get_id() );
},
],
'scope' => [
'type' => [ 'non_null' => 'ProductAttributeTypesEnum' ],
'description' => __( 'Product attribute scope.', 'wp-graphql-woocommerce' ),
@@ -42,6 +51,13 @@ class Product_Attribute_Types {
'description' => __( 'A product attribute object', 'wp-graphql-woocommerce' ),
'interfaces' => [ 'ProductAttribute' ],
'fields' => [
'id' => [
'type' => [ 'non_null' => 'ID' ],
'description' => __( 'Attribute Global ID', 'wp-graphql-woocommerce' ),
'resolve' => function ( $attribute ) {
return ! empty( $attribute->_relay_id ) ? $attribute->_relay_id : Relay::toGlobalId( 'GlobalProductAttribute', $attribute->get_id() );
},
],
'scope' => [
'type' => [ 'non_null' => 'ProductAttributeTypesEnum' ],
'description' => __( 'Product attribute scope.', 'wp-graphql-woocommerce' ),
+1 -1
View File
@@ -221,7 +221,7 @@ class CouponQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQL
$this->loginAsCustomer();
$response = $this->graphql( compact( 'query' ) );
$expected = [
$this->expectedField( 'coupons.nodes', self::IS_NULL ),
$this->expectedField( 'coupons.nodes', self::IS_FALSY ),
];
$this->assertQuerySuccessful( $response, $expected );
+6 -1
View File
@@ -3,11 +3,16 @@
// Turn off "QL_SESSION_HANDLER" for unit tests.
//define( 'NO_QL_SESSION_HANDLER', true );
/**
* Remove the "extensions" payload from GraphQL results
* so that tests can make assertions without worrying about what's in the extensions payload
*/
add_filter(
'graphql_request_results',
function( $response ) {
unset( $response['extensions'] );
return $response;
}
},
99
);