From d57303dcbe259d3ec952c9214d70adfc4614bac9 Mon Sep 17 00:00:00 2001 From: Bruno Guilera Gutchenzo Date: Fri, 14 Feb 2025 10:26:14 -0300 Subject: [PATCH] feat: rating resolvers (#2648) ## What's the purpose of this pull request? To add product rating resolvers on graphQL ## How it works? It defines a new type called: `StoreProductRating` and increments the query resolver for `product` and for `searchResult` It was necessary to adapt the `EnhancedSku` type and also to adds a rating callback to the searchResult's promise resolution ## How to test it? run the api graphql server locally with the following command: ```bash yarn dev:server ``` and make a query call ## References [JIRA Task: SFS-2093](https://vtex-dev.atlassian.net/browse/SFS-2093) ## Checklist You may erase this after checking them all :wink: **PR Description** - [ ] Adds graphQL Rating type - [ ] Increments `product` resolver - [ ] Increments `searchResult` resolver --- packages/api/mocks/ProductQuery.ts | 10 ++++ packages/api/src/__generated__/schema.ts | 10 ++++ .../src/platforms/vtex/resolvers/product.ts | 1 + .../api/src/platforms/vtex/resolvers/query.ts | 12 ++++- .../platforms/vtex/resolvers/searchResult.ts | 47 ++++++++++++------- .../src/platforms/vtex/utils/enhanceSku.ts | 8 +++- packages/api/src/typeDefs/product.graphql | 4 ++ .../api/src/typeDefs/productRating.graphql | 10 ++++ packages/api/test/queries.test.ts | 21 +++++++-- 9 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 packages/api/src/typeDefs/productRating.graphql diff --git a/packages/api/mocks/ProductQuery.ts b/packages/api/mocks/ProductQuery.ts index e4e01377b1..871dd09bbb 100644 --- a/packages/api/mocks/ProductQuery.ts +++ b/packages/api/mocks/ProductQuery.ts @@ -257,3 +257,13 @@ export const productSearchFetch = { }, }, } + +export const productRatingFetch = (productId: string) => ({ + info: `https://storeframework.vtexcommercestable.com.br/api/io/reviews-and-ratings/api/rating/${productId}`, + init: undefined, + options: { storeCookies: expect.any(Function) }, + result: { + average: 4.5, + totalCount: 20, + }, +}) diff --git a/packages/api/src/__generated__/schema.ts b/packages/api/src/__generated__/schema.ts index a35cd716ef..2762e48e15 100644 --- a/packages/api/src/__generated__/schema.ts +++ b/packages/api/src/__generated__/schema.ts @@ -1030,6 +1030,8 @@ export type StoreProduct = { offers: StoreAggregateOffer; /** Product ID, such as [ISBN](https://www.isbn-international.org/content/what-isbn) or similar global IDs. */ productID: Scalars['String']; + /** Product rating. */ + rating: StoreProductRating; /** The product's release date. Formatted using https://en.wikipedia.org/wiki/ISO_8601 */ releaseDate: Scalars['String']; /** Array with review information. */ @@ -1116,6 +1118,14 @@ export const enum StoreProductListReviewsSort { ReviewDateTimeDesc = 'reviewDateTime_desc' }; +export type StoreProductRating = { + __typename?: 'StoreProductRating'; + /** Product average rating. */ + average: Scalars['Float']; + /** Product amount of ratings received. */ + totalCount: Scalars['Int']; +}; + export type StoreProductReview = { __typename?: 'StoreProductReview'; /** Indicates if the review was approved by the store owner. */ diff --git a/packages/api/src/platforms/vtex/resolvers/product.ts b/packages/api/src/platforms/vtex/resolvers/product.ts index 6998c28a3d..67ce03e4fb 100644 --- a/packages/api/src/platforms/vtex/resolvers/product.ts +++ b/packages/api/src/platforms/vtex/resolvers/product.ts @@ -157,4 +157,5 @@ export const StoreProduct: Record> & { }, releaseDate: ({ isVariantOf: { releaseDate } }) => releaseDate ?? '', advertisement: ({ isVariantOf: { advertisement } }) => advertisement, + rating: (item) => item.rating, } diff --git a/packages/api/src/platforms/vtex/resolvers/query.ts b/packages/api/src/platforms/vtex/resolvers/query.ts index 9a77d6e037..38f1fd12a5 100644 --- a/packages/api/src/platforms/vtex/resolvers/query.ts +++ b/packages/api/src/platforms/vtex/resolvers/query.ts @@ -79,6 +79,10 @@ export const Query = { ) } + const rating = await commerce.rating(sku.itemId) + + sku.rating = rating + return sku } catch (err) { if (slug == null) { @@ -103,9 +107,15 @@ export const Query = { throw new NotFoundError(`No product found for id ${route.id}`) } + const rating = await commerce.rating(product.productId) + const sku = pickBestSku(product.items) - return enhanceSku(sku, product) + const enhancedSku = enhanceSku(sku, product) + + enhancedSku.rating = rating + + return enhancedSku } }, collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => { diff --git a/packages/api/src/platforms/vtex/resolvers/searchResult.ts b/packages/api/src/platforms/vtex/resolvers/searchResult.ts index e51a9af8cd..75bd533b9a 100644 --- a/packages/api/src/platforms/vtex/resolvers/searchResult.ts +++ b/packages/api/src/platforms/vtex/resolvers/searchResult.ts @@ -20,10 +20,10 @@ const isRootFacet = (facet: Facet, isDepartment: boolean, isBrand: boolean) => export const StoreSearchResult: Record> = { suggestions: async (root, _, ctx) => { const { - clients: { search }, + clients: { search, commerce }, } = ctx - const { searchArgs } = root + const { searchArgs, productSearchPromise } = root // If there's no search query, suggest the most popular searches. if (!searchArgs.query) { @@ -38,21 +38,26 @@ export const StoreSearchResult: Record> = { } } - const { productSearchPromise } = root const [terms, productSearchResult] = await Promise.all([ search.suggestedTerms(searchArgs), productSearchPromise, ]) - const skus = productSearchResult.products - .map((product) => { - // What determines the presentation of the SKU is the price order - // https://help.vtex.com/pt/tutorial/ordenando-imagens-na-vitrine-e-na-pagina-de-produto--tutorials_278 - const maybeSku = pickBestSku(product.items) - - return maybeSku && enhanceSku(maybeSku, product) - }) - .filter((sku) => !!sku) + const skus = await Promise.all( + productSearchResult.products + .map((product) => { + // What determines the presentation of the SKU is the price order + // https://help.vtex.com/pt/tutorial/ordenando-imagens-na-vitrine-e-na-pagina-de-produto--tutorials_278 + const maybeSku = pickBestSku(product.items) + + return maybeSku && enhanceSku(maybeSku, product) + }) + .filter((sku) => !!sku) + .map(async (sku) => ({ + ...sku, + rating: await commerce.rating(sku.itemId), + })) + ) const { searches } = terms @@ -61,7 +66,11 @@ export const StoreSearchResult: Record> = { products: skus, } }, - products: async ({ productSearchPromise }) => { + products: async ({ productSearchPromise }, _, ctx) => { + const { + clients: { commerce }, + } = ctx + const productSearchResult = await productSearchPromise const skus = productSearchResult.products @@ -74,6 +83,13 @@ export const StoreSearchResult: Record> = { }) .filter((sku) => !!sku) + const edges = await Promise.all( + skus.map(async (sku, index) => ({ + node: { ...sku, rating: await commerce.rating(sku.itemId) }, + cursor: index.toString(), + })) + ) + return { pageInfo: { hasNextPage: productSearchResult.pagination.after.length > 0, @@ -82,10 +98,7 @@ export const StoreSearchResult: Record> = { endCursor: productSearchResult.recordsFiltered.toString(), totalCount: productSearchResult.recordsFiltered, }, - edges: skus.map((sku, index) => ({ - node: sku, - cursor: index.toString(), - })), + edges, } }, facets: async ({ searchArgs }, _, ctx) => { diff --git a/packages/api/src/platforms/vtex/utils/enhanceSku.ts b/packages/api/src/platforms/vtex/utils/enhanceSku.ts index 01cfa928bc..4468b3093a 100644 --- a/packages/api/src/platforms/vtex/utils/enhanceSku.ts +++ b/packages/api/src/platforms/vtex/utils/enhanceSku.ts @@ -1,7 +1,9 @@ import type { Product, Item } from '../clients/search/types/ProductSearchResult' import { sanitizeHtml } from './sanitizeHtml' -export type EnhancedSku = Item & { isVariantOf: Product } +export type EnhancedSku = Item & { isVariantOf: Product } & { + rating: { average: number; totalCount: number } +} function sanitizeProduct(product: Product): Product { return { @@ -14,5 +16,9 @@ function sanitizeProduct(product: Product): Product { export const enhanceSku = (item: Item, product: Product): EnhancedSku => ({ ...item, + rating: { + average: 0, + totalCount: 0, + }, isVariantOf: sanitizeProduct(product), }) diff --git a/packages/api/src/typeDefs/product.graphql b/packages/api/src/typeDefs/product.graphql index b1a7dfcb50..1efcf43cf9 100644 --- a/packages/api/src/typeDefs/product.graphql +++ b/packages/api/src/typeDefs/product.graphql @@ -74,6 +74,10 @@ type StoreProduct { Advertisement information about the product. """ advertisement: Advertisement + """ + Product rating. + """ + rating: StoreProductRating! } """ diff --git a/packages/api/src/typeDefs/productRating.graphql b/packages/api/src/typeDefs/productRating.graphql new file mode 100644 index 0000000000..2b0e1a19ea --- /dev/null +++ b/packages/api/src/typeDefs/productRating.graphql @@ -0,0 +1,10 @@ +type StoreProductRating { + """ + Product average rating. + """ + average: Float! + """ + Product amount of ratings received. + """ + totalCount: Int! +} diff --git a/packages/api/test/queries.test.ts b/packages/api/test/queries.test.ts index ea86829e6d..5e66e6cc1b 100644 --- a/packages/api/test/queries.test.ts +++ b/packages/api/test/queries.test.ts @@ -20,7 +20,11 @@ import { pageTypeOfficeDesksFetch, pageTypeOfficeFetch, } from '../mocks/CollectionQuery' -import { ProductByIdQuery, productSearchFetch } from '../mocks/ProductQuery' +import { + ProductByIdQuery, + productRatingFetch, + productSearchFetch, +} from '../mocks/ProductQuery' import { RedirectQueryTermTech, redirectTermTechFetch, @@ -138,7 +142,11 @@ test('`collection` query', async () => { }) test('`product` query', async () => { - const fetchAPICalls = [productSearchFetch, salesChannelStaleFetch] + const fetchAPICalls = [ + productSearchFetch, + productRatingFetch('64953394'), + salesChannelStaleFetch, + ] mockedFetch.mockImplementation((info, init) => pickFetchAPICallResult(info, init, fetchAPICalls) @@ -146,7 +154,7 @@ test('`product` query', async () => { const response = await run(ProductByIdQuery) - expect(mockedFetch).toHaveBeenCalledTimes(2) + expect(mockedFetch).toHaveBeenCalledTimes(3) fetchAPICalls.forEach((fetchAPICall) => { expect(mockedFetch).toHaveBeenCalledWith( @@ -215,6 +223,11 @@ test('`search` query', async () => { const fetchAPICalls = [ productSearchCategory1Fetch, attributeSearchCategory1Fetch, + productRatingFetch('2791588'), + productRatingFetch('44903104'), + productRatingFetch('96175310'), + productRatingFetch('12405783'), + productRatingFetch('24041857'), salesChannelStaleFetch, ] @@ -224,7 +237,7 @@ test('`search` query', async () => { const response = await run(SearchQueryFirst5Products) - expect(mockedFetch).toHaveBeenCalledTimes(3) + expect(mockedFetch).toHaveBeenCalledTimes(8) fetchAPICalls.forEach((fetchAPICall) => { expect(mockedFetch).toHaveBeenCalledWith(