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 00000000000..5927e747c22 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -0,0 +1,107 @@ +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); + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 747d1446ad0..5d9e174f169 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -376,6 +376,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") { @@ -406,6 +411,12 @@ type Products @doc(description: "The Products object is the top-level object ret sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } +type CategoryProducts @doc(description: "The category products object returned in the Category query") { + 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") +} + 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 eb138d738ea..8fa731fb0d7 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,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 { @@ -109,4 +112,296 @@ 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']); + + /** + * @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()], + ]; + + $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) + ); + } + } }