diff --git a/.github/workflows/testing-integration.yml b/.github/workflows/testing-integration.yml index fdb125d2d..2c2b55c0d 100644 --- a/.github/workflows/testing-integration.yml +++ b/.github/workflows/testing-integration.yml @@ -1,6 +1,8 @@ name: Testing Integration on: + schedule: + - cron: '* * */7 * *' push: branches: - develop diff --git a/README.txt b/README.txt index 7dcef4e95..fd9fdaf30 100644 --- a/README.txt +++ b/README.txt @@ -5,7 +5,7 @@ Requires at least: 4.9 Tested up to: 5.2 Requires PHP: 7.0 Requires WooCommerce: 3.0.0 -Requires WPGraphQL: 0.6.0+ +Requires WPGraphQL: 0.8.0+ Works with WPGraphQL-JWT-Authentication: 0.4.0+ Stable tag: 0.4.4 License: GPL-3 diff --git a/bin/install-wp-tests.sh b/bin/install-test-env.sh similarity index 83% rename from bin/install-wp-tests.sh rename to bin/install-test-env.sh index 819ab051d..976ded0ae 100644 --- a/bin/install-wp-tests.sh +++ b/bin/install-test-env.sh @@ -110,35 +110,6 @@ install_wp() { download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php } -install_test_suite() { - # portable in-place argument for both GNU sed and Mac OSX sed - if [[ $(uname -s) == 'Darwin' ]]; then - local ioption='-i.bak' - else - local ioption='-i' - fi - - # set up testing suite if it doesn't yet exist - if [ ! -d $WP_TESTS_DIR ]; then - # set up testing suite - mkdir -p $WP_TESTS_DIR - svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes - svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data - fi - - if [ ! -f wp-tests-config.php ]; then - download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php - # remove all forward slashes in the end - WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") - sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php - fi - -} - install_db() { if [ ${SKIP_DB_CREATE} = "true" ]; then @@ -215,7 +186,6 @@ setup_plugin() { } install_wp -install_test_suite install_db configure_wordpress setup_woocommerce diff --git a/composer.json b/composer.json index 2d6bcda22..9b6600781 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ }, "autoload": { "files":[ - "includes/connection/common-post-type-args.php", + "includes/connection/wc-cpt-connection-args.php", "includes/functions.php" ], "psr-4": { @@ -43,7 +43,7 @@ ] }, "scripts": { - "install-wp-tests": "bash bin/install-wp-tests.sh", + "install-test-env": "bash bin/install-test-env.sh", "test": "vendor/bin/codecept run", "functional-test": "vendor/bin/codecept run functional", "acceptance-test": "vendor/bin/codecept run acceptance", diff --git a/includes/class-core-schema-filters.php b/includes/class-core-schema-filters.php index 19c3706e8..15ec0ae22 100644 --- a/includes/class-core-schema-filters.php +++ b/includes/class-core-schema-filters.php @@ -9,27 +9,14 @@ namespace WPGraphQL\WooCommerce; use WPGraphQL\WooCommerce\Data\Loader\WC_Customer_Loader; -use WPGraphQL\WooCommerce\Data\Loader\WC_Post_Crud_Loader; +use WPGraphQL\WooCommerce\Data\Loader\WC_CPT_Loader; +use WPGraphQL\WooCommerce\Data\Loader\WC_Db_Loader; use WPGraphQL\WooCommerce\Data\Factory; /** * Class Core_Schema_Filters */ class Core_Schema_Filters { - /** - * Stores instance WC_Customer_Loader - * - * @var WC_Customer_Loader - */ - private static $customer_loader; - - /** - * Stores instance WC_Post_Crud_Loader - * - * @var WC_Post_Crud_Loader - */ - private static $post_crud_loader; - /** * Register filters */ @@ -104,34 +91,6 @@ public static function add_filters() { ); } - /** - * Initializes WC_Loader instance - * - * @param AppContext $context - AppContext. - * - * @return WC_Post_Crud_Loader - */ - public static function post_crud_loader( $context ) { - if ( is_null( self::$post_crud_loader ) ) { - self::$post_crud_loader = new WC_Post_Crud_Loader( $context ); - } - return self::$post_crud_loader; - } - - /** - * Initializes Customer_Loader instance - * - * @param AppContext $context - AppContext. - * - * @return WC_Customer_Loader - */ - public static function customer_loader( $context ) { - if ( is_null( self::$customer_loader ) ) { - self::$customer_loader = new WC_Customer_Loader( $context ); - } - return self::$customer_loader; - } - /** * Registers WooCommerce post-types to be used in GraphQL schema * @@ -255,12 +214,20 @@ public static function register_taxonomy_args( $args, $taxonomy ) { */ public static function graphql_data_loaders( $loaders, $context ) { // WooCommerce customer loader. - $customer_loader = self::customer_loader( $context ); + $customer_loader = new WC_Customer_Loader( $context ); $loaders['wc_customer'] = &$customer_loader; - // WooCommerce crud object loader. - $post_crud_loader = self::post_crud_loader( $context ); - $loaders['wc_post_crud'] = &$post_crud_loader; + // WooCommerce CPT loader. + $cpt_loader = new WC_CPT_Loader( $context ); + $loaders['wc_cpt'] = &$cpt_loader; + + // WooCommerce DB loaders. + $cart_item_loader = new WC_Db_Loader( $context, 'CART_ITEM' ); + $loaders['cart_item'] = &$cart_item_loader; + $downloadable_item_loader = new WC_Db_Loader( $context, 'DOWNLOADABLE_ITEM' ); + $loaders['downloadable_item'] = &$downloadable_item_loader; + $tax_rate_loader = new WC_Db_Loader( $context, 'TAX_RATE' ); + $loaders['tax_rate'] = &$tax_rate_loader; return $loaders; } diff --git a/includes/connection/class-coupons.php b/includes/connection/class-coupons.php index 129046ecc..3bdbc544b 100644 --- a/includes/connection/class-coupons.php +++ b/includes/connection/class-coupons.php @@ -50,10 +50,7 @@ public static function get_connection_config( $args = array() ): array { 'toType' => 'Coupon', 'fromFieldName' => 'coupons', 'connectionArgs' => self::get_connection_args(), - 'resolveNode' => function( $id, array $args, AppContext $context ) { - return Factory::resolve_crud_object( $id, $context ); - }, - 'resolve' => function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + 'resolve' => function ( $source, $args, $context, $info ) { return Factory::resolve_coupon_connection( $source, $args, $context, $info ); }, ), @@ -68,7 +65,7 @@ public static function get_connection_config( $args = array() ): array { */ public static function get_connection_args(): array { return array_merge( - get_common_post_type_args(), + get_wc_cpt_connection_args(), array( 'code' => array( 'type' => 'String', diff --git a/includes/connection/class-customers.php b/includes/connection/class-customers.php index 525302bb5..01328cdec 100644 --- a/includes/connection/class-customers.php +++ b/includes/connection/class-customers.php @@ -54,9 +54,6 @@ public static function get_connection_config( $args ): array { return array_merge( array( 'connectionArgs' => self::get_connection_args(), - 'resolveNode' => function( $id, array $args, AppContext $context ) { - return Factory::resolve_customer( $id, $context ); - }, 'resolve' => function ( $source, array $args, AppContext $context, ResolveInfo $info ) { return Factory::resolve_customer_connection( $source, $args, $context, $info ); }, diff --git a/includes/connection/class-orders.php b/includes/connection/class-orders.php index 42a3bda0a..270c1fbdb 100644 --- a/includes/connection/class-orders.php +++ b/includes/connection/class-orders.php @@ -54,9 +54,6 @@ public static function get_connection_config( $args = array() ): array { 'toType' => 'Order', 'fromFieldName' => 'orders', 'connectionArgs' => self::get_connection_args(), - 'resolveNode' => function( $id, $args, AppContext $context ) { - return Factory::resolve_crud_object( $id, $context ); - }, 'resolve' => function ( $source, array $args, AppContext $context, ResolveInfo $info ) { return Factory::resolve_order_connection( $source, $args, $context, $info ); }, @@ -75,7 +72,7 @@ public static function get_connection_args( $access = 'public' ): array { switch ( $access ) { case 'private': return array_merge( - get_common_post_type_args(), + get_wc_cpt_connection_args(), array( 'statuses' => array( 'type' => array( 'list_of' => 'OrderStatusEnum' ), diff --git a/includes/connection/class-product-reviews.php b/includes/connection/class-product-reviews.php index 68b0faf37..ccbc68f5d 100644 --- a/includes/connection/class-product-reviews.php +++ b/includes/connection/class-product-reviews.php @@ -46,7 +46,8 @@ public static function register_connections() { 'type' => 'Float', 'description' => __( 'Review rating', 'wp-graphql-woocommerce' ), 'resolve' => function( $source ) { - $rating = get_comment_meta( $source['node'], 'rating', true ); + $review = $source['node']; + $rating = get_comment_meta( $review->commentId, 'rating', true ); return $rating ? $rating : 0; }, ), diff --git a/includes/connection/class-products.php b/includes/connection/class-products.php index 825e3b08b..1bf42d0aa 100644 --- a/includes/connection/class-products.php +++ b/includes/connection/class-products.php @@ -126,9 +126,6 @@ public static function get_connection_config( $args = array() ): array { 'toType' => 'Product', 'fromFieldName' => 'products', 'connectionArgs' => self::get_connection_args(), - 'resolveNode' => function( $id, array $args, AppContext $context ) { - return Factory::resolve_crud_object( $id, $context ); - }, 'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) { return Factory::resolve_product_connection( $source, $args, $context, $info ); }, @@ -277,6 +274,6 @@ public static function get_connection_args(): array { ); } - return array_merge( get_common_post_type_args(), $args ); + return array_merge( get_wc_cpt_connection_args(), $args ); } } diff --git a/includes/connection/class-refunds.php b/includes/connection/class-refunds.php index 48fa9fed7..a868ee7ba 100644 --- a/includes/connection/class-refunds.php +++ b/includes/connection/class-refunds.php @@ -51,9 +51,6 @@ public static function get_connection_config( $args = array() ): array { 'toType' => 'Refund', 'fromFieldName' => 'refunds', 'connectionArgs' => self::get_connection_args(), - 'resolveNode' => function( $id, array $args, AppContext $context ) { - return Factory::resolve_crud_object( $id, $context ); - }, 'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) { return Factory::resolve_refund_connection( $source, $args, $context, $info ); }, @@ -69,7 +66,7 @@ public static function get_connection_config( $args = array() ): array { */ public static function get_connection_args(): array { return array_merge( - get_common_post_type_args(), + get_wc_cpt_connection_args(), array( 'statuses' => array( 'type' => array( 'list_of' => 'String' ), diff --git a/includes/connection/class-tax-rates.php b/includes/connection/class-tax-rates.php index 7433ae141..84ad21db2 100644 --- a/includes/connection/class-tax-rates.php +++ b/includes/connection/class-tax-rates.php @@ -41,9 +41,6 @@ public static function get_connection_config( $args = array() ): array { 'toType' => 'TaxRate', 'fromFieldName' => 'taxRates', 'connectionArgs' => self::get_connection_args(), - 'resolveNode' => function( $id ) { - return Factory::resolve_tax_rate( $id ); - }, 'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) { return Factory::resolve_tax_rate_connection( $source, $args, $context, $info ); }, diff --git a/includes/connection/common-post-type-args.php b/includes/connection/wc-cpt-connection-args.php similarity index 89% rename from includes/connection/common-post-type-args.php rename to includes/connection/wc-cpt-connection-args.php index a29be73db..3be57e61d 100644 --- a/includes/connection/common-post-type-args.php +++ b/includes/connection/wc-cpt-connection-args.php @@ -1,6 +1,6 @@ array( 'type' => 'String', diff --git a/includes/data/class-factory.php b/includes/data/class-factory.php index a3dd41d0f..9470b08bd 100644 --- a/includes/data/class-factory.php +++ b/includes/data/class-factory.php @@ -84,12 +84,11 @@ public static function resolve_crud_object( $id, AppContext $context ) { if ( empty( $id ) || ! absint( $id ) ) { return null; } - $object_id = absint( $id ); - $loader = $context->getLoader( 'wc_post_crud' ); - $loader->buffer( array( $object_id ) ); + + $context->getLoader( 'wc_cpt' )->buffer( array( $id ) ); return new Deferred( - function () use ( $loader, $object_id ) { - return $loader->load( $object_id ); + function () use ( $id, $context ) { + return $context->getLoader( 'wc_cpt' )->load( $id ); } ); } @@ -117,41 +116,24 @@ public static function resolve_order_item( $item ) { /** * Returns the tax rate Model for the tax rate ID. * - * @param int $id - Tax rate ID. - * - * @return Tax_Rate - * @access public - * @throws UserError Invalid object. + * @param string $id - Tax rate ID. + * @param AppContext $context - AppContext object. + * + * @return Deferred object */ - public static function resolve_tax_rate( $id ) { - global $wpdb; - - $rate = \WC_Tax::_get_tax_rate( $id, OBJECT ); - if ( ! \is_wp_error( $rate ) && ! empty( $rate ) ) { - // Get locales from a tax rate. - $locales = $wpdb->get_results( - $wpdb->prepare( - "SELECT location_code, location_type - FROM {$wpdb->prefix}woocommerce_tax_rate_locations - WHERE tax_rate_id = %d", - $rate->tax_rate_id - ) - ); + public static function resolve_tax_rate( $id, AppContext $context ) { + if ( empty( $id ) || ! is_numeric( $id ) ) { + return null; + } - foreach ( $locales as $locale ) { - if ( empty( $rate->{'tax_rate_' . $locale->location_type} ) ) { - $rate->{'tax_rate_' . $locale->location_type} = array(); - } - $rate->{'tax_rate_' . $locale->location_type}[] = $locale->location_code; + $id = absint( $id ); + $loader = $context->getLoader( 'tax_rate' ); + $loader->buffer( array( $id ) ); + return new Deferred( + function () use ( $loader, $id ) { + return $loader->load( $id ); } - - return new Tax_Rate( $rate ); - } else { - throw new UserError( - /* translators: %s: Tax rate ID */ - sprintf( __( 'No Tax Rate assigned to ID %s was found ', 'wp-graphql-woocommerce' ), $id ) - ); - } + ); } /** @@ -198,12 +180,22 @@ public static function resolve_cart() { /** * Resolves a cart item by key. * - * @param string $id cart item key. - * - * @return object + * @param string $key Cart item key. + * @param AppContext $context AppContext object. + * + * @return Deferred object */ - public static function resolve_cart_item( $id ) { - return self::resolve_cart()->get_cart_item( $id ); + public static function resolve_cart_item( $key, AppContext $context ) { + if ( empty( $key ) ) { + return null; + } + + $context->getLoader( 'cart_item' )->buffer( array( $key ) ); + return new Deferred( + function () use ( $key, $context ) { + return $context->getLoader( 'cart_item' )->load( $key ); + } + ); } /** @@ -221,6 +213,28 @@ public static function resolve_cart_fee( $id ) { return null; } + /** + * Resolves a downloadable item by ID. + * + * @param int $id Downloadable item ID. + * @param AppContext $context AppContext object. + * + * @return Deferred object + */ + public static function resolve_downloadable_item( $id, AppContext $context ) { + if ( empty( $id ) || ! absint( $id ) ) { + return null; + } + $object_id = absint( $id ); + $loader = $context->getLoader( 'downloadable_item' ); + $loader->buffer( array( $object_id ) ); + return new Deferred( + function () use ( $loader, $object_id ) { + return $loader->load( $object_id ); + } + ); + } + /** * Resolves Relay node for some WooGraphQL types. * @@ -247,7 +261,7 @@ public static function resolve_node( $node, $id, $type, $context ) { $node = self::resolve_shipping_method( $id ); break; case 'tax_rate': - $node = self::resolve_tax_rate( $id ); + $node = self::resolve_tax_rate( $id, $context ); break; } diff --git a/includes/data/connection/class-cart-item-connection-resolver.php b/includes/data/connection/class-cart-item-connection-resolver.php index 9dc6766c8..8b500f816 100644 --- a/includes/data/connection/class-cart-item-connection-resolver.php +++ b/includes/data/connection/class-cart-item-connection-resolver.php @@ -15,15 +15,27 @@ use GraphQLRelay\Connection\ArrayConnection; use WPGraphQL\AppContext; use WPGraphQL\Data\Connection\AbstractConnectionResolver; +use WPGraphQL\WooCommerce\Data\Factory; /** * Class Cart_Item_Connection_Resolver */ class Cart_Item_Connection_Resolver extends AbstractConnectionResolver { /** - * Include shared connection functions. + * Include Db Loader connection common functions. */ - use WC_Connection_Functions; + use WC_Db_Loader_Common; + + const PREFIX = 'CI'; + + /** + * Return the name of the loader to be used with the connection resolver + * + * @return string + */ + public function get_loader_name() { + return 'cart_item'; + } /** * Confirms if cart items should be retrieved. @@ -89,7 +101,13 @@ public function get_query() { $cart_items = array_splice( $cart_items, 0, $cursor_offset ); } - return array_values( $cart_items ); + // Cache cart items for later. + foreach ( $cart_items as $item ) { + $this->loader->prime( $item['key'], $item ); + } + + // Return cart item keys. + return array_column( $cart_items, 'key' ); } /** @@ -103,27 +121,23 @@ public function get_offset() { // Get the offset. if ( ! empty( $this->args['after'] ) ) { - $offset = $this->args['after']; + $offset = $this->cursor_to_offset( self::PREFIX, $this->args['after'] ); } elseif ( ! empty( $this->args['before'] ) ) { - $offset = $this->args['before']; + $offset = $this->cursor_to_offset( self::PREFIX, $this->args['before'] ); } - /** - * Return the higher of the two values - */ return $offset; } /** - * Create cursor for cart item node. + * Create cursor for downloadable item node. * - * @param array $node Cart item. - * @param string $key Cart item key. + * @param string $id Cart item key. * * @return string */ - protected function get_cursor_for_node( $node, $key = null ) { - return $node['key']; + protected function get_cursor_for_node( $id ) { + return $this->offset_to_cursor( self::PREFIX, $id ); } /** @@ -131,18 +145,30 @@ protected function get_cursor_for_node( $node, $key = null ) { * * @return array */ - public function get_items() { + public function get_ids() { return ! empty( $this->query ) ? $this->query : array(); } /** - * Wrapper for "WC_Connection_Functions::is_valid_cart_item_offset()" + * Check if cart item key is valid by confirming the validity of + * the cart item in the cart encoded into cart item key. * - * @param integer $offset Post ID. + * @param string $offset Cart item key. * * @return bool */ public function is_valid_offset( $offset ) { - return $this->is_valid_cart_item_offset( $offset ); + return ! empty( $this->source->get_cart_item( $offset ) ); + } + + /** + * Validates cart item model. + * + * @param array $model Cart item model. + * + * @return bool + */ + protected function is_valid_model( $model ) { + return ! empty( $model ) && ! empty( $model['key'] ) && ! empty( $model['product_id'] ); } } diff --git a/includes/data/connection/class-coupon-connection-resolver.php b/includes/data/connection/class-coupon-connection-resolver.php index d533c7039..795cdbfd8 100644 --- a/includes/data/connection/class-coupon-connection-resolver.php +++ b/includes/data/connection/class-coupon-connection-resolver.php @@ -14,15 +14,16 @@ use WPGraphQL\AppContext; use WPGraphQL\Data\Connection\AbstractConnectionResolver; use WPGraphQL\Extension\WooCommerce\Model\Order; +use WPGraphQL\Extension\WooCommerce\Model\Coupon; /** * Class Coupon_Connection_Resolver */ class Coupon_Connection_Resolver extends AbstractConnectionResolver { /** - * Include shared connection functions. + * Include CPT Loader connection common functions. */ - use WC_Connection_Functions; + use WC_CPT_Loader_Common; /** * The name of the post type, or array of post types the connection resolver is resolving for @@ -50,6 +51,26 @@ public function __construct( $source, $args, $context, $info ) { parent::__construct( $source, $args, $context, $info ); } + /** + * Return the name of the loader to be used with the connection resolver + * + * @return string + */ + public function get_loader_name() { + return 'wc_cpt'; + } + + /** + * Given an ID, return the model for the entity or null + * + * @param integer $id + * + * @return mixed|Coupon|null + */ + public function get_node_by_id( $id ) { + return $this->get_cpt_model_by_id( $id ); + } + /** * Confirms the uses has the privileges to query Coupons * @@ -165,7 +186,7 @@ public function get_query() { * * @return array */ - public function get_items() { + public function get_ids() { return ! empty( $this->query->posts ) ? $this->query->posts : array(); } diff --git a/includes/data/connection/class-customer-connection-resolver.php b/includes/data/connection/class-customer-connection-resolver.php index e55bea372..5e923809d 100644 --- a/includes/data/connection/class-customer-connection-resolver.php +++ b/includes/data/connection/class-customer-connection-resolver.php @@ -19,9 +19,13 @@ */ class Customer_Connection_Resolver extends AbstractConnectionResolver { /** - * Include shared connection functions. + * Return the name of the loader to be used with the connection resolver + * + * @return string */ - use WC_Connection_Functions; + public function get_loader_name() { + return 'wc_customer'; + } /** * Confirms the uses has the privileges to query Customers @@ -158,7 +162,7 @@ public function get_query() { * * @return array */ - public function get_items() { + public function get_ids() { $results = $this->get_query()->get_results(); return ! empty( $results ) ? $results : array(); } @@ -215,13 +219,21 @@ public function sanitize_input_fields( array $where_args ) { } /** - * Wrapper for "WC_Connection_Functions::is_valid_user_offset()" + * Determine whether or not the the offset is valid, i.e the user corresponding to the offset exists. + * Offset is equivalent to user_id. So this function is equivalent + * to checking if the user with the given ID exists. * - * @param integer $offset User ID. + * @param integer $offset User ID. * * @return bool */ public function is_valid_offset( $offset ) { - return $this->is_valid_user_offset( $offset ); + global $wpdb; + + if ( ! empty( wp_cache_get( $offset, 'users' ) ) ) { + return true; + } + + return $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS (SELECT 1 FROM $wpdb->users WHERE ID = %d)", $offset ) ); } } diff --git a/includes/data/connection/class-downloadable-item-connection-resolver.php b/includes/data/connection/class-downloadable-item-connection-resolver.php index 5c3c102af..fa254580c 100644 --- a/includes/data/connection/class-downloadable-item-connection-resolver.php +++ b/includes/data/connection/class-downloadable-item-connection-resolver.php @@ -15,6 +15,7 @@ use GraphQLRelay\Connection\ArrayConnection; use WPGraphQL\AppContext; use WPGraphQL\Data\Connection\AbstractConnectionResolver; +use WPGraphQL\WooCommerce\Data\Factory; use WPGraphQL\WooCommerce\Model\Customer; /** @@ -22,9 +23,20 @@ */ class Downloadable_Item_Connection_Resolver extends AbstractConnectionResolver { /** - * Include shared connection functions. + * Include Db Loader connection common functions. */ - use WC_Connection_Functions; + use WC_Db_Loader_Common; + + const PREFIX = 'DI'; + + /** + * Return the name of the loader to be used with the connection resolver + * + * @return string + */ + public function get_loader_name() { + return 'downloadable_item'; + } /** * Confirms if downloadable items should be retrieved. @@ -122,7 +134,7 @@ public function get_query() { } $cursor_key = $this->get_offset(); - $cursor_offset = array_search( $cursor_key, \array_column( $items, 'download_id' ), true ); + $cursor_offset = array_search( $cursor_key, array_column( $items, 'download_id' ), true ) ?? null; if ( ! empty( $this->args['after'] ) ) { $items = array_splice( $items, $cursor_offset + 1 ); @@ -130,7 +142,12 @@ public function get_query() { $items = array_splice( $items, 0, $cursor_offset ); } - return $items; + // Cache items for later. + foreach ( $items as $item ) { + $this->loader->prime( $item['download_id'], $item ); + } + + return array_column( $items, 'download_id' ); } /** @@ -144,27 +161,23 @@ public function get_offset() { // Get the offset. if ( ! empty( $this->args['after'] ) ) { - $offset = $this->args['after']; + $offset = $this->cursor_to_offset( self::PREFIX, $this->args['after'] ); } elseif ( ! empty( $this->args['before'] ) ) { - $offset = $this->args['before']; + $offset = $this->cursor_to_offset( self::PREFIX, $this->args['before'] ); } - /** - * Return the higher of the two values - */ return $offset; } /** * Create cursor for downloadable item node. * - * @param array $node Cart item. - * @param string $key Cart item key. + * @param string $id Downloadable item ID. * * @return string */ - protected function get_cursor_for_node( $node, $key = null ) { - return $node['download_id']; + protected function get_cursor_for_node( $id ) { + return $this->offset_to_cursor( self::PREFIX, $id ); } /** @@ -172,7 +185,7 @@ protected function get_cursor_for_node( $node, $key = null ) { * * @return array */ - public function get_items() { + public function get_ids() { return ! empty( $this->query ) ? $this->query : array(); } @@ -186,4 +199,18 @@ public function get_items() { public function is_valid_offset( $offset ) { return 'string' === gettype( $offset ); } + + /** + * Validates Model. + * + * If model isn't a class with a `fields` member, this function with have be overridden in + * the Connection class. + * + * @param array $model Downloadable item model. + * + * @return bool + */ + protected function is_valid_model( $model ) { + return isset( $model ) && ! empty( $model['download_id'] ); + } } diff --git a/includes/data/connection/class-order-connection-resolver.php b/includes/data/connection/class-order-connection-resolver.php index 77251e636..5485e2308 100644 --- a/includes/data/connection/class-order-connection-resolver.php +++ b/includes/data/connection/class-order-connection-resolver.php @@ -14,15 +14,16 @@ use WPGraphQL\AppContext; use WPGraphQL\Data\Connection\AbstractConnectionResolver; use WPGraphQL\WooCommerce\Model\Customer; +use WPGraphQL\WooCommerce\Model\Order; /** * Class Order_Connection_Resolver */ class Order_Connection_Resolver extends AbstractConnectionResolver { /** - * Include shared connection functions. + * Include CPT Loader connection common functions. */ - use WC_Connection_Functions; + use WC_CPT_Loader_Common; /** * The name of the post type, or array of post types the connection resolver is resolving for @@ -50,6 +51,26 @@ public function __construct( $source, $args, $context, $info ) { parent::__construct( $source, $args, $context, $info ); } + /** + * Return the name of the loader to be used with the connection resolver + * + * @return string + */ + public function get_loader_name() { + return 'wc_cpt'; + } + + /** + * Given an ID, return the model for the entity or null + * + * @param integer $id + * + * @return mixed|Order|null + */ + public function get_node_by_id( $id ) { + return $this->get_cpt_model_by_id( $id ); + } + /** * Checks if user is authorized to query orders * @@ -161,7 +182,7 @@ public function get_query() { * * @return array */ - public function get_items() { + public function get_ids() { return ! empty( $this->query->get_orders() ) ? $this->query->get_orders() : array(); } diff --git a/includes/data/connection/class-product-connection-resolver.php b/includes/data/connection/class-product-connection-resolver.php index bf85d9038..0cea820a2 100644 --- a/includes/data/connection/class-product-connection-resolver.php +++ b/includes/data/connection/class-product-connection-resolver.php @@ -13,19 +13,20 @@ use WPGraphQL\Data\Connection\AbstractConnectionResolver; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; +use WPGraphQL\Model\Term; use WPGraphQL\WooCommerce\Model\Coupon; use WPGraphQL\WooCommerce\Model\Customer; use WPGraphQL\WooCommerce\Model\Product; -use WPGraphQL\Model\Term; +use WPGraphQL\WooCommerce\Model\Product_Variation; /** * Class Product_Connection_Resolver */ class Product_Connection_Resolver extends AbstractConnectionResolver { /** - * Include shared connection functions. + * Include CPT Loader connection common functions. */ - use WC_Connection_Functions; + use WC_CPT_Loader_Common; /** * The name of the post type, or array of post types the connection resolver is resolving for @@ -66,6 +67,37 @@ public function __construct( $source, $args, $context, $info ) { parent::__construct( $source, $args, $context, $info ); } + /** + * Return the name of the loader to be used with the connection resolver + * + * @return string + */ + public function get_loader_name() { + return 'wc_cpt'; + } + + /** + * Given an ID, return the model for the entity or null + * + * @param integer $id + * + * @return Product|Product_Variation|null + * + * @throws \Exception + */ + public function get_node_by_id( $id ) { + $post = get_post( $id ); + if ( empty( $post ) || is_wp_error( $post ) ) { + return null; + } + + if ( 'product_variation' === $post->post_type ) { + return new Product_Variation( $id ); + } + + return new Product( $id ); + } + /** * Confirms the user has the privileges to query the products * @@ -292,7 +324,7 @@ public function get_query() { * * @return array */ - public function get_items() { + public function get_ids() { return ! empty( $this->query->posts ) ? $this->query->posts : array(); } diff --git a/includes/data/connection/class-refund-connection-resolver.php b/includes/data/connection/class-refund-connection-resolver.php index e68d5092a..99019751b 100644 --- a/includes/data/connection/class-refund-connection-resolver.php +++ b/includes/data/connection/class-refund-connection-resolver.php @@ -15,15 +15,16 @@ use WPGraphQL\Data\Connection\AbstractConnectionResolver; use WPGraphQL\WooCommerce\Model\Customer; use WPGraphQL\WooCommerce\Model\Order; +use WPGraphQL\WooCommerce\Model\Refund; /** * Class Refund_Connection_Resolver */ class Refund_Connection_Resolver extends AbstractConnectionResolver { /** - * Include shared connection functions. + * Include CPT Loader connection common functions. */ - use WC_Connection_Functions; + use WC_CPT_Loader_Common; /** * The name of the post type, or array of post types the connection resolver is resolving for @@ -51,6 +52,26 @@ public function __construct( $source, $args, $context, $info ) { parent::__construct( $source, $args, $context, $info ); } + /** + * Return the name of the loader to be used with the connection resolver + * + * @return string + */ + public function get_loader_name() { + return 'wc_cpt'; + } + + /** + * Given an ID, return the model for the entity or null + * + * @param integer $id + * + * @return mixed|Refund|null + */ + public function get_node_by_id( $id ) { + return $this->get_cpt_model_by_id( $id ); + } + /** * Confirms the uses has the privileges to query Refunds * @@ -173,7 +194,7 @@ public function get_query() { * * @return array */ - public function get_items() { + public function get_ids() { return ! empty( $this->query->get_orders() ) ? $this->query->get_orders() : array(); } diff --git a/includes/data/connection/class-tax-rate-connection-resolver.php b/includes/data/connection/class-tax-rate-connection-resolver.php index 68f7a830a..b64f534af 100644 --- a/includes/data/connection/class-tax-rate-connection-resolver.php +++ b/includes/data/connection/class-tax-rate-connection-resolver.php @@ -13,11 +13,22 @@ use WPGraphQL\Data\Connection\AbstractConnectionResolver; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; +use WPGraphQL\WooCommerce\Data\Factory; +use WPGraphQL\WooCommerce\Model\Tax_Rate; /** * Class Tax_Rate_Connection_Resolver */ class Tax_Rate_Connection_Resolver extends AbstractConnectionResolver { + /** + * Return the name of the loader to be used with the connection resolver + * + * @return string + */ + public function get_loader_name() { + return 'tax_rate'; + } + /** * Confirms the uses has the privileges to query Tax Rates * @@ -138,7 +149,7 @@ function( $rate ) { * * @return array */ - public function get_items() { + public function get_ids() { return ! empty( $this->query ) ? $this->query : array(); } diff --git a/includes/data/connection/trait-wc-connection-functions.php b/includes/data/connection/trait-wc-cpt-loader-common.php similarity index 70% rename from includes/data/connection/trait-wc-connection-functions.php rename to includes/data/connection/trait-wc-cpt-loader-common.php index ed28b163b..f3d883d2f 100644 --- a/includes/data/connection/trait-wc-connection-functions.php +++ b/includes/data/connection/trait-wc-cpt-loader-common.php @@ -1,17 +1,19 @@ get_var( $wpdb->prepare( "SELECT EXISTS (SELECT 1 FROM $wpdb->posts WHERE ID = %d)", $offset ) ); } - /** - * Determine whether or not the the offset is valid, i.e the cart item corresponding to the offset exists. - * Offset is equivalent to a cart item key. So this function is equivalent - * to checking if the cart item with the given key exists. - * - * @param string $offset Cart item key. - * - * @return bool - */ - public function is_valid_cart_item_offset( $offset ) { - return ! empty( WC()->cart->get_cart_item( $offset ) ); - } - - /** - * Determine whether or not the the offset is valid, i.e the user corresponding to the offset exists. - * Offset is equivalent to user_id. So this function is equivalent - * to checking if the user with the given ID exists. - * - * @param integer $offset User ID. - * - * @return bool - */ - public function is_valid_user_offset( $offset ) { - global $wpdb; - - if ( ! empty( wp_cache_get( $offset, 'users' ) ) ) { - return true; - } - - return $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS (SELECT 1 FROM $wpdb->users WHERE ID = %d)", $offset ) ); - } - /** * Sanitizes common post-type connection query input. * @@ -135,4 +105,15 @@ public function sanitize_common_inputs( array $input ) { return $args; } + + /** + * Return WooCommerce CPT models by ID. + * + * @param int $id ID. + * + * @return mixed|null + */ + public function get_cpt_model_by_id( $id ) { + return $this->loader->resolve_model( get_post_type( $id ), $id ); + } } diff --git a/includes/data/connection/trait-wc-db-loader-common.php b/includes/data/connection/trait-wc-db-loader-common.php new file mode 100644 index 000000000..50c09bc90 --- /dev/null +++ b/includes/data/connection/trait-wc-db-loader-common.php @@ -0,0 +1,40 @@ +loaded_objects[ $key ] = new Deferred( - function() use ( $post_type, $key ) { - // Resolve post author for future capability checks. - if ( 'shop_order' === $post_type ) { - $customer_id = get_post_meta( $key, '_customer_user', true ); - if ( ! empty( $customer_id ) ) { - $customer = Factory::resolve_customer( $customer_id, $this->context ); - return $customer->then( - function () use ( $post_type, $key ) { - return $this->resolve_model( $post_type, $key ); - } - ); - } - } elseif ( 'product_variation' === $post_type || 'shop_refund' === $post_type ) { - $parent_id = get_post_field( 'post_parent', $key ); - $parent = Factory::resolve_crud_object( $parent_id, $this->context ); - return $parent->then( - function () use ( $post_type, $key ) { - return $this->resolve_model( $post_type, $key ); - } - ); + $context = $this->context; + $customer_id = null; + $parent_id = null; + + // Resolve post author for future capability checks. + switch ( $post_type ) { + case 'shop_order': + $customer_id = get_post_meta( $key, '_customer_user', true ); + if ( ! empty( $customer_id ) ) { + $this->context->getLoader( 'wc_customer' )->buffer( [ $customer_id ] ); + } + break; + case 'product_variation': + case 'shop_refund': + $parent_id = get_post_field( 'post_parent', $key ); + $this->buffer( [ $parent_id ] ); + break; + } + + /** + * This is a deferred function that allows us to do batch loading + * of dependant resources. When the Model Layer attempts to determine + * access control of a Post, it needs to know the owner of it, and + * if it's a revision, it needs the Parent. + * + * This deferred function allows for the objects to be loaded all at once + * instead of loading once per entity, thus reducing the n+1 problem. + */ + $load_dependencies = new Deferred( + function() use ( $key, $post_type, $customer_id, $parent_id, $context ) { + if ( ! empty( $customer_id ) ) { + $context->getLoader( 'wc_customer' )->load( $customer_id ); + } + if ( ! empty( $parent_id ) ) { + $this->load( $parent_id ); } + + /** + * Run an action when the dependencies are being loaded for + * Post Objects + */ + do_action( 'woographql_cpt_loader_load_dependencies', $this, $key, $post_type ); + + return; + } + ); + + /** + * Once dependencies are loaded, return the Post Object + */ + $loaded_posts[ $key ] = $load_dependencies->then( + function() use ( $post_type, $key ) { return $this->resolve_model( $post_type, $key ); } ); } - return ! empty( $this->loaded_objects ) ? $this->loaded_objects : array(); + + return ! empty( $loaded_posts ) ? $loaded_posts : []; } } diff --git a/includes/data/loader/class-wc-db-loader.php b/includes/data/loader/class-wc-db-loader.php new file mode 100644 index 000000000..e3175aa93 --- /dev/null +++ b/includes/data/loader/class-wc-db-loader.php @@ -0,0 +1,148 @@ +loader_type = $loader_type; + parent::__construct( $context ); + } + + /** + * Given array of keys, loads and returns a map consisting of keys from `keys` array and loaded + * posts as the values + * + * @param array $keys - array of IDs. + * + * @return array + * @throws \Exception Invalid loader type + */ + public function loadKeys( array $keys ) { + $loader = null; + switch ( $this->loader_type ) { + case 'CART_ITEM': + $loader = array( $this, 'load_cart_item_from_key' ); + break; + case 'DOWNLOADABLE_ITEM': + $loader = array( $this, 'load_downloadable_item_from_id' ); + break; + case 'TAX_RATE': + $loader = array( $this, 'load_tax_rate_from_id' ); + break; + default: + $loader = apply_filters( 'woographql_db_loader_func', null ); + if ( empty( $loader ) ) { + throw new \Exception( + /* translators: %s: Loader Type */ + sprintf( __( 'Loader type invalid: %s', 'wp-graphql-woocommerce' ), $this->loader_type ) + ); + } + } + + $loaded_items = array(); + + /** + * Loop over the keys and return an array of items. + */ + foreach ( $keys as $key ) { + $loaded_items[ $key ] = call_user_func( $loader, $key ); + } + + return ! empty( $loaded_items ) ? $loaded_items : array(); + } + + /** + * Returns the cart item connected the provided key. + * + * @param string $key - Cart item key. + * + * @return array + */ + public function load_cart_item_from_key( $key ) { + // Add the cart item's product and product variation to WC-CPT buffer. + return Factory::resolve_cart()->get_cart_item( $key ); + } + + /** + * Returns the downloadable item connected the provided IDs. + * + * @param int $id - Downloadable item ID. + * + * @return WC_Customer_Download|null + */ + public function load_downloadable_item_from_id( $id ) { + $node = new \WC_Customer_Download( $id ); + return 0 === $node->get_id() ? $node : null; + } + + /** + * Returns the tax rate connected the provided IDs. + * + * @param int $key - Tax rate IDs. + * + * @return Tax_Rate|null + */ + public function load_tax_rate_from_id( $id ) { + global $wpdb; + + $rate = \WC_Tax::_get_tax_rate( $id, OBJECT ); + if ( ! \is_wp_error( $rate ) && ! empty( $rate ) ) { + // Get locales from a tax rate. + $locales = $wpdb->get_results( + $wpdb->prepare( + "SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d", + $rate->tax_rate_id + ) + ); + + foreach ( $locales as $locale ) { + if ( empty( $rate->{'tax_rate_' . $locale->location_type} ) ) { + $rate->{'tax_rate_' . $locale->location_type} = array(); + } + $rate->{'tax_rate_' . $locale->location_type}[] = $locale->location_code; + } + return new Tax_Rate( $rate ); + } else { + return null; + } + } +} diff --git a/includes/data/mutation/class-cart-mutation.php b/includes/data/mutation/class-cart-mutation.php index 5ec0ab6c6..c8c7565cf 100644 --- a/includes/data/mutation/class-cart-mutation.php +++ b/includes/data/mutation/class-cart-mutation.php @@ -9,6 +9,7 @@ namespace WPGraphQL\WooCommerce\Data\Mutation; use GraphQL\Error\UserError; +use WPGraphQL\WooCommerce\Data\Factory; /** * Class - Cart_Mutation @@ -26,7 +27,7 @@ public static function get_cart_field( $fallback = false ) { 'resolve' => function ( $payload ) use ( $fallback ) { $cart = ! empty( $payload['cart'] ) ? $payload['cart'] : null; if ( is_null( $cart ) && $fallback ) { - $cart = \WC()->cart; + $cart = Factory::resolve_cart(); } return $cart; }, diff --git a/includes/type/interface/class-product.php b/includes/type/interface/class-product.php index e24a56fcb..5dac38c53 100644 --- a/includes/type/interface/class-product.php +++ b/includes/type/interface/class-product.php @@ -103,75 +103,6 @@ public static function register_interface( &$type_registry ) { }, ) ); - - /** - * DEPRECATED - * - * Will be removed in v0.5.x. - */ - register_graphql_field( - 'RootQuery', - 'productBy', - array( - 'type' => 'Product', - 'isDeprecated' => true, - 'deprecationReason' => __( - 'This query has been deprecation, and will be removed in v0.5.x. Please use "product(id: value, idType: DATABASE_ID|SLUG|SKU)" instead', - 'wp-graphql-woocommerce' - ), - 'description' => __( 'A product object', 'wp-graphql-woocommerce' ), - 'args' => array( - 'id' => array( - 'type' => 'ID', - 'description' => __( 'Get the product by its global ID', 'wp-graphql-woocommerce' ), - ), - 'productId' => array( - 'type' => 'Int', - 'description' => __( 'Get the product by its database ID', 'wp-graphql-woocommerce' ), - ), - 'slug' => array( - 'type' => 'String', - 'description' => __( 'Get the product by its slug', 'wp-graphql-woocommerce' ), - ), - 'sku' => array( - 'type' => 'String', - 'description' => __( 'Get the product by its sku', 'wp-graphql-woocommerce' ), - ), - ), - 'resolve' => function ( $source, array $args, AppContext $context ) { - $product_id = 0; - $id_type = ''; - if ( ! empty( $args['id'] ) ) { - $id_components = Relay::fromGlobalId( $args['id'] ); - if ( empty( $id_components['id'] ) || empty( $id_components['type'] ) ) { - throw new UserError( __( 'The "id" is invalid', 'wp-graphql-woocommerce' ) ); - } - $product_id = absint( $id_components['id'] ); - $id_type = 'ID'; - } elseif ( ! empty( $args['productId'] ) ) { - $product_id = absint( $args['productId'] ); - $id_type = 'product ID'; - } elseif ( ! empty( $args['slug'] ) ) { - $post = get_page_by_path( $args['slug'], OBJECT, 'product' ); - $product_id = ! empty( $post ) ? absint( $post->ID ) : 0; - $id_type = 'slug'; - } elseif ( ! empty( $args['sku'] ) ) { - $product_id = \wc_get_product_id_by_sku( $args['sku'] ); - $id_type = 'sku'; - } - - if ( empty( $product_id ) ) { - /* translators: %1$s: ID type, %2$s: ID value */ - throw new UserError( sprintf( __( 'No product ID was found corresponding to the %1$s: %2$s', 'wp-graphql-woocommerce' ), $id_type, $product_id ) ); - } elseif ( get_post( $product_id )->post_type !== 'product' ) { - /* translators: %1$s: ID type, %2$s: ID value */ - throw new UserError( sprintf( __( 'No product exists with the %1$s: %2$s', 'wp-graphql-woocommerce' ), $id_type, $product_id ) ); - } - - return Factory::resolve_crud_object( $product_id, $context ); - }, - ) - ); } /** diff --git a/includes/type/object/class-cart-type.php b/includes/type/object/class-cart-type.php index 46f02ff3a..fad874628 100644 --- a/includes/type/object/class-cart-type.php +++ b/includes/type/object/class-cart-type.php @@ -56,8 +56,8 @@ public static function register() { ), ), 'description' => __( 'The cart object', 'wp-graphql-woocommerce' ), - 'resolve' => function( $source, array $args ) { - $item = Factory::resolve_cart_item( $args['key'] ); + 'resolve' => function( $source, array $args, AppContext $context ) { + $item = Factory::resolve_cart_item( $args['key'], $context ); if ( empty( $item ) ) { throw new UserError( __( 'The key input is invalid', 'wp-graphql-woocommerce' ) ); } diff --git a/includes/type/object/class-coupon-type.php b/includes/type/object/class-coupon-type.php index a91f55e68..97b8db6a6 100644 --- a/includes/type/object/class-coupon-type.php +++ b/includes/type/object/class-coupon-type.php @@ -157,69 +157,5 @@ public static function register() { }, ) ); - - /** - * DEPRECATED - * - * Will be removed in v0.5.x. - */ - register_graphql_field( - 'RootQuery', - 'couponBy', - array( - 'type' => 'Coupon', - 'description' => __( 'A coupon object', 'wp-graphql-woocommerce' ), - 'isDeprecated' => true, - 'deprecationReason' => __( - 'This query has been deprecation, and will be removed in v0.5.x. Please use "coupon(id: value, idType: DATABASE_ID|CODE)" instead', - 'wp-graphql-woocommerce' - ), - 'args' => array( - 'id' => array( - 'type' => 'ID', - 'description' => __( 'Get the coupon by its global ID', 'wp-graphql-woocommerce' ), - ), - 'couponId' => array( - 'type' => 'Int', - 'description' => __( 'Get the coupon by its database ID', 'wp-graphql-woocommerce' ), - ), - 'code' => array( - 'type' => 'String', - 'description' => __( 'Get the coupon by its code', 'wp-graphql-woocommerce' ), - ), - ), - 'resolve' => function( $source, array $args, AppContext $context ) { - $coupon_id = null; - if ( ! empty( $args['id'] ) ) { - $id_components = Relay::fromGlobalId( $args['id'] ); - if ( empty( $id_components['id'] ) || empty( $id_components['type'] ) ) { - throw new UserError( __( 'The "id" is invalid', 'wp-graphql-woocommerce' ) ); - } - - $coupon_id = absint( $id_components['id'] ); - $id = $args['id']; - $id_type = 'ID'; - } elseif ( ! empty( $args['couponId'] ) ) { - $coupon_id = absint( $args['couponId'] ); - $id = $args['couponId']; - $id_type = 'DATABASE_ID'; - } elseif ( ! empty( $args['code'] ) ) { - $coupon_id = \wc_get_coupon_id_by_code( $args['code'] ); - $id = $args['code']; - $id_type = 'CODE'; - } - - if ( empty( $coupon_id ) ) { - /* translators: %1$s: ID type, %2$s: ID value */ - throw new UserError( sprintf( __( 'No coupon ID was found corresponding to the %1$s: %2$s', 'wp-graphql-woocommerce' ), $id_type, $id ) ); - } elseif ( get_post( $coupon_id )->post_type !== 'shop_coupon' ) { - /* translators: %1$s: ID type, %2$s: ID value */ - throw new UserError( sprintf( __( 'No coupon exists with the %1$s: %2$s', 'wp-graphql-woocommerce' ), $id_type, $id ) ); - } - - return Factory::resolve_crud_object( $coupon_id, $context ); - }, - ) - ); } } diff --git a/includes/type/object/class-order-item-type.php b/includes/type/object/class-order-item-type.php index 601581668..28e41cb73 100644 --- a/includes/type/object/class-order-item-type.php +++ b/includes/type/object/class-order-item-type.php @@ -146,8 +146,8 @@ public static function register() { 'taxRate' => array( 'type' => 'TaxRate', 'description' => __( 'Tax line\'s tax rate', 'wp-graphql-woocommerce' ), - 'resolve' => function( $source ) { - return Factory::resolve_tax_rate( $source->rate_id ); + 'resolve' => function( $source, array $args, AppContext $context ) { + return Factory::resolve_tax_rate( $source->rate_id, $context ); }, ), ), diff --git a/includes/type/object/class-tax-rate-type.php b/includes/type/object/class-tax-rate-type.php index d4b5fa6e9..bf1e35779 100644 --- a/includes/type/object/class-tax-rate-type.php +++ b/includes/type/object/class-tax-rate-type.php @@ -12,6 +12,7 @@ use GraphQL\Error\UserError; use GraphQLRelay\Relay; +use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Data\Factory; /** @@ -110,7 +111,7 @@ public static function register() { ), ), ), - 'resolve' => function ( $source, array $args ) { + 'resolve' => function ( $source, array $args, AppContext $context ) { $id = isset( $args['id'] ) ? $args['id'] : null; $id_type = isset( $args['idType'] ) ? $args['idType'] : 'global_id'; @@ -139,7 +140,7 @@ public static function register() { break; } - return Factory::resolve_tax_rate( $rate_id ); + return Factory::resolve_tax_rate( $rate_id, $context ); }, ) ); diff --git a/tests/_support/Helper/crud-helpers/product.php b/tests/_support/Helper/crud-helpers/product.php index e93a8b1ff..3ddbdca79 100644 --- a/tests/_support/Helper/crud-helpers/product.php +++ b/tests/_support/Helper/crud-helpers/product.php @@ -493,7 +493,7 @@ public function print_review_edges( $ids ) { $reviews = array(); foreach ( $ids as $review_id ) { $reviews[] = array( - 'rating' => get_comment_meta( $review_id, 'rating', true ), + 'rating' => floatval( get_comment_meta( $review_id, 'rating', true ) ), 'node' => array( 'id' => Relay::toGlobalId( 'comment', $review_id ) ), diff --git a/tests/wpunit/CartQueriesTest.php b/tests/wpunit/CartQueriesTest.php index fc7562c39..c30d2b1ff 100644 --- a/tests/wpunit/CartQueriesTest.php +++ b/tests/wpunit/CartQueriesTest.php @@ -30,6 +30,10 @@ public function tearDown() { parent::tearDown(); } + private function key_to_cursor( $key ) { + return base64_encode( "CI:{$key}" ); + } + // tests public function testCartQuery() { $cart = WC()->cart; @@ -321,7 +325,7 @@ public function testCartItemPagination() { node { key } - } + } } } } @@ -348,7 +352,7 @@ public function testCartItemPagination() { 'edges' => array_map( function( $item ) { return array( - 'cursor' => $item, + 'cursor' => $this->key_to_cursor( $item ), 'node' => array( 'key' => $item ), ); }, @@ -369,7 +373,10 @@ function( $item ) { * * Tests "after" parameter. */ - $variables = array( 'first' => 2, 'after' => $cart_items[1] ); + $variables = array( + 'first' => 2, + 'after' => $this->key_to_cursor( $cart_items[1] ) + ); $actual = graphql( array( 'query' => $query, @@ -385,7 +392,7 @@ function( $item ) { 'edges' => array_map( function( $item ) { return array( - 'cursor' => $item, + 'cursor' => $this->key_to_cursor( $item ), 'node' => array( 'key' => $item ), ); }, @@ -422,7 +429,7 @@ function( $item ) { 'edges' => array_map( function( $item ) { return array( - 'cursor' => $item, + 'cursor' => $this->key_to_cursor( $item ), 'node' => array( 'key' => $item ), ); }, @@ -459,7 +466,7 @@ function( $item ) { 'edges' => array_map( function( $item ) { return array( - 'cursor' => $item, + 'cursor' => $this->key_to_cursor( $item ), 'node' => array( 'key' => $item ), ); }, diff --git a/tests/wpunit/OrderMutationsTest.php b/tests/wpunit/OrderMutationsTest.php index aaef243e1..6e89c5b99 100644 --- a/tests/wpunit/OrderMutationsTest.php +++ b/tests/wpunit/OrderMutationsTest.php @@ -884,7 +884,7 @@ public function testDeleteOrderMutation() { codecept_debug( $initial_response ); // Clear loader cache. - $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_post_crud' ); + $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_cpt' ); // Retrieve order and items $order_id = $initial_response['data']['createOrder']['order']['orderId']; @@ -1035,7 +1035,7 @@ public function testDeleteOrderItemsMutation() { codecept_debug( $initial_response ); // Clear loader cache. - $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_post_crud' ); + $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_cpt' ); // Retrieve order and items $order_id = $initial_response['data']['createOrder']['order']['orderId']; diff --git a/tests/wpunit/OrderQueriesTest.php b/tests/wpunit/OrderQueriesTest.php index 9b22f3439..210e8e5ad 100644 --- a/tests/wpunit/OrderQueriesTest.php +++ b/tests/wpunit/OrderQueriesTest.php @@ -135,7 +135,7 @@ public function testOrderQuery() { $this->assertEquals( $expected, $actual ); // Clear loader cache. - $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_post_crud' ); + $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_cpt' ); /** * Assertion Two diff --git a/tests/wpunit/ProductQueriesTest.php b/tests/wpunit/ProductQueriesTest.php index a9e19def8..71b04f861 100644 --- a/tests/wpunit/ProductQueriesTest.php +++ b/tests/wpunit/ProductQueriesTest.php @@ -131,7 +131,7 @@ public function testSimpleProductQuery() { $this->assertEquals( $expected, $actual ); // Clear cache - $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_post_crud' ); + $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_cpt' ); /** * Assertion Two @@ -182,8 +182,8 @@ public function testProductTaxonomies() { ); $query = ' - query ( $id: ID, $productId: Int, $slug: String, $sku: String ) { - productBy( id: $id, productId: $productId, slug: $slug, sku: $sku ) { + query ( $id: ID!, $idType: ProductIdTypeEnum ) { + product( id: $id, idType: $idType ) { ... on SimpleProduct { id image { @@ -214,11 +214,14 @@ public function testProductTaxonomies() { * * Test querying product with "productId" argument. */ - $variables = array( 'productId' => $product ); + $variables = array( + 'id' => $product, + 'idType' => 'DATABASE_ID' + ); $actual = graphql( array( 'query' => $query, 'variables' => $variables ) ); $expected = array( 'data' => array( - 'productBy' => array( + 'product' => array( 'id' => $this->helper->to_relay_id( $product ), 'image' => array( 'id' => \GraphQLRelay\Relay::toGlobalId( 'attachment', $attachment_id ), @@ -1236,7 +1239,7 @@ public function testProductToReviewConnections() { 'product' => array( 'id' => $this->helper->to_relay_id( $this->product ), 'reviews' => array( - 'averageRating' => $product->get_average_rating(), + 'averageRating' => floatval( $product->get_average_rating() ), 'edges' => $this->helper->print_review_edges( $reviews ), ), ), diff --git a/tests/wpunit/ProductVariationQueriesTest.php b/tests/wpunit/ProductVariationQueriesTest.php index 8bef25c54..10244bbbf 100644 --- a/tests/wpunit/ProductVariationQueriesTest.php +++ b/tests/wpunit/ProductVariationQueriesTest.php @@ -96,7 +96,7 @@ public function testVariationQuery() { $this->assertEquals( $expected, $actual ); - $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_post_crud' ); + $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_cpt' ); /** * Assertion Two diff --git a/tests/wpunit/RefundQueriesTest.php b/tests/wpunit/RefundQueriesTest.php index cc575f7dc..45a08bfc8 100644 --- a/tests/wpunit/RefundQueriesTest.php +++ b/tests/wpunit/RefundQueriesTest.php @@ -73,7 +73,7 @@ public function testRefundQuery() { $this->assertEquals( $expected, $actual ); // Clear customer cache. - $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_post_crud' ); + $this->getModule('\Helper\Wpunit')->clear_loader_cache( 'wc_cpt' ); /** * Assertion Two diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 6730abb9a..7c6127c36 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -43,11 +43,13 @@ 'WPGraphQL\\WooCommerce\\Data\\Connection\\Shipping_Method_Connection_Resolver' => $baseDir . '/includes/data/connection/class-shipping-method-connection-resolver.php', 'WPGraphQL\\WooCommerce\\Data\\Connection\\Tax_Rate_Connection_Resolver' => $baseDir . '/includes/data/connection/class-tax-rate-connection-resolver.php', 'WPGraphQL\\WooCommerce\\Data\\Connection\\Variation_Attribute_Connection_Resolver' => $baseDir . '/includes/data/connection/class-variation-attribute-connection-resolver.php', - 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_Connection_Functions' => $baseDir . '/includes/data/connection/trait-wc-connection-functions.php', + 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_CPT_Loader_Common' => $baseDir . '/includes/data/connection/trait-wc-cpt-loader-common.php', + 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_Db_Loader_Common' => $baseDir . '/includes/data/connection/trait-wc-db-loader-common.php', 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_Term_Connection_Resolver' => $baseDir . '/includes/data/connection/class-wc-term-connection-resolver.php', 'WPGraphQL\\WooCommerce\\Data\\Factory' => $baseDir . '/includes/data/class-factory.php', + 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_CPT_Loader' => $baseDir . '/includes/data/loader/class-wc-cpt-loader.php', 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_Customer_Loader' => $baseDir . '/includes/data/loader/class-wc-customer-loader.php', - 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_Post_Crud_Loader' => $baseDir . '/includes/data/loader/class-wc-post-crud-loader.php', + 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_Db_Loader' => $baseDir . '/includes/data/loader/class-wc-db-loader.php', 'WPGraphQL\\WooCommerce\\Data\\Mutation\\Cart_Mutation' => $baseDir . '/includes/data/mutation/class-cart-mutation.php', 'WPGraphQL\\WooCommerce\\Data\\Mutation\\Checkout_Mutation' => $baseDir . '/includes/data/mutation/class-checkout-mutation.php', 'WPGraphQL\\WooCommerce\\Data\\Mutation\\Customer_Mutation' => $baseDir . '/includes/data/mutation/class-customer-mutation.php', diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index aef00ef4c..c7c1484ae 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -6,6 +6,6 @@ $baseDir = dirname($vendorDir); return array( - '914b07b8cf678ed0b81bfdb5d23b4f2b' => $baseDir . '/includes/connection/common-post-type-args.php', + '944484f100dc1864a5320474d49ebd5a' => $baseDir . '/includes/connection/wc-cpt-connection-args.php', '45a15019e901000ab8608c03ebff44fb' => $baseDir . '/includes/functions.php', ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index d0583a046..154a75e28 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -7,7 +7,7 @@ class ComposerStaticInit605a02cd7bfd584c6ac9e4f2b4d51817 { public static $files = array ( - '914b07b8cf678ed0b81bfdb5d23b4f2b' => __DIR__ . '/../..' . '/includes/connection/common-post-type-args.php', + '944484f100dc1864a5320474d49ebd5a' => __DIR__ . '/../..' . '/includes/connection/wc-cpt-connection-args.php', '45a15019e901000ab8608c03ebff44fb' => __DIR__ . '/../..' . '/includes/functions.php', ); @@ -71,11 +71,13 @@ class ComposerStaticInit605a02cd7bfd584c6ac9e4f2b4d51817 'WPGraphQL\\WooCommerce\\Data\\Connection\\Shipping_Method_Connection_Resolver' => __DIR__ . '/../..' . '/includes/data/connection/class-shipping-method-connection-resolver.php', 'WPGraphQL\\WooCommerce\\Data\\Connection\\Tax_Rate_Connection_Resolver' => __DIR__ . '/../..' . '/includes/data/connection/class-tax-rate-connection-resolver.php', 'WPGraphQL\\WooCommerce\\Data\\Connection\\Variation_Attribute_Connection_Resolver' => __DIR__ . '/../..' . '/includes/data/connection/class-variation-attribute-connection-resolver.php', - 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_Connection_Functions' => __DIR__ . '/../..' . '/includes/data/connection/trait-wc-connection-functions.php', + 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_CPT_Loader_Common' => __DIR__ . '/../..' . '/includes/data/connection/trait-wc-cpt-loader-common.php', + 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_Db_Loader_Common' => __DIR__ . '/../..' . '/includes/data/connection/trait-wc-db-loader-common.php', 'WPGraphQL\\WooCommerce\\Data\\Connection\\WC_Term_Connection_Resolver' => __DIR__ . '/../..' . '/includes/data/connection/class-wc-term-connection-resolver.php', 'WPGraphQL\\WooCommerce\\Data\\Factory' => __DIR__ . '/../..' . '/includes/data/class-factory.php', + 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_CPT_Loader' => __DIR__ . '/../..' . '/includes/data/loader/class-wc-cpt-loader.php', 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_Customer_Loader' => __DIR__ . '/../..' . '/includes/data/loader/class-wc-customer-loader.php', - 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_Post_Crud_Loader' => __DIR__ . '/../..' . '/includes/data/loader/class-wc-post-crud-loader.php', + 'WPGraphQL\\WooCommerce\\Data\\Loader\\WC_Db_Loader' => __DIR__ . '/../..' . '/includes/data/loader/class-wc-db-loader.php', 'WPGraphQL\\WooCommerce\\Data\\Mutation\\Cart_Mutation' => __DIR__ . '/../..' . '/includes/data/mutation/class-cart-mutation.php', 'WPGraphQL\\WooCommerce\\Data\\Mutation\\Checkout_Mutation' => __DIR__ . '/../..' . '/includes/data/mutation/class-checkout-mutation.php', 'WPGraphQL\\WooCommerce\\Data\\Mutation\\Customer_Mutation' => __DIR__ . '/../..' . '/includes/data/mutation/class-customer-mutation.php', diff --git a/wp-graphql-woocommerce.php b/wp-graphql-woocommerce.php index 4becbde19..16887ab84 100644 --- a/wp-graphql-woocommerce.php +++ b/wp-graphql-woocommerce.php @@ -3,7 +3,7 @@ * Plugin Name: WPGraphQL WooCommerce (WooGraphQL) * Plugin URI: https://github.com/kidunot89/wp-graphql-woocommerce * Description: Adds Woocommerce Functionality to WPGraphQL schema. - * Version: 0.4.4 + * Version: 0.5.0 * Author: kidunot89 * Author URI: https://axistaylor.com * Text Domain: wp-graphql-woocommerce @@ -11,9 +11,9 @@ * License: GPL-3 * License URI: https://www.gnu.org/licenses/gpl-3.0.html * WC requires at least: 3.0.0 - * WC tested up to: 3.7.1 - * WPGraphQL requires at least: 0.6.0+ - * WPGraphQL-JWT-Authentication requires at least: 0.3.4+ + * WC tested up to: 4.0.0 + * WPGraphQL requires at least: 0.8.0+ + * WPGraphQL-JWT-Authentication requires at least: 0.4.0+ * * @package WPGraphQL\WooCommerce * @author kidunot89