From 8937697f5f6c3a74acf68ff20c21d70ffa0752a9 Mon Sep 17 00:00:00 2001 From: Facundo Capua Date: Mon, 21 May 2018 16:18:21 -0300 Subject: [PATCH 1/7] Include 'products' in category query --- .../Model/Resolver/Category/Products.php | 106 ++++++++++++ .../CatalogGraphQl/etc/schema.graphqls | 11 ++ .../Magento/GraphQl/Catalog/CategoryTest.php | 160 ++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php new file mode 100644 index 0000000000000..4575f06c4fc83 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -0,0 +1,106 @@ +productRepository = $productRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterQuery = $filterQuery; + $this->valueFactory = $valueFactory; + } + + /** + * {@inheritdoc} + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): Value { + $args['filter'] = [ + 'category_ids' => [ + 'eq' => $value['id'] + ] + ]; + $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); + $searchCriteria->setCurrentPage($args['currentPage']); + $searchCriteria->setPageSize($args['pageSize']); + $searchResult = $this->filterQuery->getResult($searchCriteria, $info); + + //possible division by 0 + if ($searchCriteria->getPageSize()) { + $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); + } else { + $maxPages = 0; + } + + $currentPage = $searchCriteria->getCurrentPage(); + if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { + $currentPage = new GraphQlInputException( + __( + 'currentPage value %1 specified is greater than the number of pages available.', + [$maxPages] + ) + ); + } + + $data = [ + 'total_count' => $searchResult->getTotalCount(), + 'items' => $searchResult->getProductsSearchResult(), + 'page_info' => [ + 'page_size' => $searchCriteria->getPageSize(), + 'current_page' => $currentPage + ] + ]; + + $result = function () use ($data) { + return $data; + }; + + return $this->valueFactory->create($result); + } + +} \ No newline at end of file diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index ca1ff78654319..64e56a738bbf6 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -375,6 +375,11 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model updated_at: String @doc(description: "Timestamp indicating when the category was updated") product_count: Int @doc(description: "The number of products in the category") default_sort_by: String @doc(description: "The attribute to use for sorting") + products( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + ): CategoryProducts @doc(description: "The list of products assigned to the category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") } type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option") { @@ -404,6 +409,12 @@ type Products @doc(description: "The Products object is the top-level object ret filters: [LayerFilter] @doc(description: "Layered navigation filters array") } +type CategoryProducts @doc(description: "The category products object returned in the Category query") { + items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria") + page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") + total_count: Int @doc(description: "The number of products returned") +} + input ProductFilterInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer") diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index eb138d738ea10..78a7aa8bed63a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -9,6 +9,7 @@ use Magento\Framework\DataObject; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\ProductRepositoryInterface; class CategoryTest extends GraphQlAbstract { @@ -109,4 +110,163 @@ public function testCategoriesTree() $responseDataObject->getData('category/children/7/children/1/children/0/id') ); } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCategoryProducts() + { + $categoryId = 4; + $query = <<graphQlQuery($query); + $this->assertArrayHasKey('products', $response['category']); + $this->assertArrayHasKey('total_count', $response['category']['products']); + $this->assertEquals(2, $response['category']['products']['total_count']); + $this->assertEquals(1, $response['category']['products']['page_info']['current_page']); + $this->assertEquals(20, $response['category']['products']['page_info']['page_size']); + } } From 8753c5b24dc3fe5edd49ca9113a282f7120baad7 Mon Sep 17 00:00:00 2001 From: Facundo Capua Date: Mon, 21 May 2018 16:33:29 -0300 Subject: [PATCH 2/7] Fix code style issues --- .../CatalogGraphQl/Model/Resolver/Category/Products.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index 4575f06c4fc83..baa8f0c8bc5d3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -17,8 +17,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Filter; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -class Products - implements ResolverInterface +class Products implements ResolverInterface { /** @var \Magento\Catalog\Api\ProductRepositoryInterface */ private $productRepository; @@ -102,5 +101,4 @@ public function resolve( return $this->valueFactory->create($result); } - -} \ No newline at end of file +} From 00b22e77d0dc79c7599bee4c1af5cad71546bcb5 Mon Sep 17 00:00:00 2001 From: Facundo Capua Date: Tue, 22 May 2018 20:58:28 -0300 Subject: [PATCH 3/7] Added some documentation to the class --- .../CatalogGraphQl/Model/Resolver/Category/Products.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index baa8f0c8bc5d3..5927e747c2238 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -17,6 +17,9 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Filter; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +/** + * Category products resolver, used by GraphQL endpoints to retrieve products assigned to a category + */ class Products implements ResolverInterface { /** @var \Magento\Catalog\Api\ProductRepositoryInterface */ From 9075259456d8434820babaafa60655121c180336 Mon Sep 17 00:00:00 2001 From: Facundo Capua Date: Tue, 22 May 2018 20:59:11 -0300 Subject: [PATCH 4/7] Adjust documentation to describe the actual functionality --- app/code/Magento/CatalogGraphQl/etc/schema.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 64e56a738bbf6..f9e43959c79d9 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -410,7 +410,7 @@ type Products @doc(description: "The Products object is the top-level object ret } type CategoryProducts @doc(description: "The category products object returned in the Category query") { - items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria") + items: [ProductInterface] @doc(description: "An array of products that are assigned to the category") page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") total_count: Int @doc(description: "The number of products returned") } From 4224794b9a046b0bb80f5566a1eda689dee01956 Mon Sep 17 00:00:00 2001 From: Facundo Capua Date: Tue, 22 May 2018 20:59:35 -0300 Subject: [PATCH 5/7] Added assertions to the product information returned in the tests --- .../Magento/GraphQl/Catalog/CategoryTest.php | 147 +++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 78a7aa8bed63a..f6121bea2d830 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -9,7 +9,9 @@ use Magento\Framework\DataObject; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; class CategoryTest extends GraphQlAbstract { @@ -172,7 +174,6 @@ public function testCategoryProducts() new_from_date new_to_date options_container - price { minimalPrice { amount { @@ -268,5 +269,149 @@ public function testCategoryProducts() $this->assertEquals(2, $response['category']['products']['total_count']); $this->assertEquals(1, $response['category']['products']['page_info']['current_page']); $this->assertEquals(20, $response['category']['products']['page_info']['page_size']); + + /** + * @var ProductRepositoryInterface $productRepository + */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $firstProductSku = 'simple'; + $firstProduct = $productRepository->get($firstProductSku, false, null, true); + $this->assertBaseFields($firstProduct, $response['category']['products']['items'][0]); + $this->assertAttributes($response['category']['products']['items'][0]); + $this->assertWebsites($firstProduct, $response['category']['products']['items'][0]['websites']); + + $secondProductSku = '12345'; + $secondProduct = $productRepository->get($secondProductSku, false, null, true); + $this->assertBaseFields($secondProduct, $response['category']['products']['items'][1]); + $this->assertAttributes($response['category']['products']['items'][1]); + $this->assertWebsites($secondProduct, $response['category']['products']['items'][1]['websites']); + } + + /** + * @param ProductInterface $product + * @param array $actualResponse + */ + private function assertBaseFields($product, $actualResponse) + { + + $assertionMap = [ + ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], + ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], + ['response_field' => 'id', 'expected_value' => $product->getId()], + ['response_field' => 'name', 'expected_value' => $product->getName()], + ['response_field' => 'price', 'expected_value' => + [ + 'minimalPrice' => [ + 'amount' => [ + 'value' => $product->getPrice(), + 'currency' => 'USD' + ], + 'adjustments' => [] + ], + 'regularPrice' => [ + 'amount' => [ + 'value' => $product->getPrice(), + 'currency' => 'USD' + ], + 'adjustments' => [] + ], + 'maximalPrice' => [ + 'amount' => [ + 'value' => $product->getPrice(), + 'currency' => 'USD' + ], + 'adjustments' => [] + ], + ] + ], + ['response_field' => 'sku', 'expected_value' => $product->getSku()], + ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], + ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], +// ['response_field' => 'weight', 'expected_value' => $product->getWeight()], + ]; + + $this->assertResponseFields($actualResponse, $assertionMap); + } + + /** + * @param ProductInterface $product + * @param array $actualResponse + */ + private function assertWebsites($product, $actualResponse) + { + $assertionMap = [ + [ + 'id' => current($product->getExtensionAttributes()->getWebsiteIds()), + 'name' => 'Main Website', + 'code' => 'base', + 'sort_order' => 0, + 'default_group_id' => '1', + 'is_default' => true, + ] + ]; + + $this->assertEquals($actualResponse, $assertionMap); + } + + /** + * @param array $actualResponse + */ + private function assertAttributes($actualResponse) + { + $eavAttributes = [ + 'url_key', + 'description', + 'meta_description', + 'meta_keyword', + 'meta_title', + 'short_description', + 'tax_class_id', + 'country_of_manufacture', + 'gift_message_available', + 'new_from_date', + 'new_to_date', + 'options_container', + 'special_price', + 'special_from_date', + 'special_to_date', + ]; + + foreach($eavAttributes as $eavAttribute){ + $this->assertArrayHasKey($eavAttribute, $actualResponse); + } + } + + /** + * @param array $actualResponse + * @param array $assertionMap ['response_field_name' => 'response_field_value', ...] + * OR [['response_field' => $field, 'expected_value' => $value], ...] + */ + private function assertResponseFields($actualResponse, $assertionMap) + { + foreach ($assertionMap as $key => $assertionData) { + $expectedValue = isset($assertionData['expected_value']) + ? $assertionData['expected_value'] + : $assertionData; + $responseField = isset($assertionData['response_field']) ? $assertionData['response_field'] : $key; + self::assertNotNull( + $expectedValue, + "Value of '{$responseField}' field must not be NULL" + ); + self::assertEquals( + $expectedValue, + $actualResponse[$responseField], + "Value of '{$responseField}' field in response does not match expected value: " + . var_export($expectedValue, true) + ); + } + } + + private function eavAttributesToGraphQlSchemaFieldTranslator($attributeCode) + { + if(isset($this->eavAttributesToGraphQlSchemaFieldMap[$attributeCode])){ + return $this->eavAttributesToGraphQlSchemaFieldMap[$attributeCode]; + } + + return $attributeCode; } } From 5f405b438e6a12fa1331946bf204c2112f6f708b Mon Sep 17 00:00:00 2001 From: Facundo Capua Date: Wed, 23 May 2018 09:19:00 -0300 Subject: [PATCH 6/7] Fix code styles issues --- .../testsuite/Magento/GraphQl/Catalog/CategoryTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index f6121bea2d830..033b48b5ab9b5 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -376,7 +376,7 @@ private function assertAttributes($actualResponse) 'special_to_date', ]; - foreach($eavAttributes as $eavAttribute){ + foreach ($eavAttributes as $eavAttribute) { $this->assertArrayHasKey($eavAttribute, $actualResponse); } } @@ -408,7 +408,7 @@ private function assertResponseFields($actualResponse, $assertionMap) private function eavAttributesToGraphQlSchemaFieldTranslator($attributeCode) { - if(isset($this->eavAttributesToGraphQlSchemaFieldMap[$attributeCode])){ + if (isset($this->eavAttributesToGraphQlSchemaFieldMap[$attributeCode])) { return $this->eavAttributesToGraphQlSchemaFieldMap[$attributeCode]; } From a9e5d85fcc2b691247a768bac2c2d2d2b01dfb3e Mon Sep 17 00:00:00 2001 From: Facundo Capua Date: Wed, 23 May 2018 14:08:42 -0300 Subject: [PATCH 7/7] Removed unused and commented code --- .../testsuite/Magento/GraphQl/Catalog/CategoryTest.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 033b48b5ab9b5..8fa731fb0d7f2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -327,7 +327,6 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], -// ['response_field' => 'weight', 'expected_value' => $product->getWeight()], ]; $this->assertResponseFields($actualResponse, $assertionMap); @@ -405,13 +404,4 @@ private function assertResponseFields($actualResponse, $assertionMap) ); } } - - private function eavAttributesToGraphQlSchemaFieldTranslator($attributeCode) - { - if (isset($this->eavAttributesToGraphQlSchemaFieldMap[$attributeCode])) { - return $this->eavAttributesToGraphQlSchemaFieldMap[$attributeCode]; - } - - return $attributeCode; - } }