Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC Reference] Add allVariantProducts property #2451

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/api/src/__generated__/schema.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 87 additions & 0 deletions packages/api/src/platforms/vtex/loaders/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { isValidSkuId, pickBestSku } from '../utils/sku'
import { BadRequestError, NotFoundError } from '../../errors'
import { findChannel, findLocale, findSkuId, findSlug } from '../utils/facets'
import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
import { enhanceSku } from '../utils/enhanceSku'
import { IStoreSelectedFacet } from '../../..'
import { Context } from '..'

export const getProductLoader = async ( locator: IStoreSelectedFacet[], ctx: Context) => {
// Insert channel in context for later usage
const channel = findChannel(locator)
const locale = findLocale(locator)
const id = findSkuId(locator)
const slug = findSlug(locator)

if (channel) {
mutateChannelContext(ctx, channel)
}

if (locale) {
mutateLocaleContext(ctx, locale)
}

const {
loaders: { skuLoader },
clients: { commerce, search },
} = ctx

try {
const skuId = id ?? slug?.split('-').pop() ?? ''

if (!isValidSkuId(skuId)) {
throw new Error('Invalid SkuId')
}

const sku = await skuLoader.load(skuId)

/**
* Here be dragons 🦄🦄🦄
*
* In some cases, the slug has a valid skuId for a different
* product. This condition makes sure that the fetched sku
* is the one we actually asked for
* */
if (
slug &&
sku.isVariantOf.linkText &&
!slug.startsWith(sku.isVariantOf.linkText)
) {
throw new Error(
`Slug was set but the fetched sku does not satisfy the slug condition. slug: ${slug}, linkText: ${sku.isVariantOf.linkText}`
)
}

console.log("SKU", JSON.stringify(sku, null, 2))

return sku
} catch (err) {
if (slug == null) {
throw new BadRequestError('Missing slug or id')
}

const route = await commerce.catalog.portal.pagetype(`${slug}/p`)

if (route.pageType !== 'Product' || !route.id) {
throw new NotFoundError(`No product found for slug ${slug}`)
}

const {
products: [product],
} = await search.products({
page: 0,
count: 1,
query: `product:${route.id}`,
})

console.log("PRODUCT", product)

if (!product) {
throw new NotFoundError(`No product found for id ${route.id}`)
}

const sku = pickBestSku(product.items)

return enhanceSku(sku, product)
}
}
5 changes: 2 additions & 3 deletions packages/api/src/platforms/vtex/resolvers/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@ export const StoreProduct: Record<string, Resolver<Root>> & {

image: Resolver<Root, any, StoreImage[]>
} = {
productID: ({ itemId }) => itemId,
productID: (root) => root.itemId,
name: ({ isVariantOf, name }) => name ?? isVariantOf.productName,
slug: ({ isVariantOf: { linkText }, itemId }) => getSlug(linkText, itemId),
lala: ({ isVariantOf: { linkText }, itemId }) => getSlug(linkText, itemId),
description: ({ isVariantOf: { description } }) => description,
seo: ({ isVariantOf }) => ({
title: isVariantOf.productName,
description: isVariantOf.description,
canonical: canonicalFromProduct(isVariantOf),
}),
brand: ({ isVariantOf: { brand } }) => ({ name: brand }),
Expand Down
86 changes: 5 additions & 81 deletions packages/api/src/platforms/vtex/resolvers/query.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { FACET_CROSS_SELLING_MAP } from './../utils/facets'
import { BadRequestError, NotFoundError } from '../../errors'
import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
import { enhanceSku } from '../utils/enhanceSku'
import {
findChannel,
findCrossSelling,
findLocale,
findSkuId,
findSlug,
transformSelectedFacet,
findLocale, transformSelectedFacet
} from '../utils/facets'
import { SORT_MAP } from '../utils/sort'
import { StoreCollection } from './collection'
Expand All @@ -24,84 +20,12 @@ import type {
} from '../../../__generated__/schema'
import type { CategoryTree } from '../clients/commerce/types/CategoryTree'
import type { Context } from '../index'
import { isValidSkuId, pickBestSku } from '../utils/sku'
import { SearchArgs } from '../clients/search'
import { getProductLoader } from '../loaders/product'

export const Query = {
product: async (_: unknown, { locator }: QueryProductArgs, ctx: Context) => {
// Insert channel in context for later usage
const channel = findChannel(locator)
const locale = findLocale(locator)
const id = findSkuId(locator)
const slug = findSlug(locator)

if (channel) {
mutateChannelContext(ctx, channel)
}

if (locale) {
mutateLocaleContext(ctx, locale)
}

const {
loaders: { skuLoader },
clients: { commerce, search },
} = ctx

try {
const skuId = id ?? slug?.split('-').pop() ?? ''

if (!isValidSkuId(skuId)) {
throw new Error('Invalid SkuId')
}

const sku = await skuLoader.load(skuId)

/**
* Here be dragons 🦄🦄🦄
*
* In some cases, the slug has a valid skuId for a different
* product. This condition makes sure that the fetched sku
* is the one we actually asked for
* */
if (
slug &&
sku.isVariantOf.linkText &&
!slug.startsWith(sku.isVariantOf.linkText)
) {
throw new Error(
`Slug was set but the fetched sku does not satisfy the slug condition. slug: ${slug}, linkText: ${sku.isVariantOf.linkText}`
)
}

return sku
} catch (err) {
if (slug == null) {
throw new BadRequestError('Missing slug or id')
}

const route = await commerce.catalog.portal.pagetype(`${slug}/p`)

if (route.pageType !== 'Product' || !route.id) {
throw new NotFoundError(`No product found for slug ${slug}`)
}

const {
products: [product],
} = await search.products({
page: 0,
count: 1,
query: `product:${route.id}`,
})

if (!product) {
throw new NotFoundError(`No product found for id ${route.id}`)
}

const sku = pickBestSku(product.items)

return enhanceSku(sku, product)
}
return getProductLoader(locator, ctx)
},
collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => {
const {
Expand Down Expand Up @@ -279,7 +203,7 @@ export const Query = {
address,
}
},

redirect: async (
_: unknown,
{ term, selectedFacets }: QueryRedirectArgs,
Expand All @@ -302,7 +226,7 @@ export const Query = {
url: redirect,
}
},

sellers: async (
_: unknown,
{ postalCode, geoCoordinates, country, salesChannel }: QuerySellersArgs,
Expand Down
19 changes: 17 additions & 2 deletions packages/api/src/platforms/vtex/resolvers/skuVariations.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Resolver } from '..'
import type { PromiseType } from '../../../typings'
import type { StoreProduct } from './product'
import { StoreProduct } from './product'
import {
createSlugsMap,
getActiveSkuVariations,
getFormattedVariations,
getVariantsByName,
} from '../utils/skuVariants'
import { getProductLoader } from '../loaders/product'

export type Root = PromiseType<ReturnType<typeof StoreProduct.isVariantOf>>

Expand All @@ -27,6 +28,10 @@ export const SkuVariants: Record<string, Resolver<Root>> = {
),

availableVariations: (root, args) => {
console.log("items", JSON.stringify(root, null, "lala"))

root.isVariantOf.items

const dominantVariantName = (args as SlugsMapArgs).dominantVariantName ?? root.variations[0]?.name
const activeVariations = getActiveSkuVariations(root.variations)

Expand All @@ -40,4 +45,14 @@ export const SkuVariants: Record<string, Resolver<Root>> = {

return filteredFormattedVariations
},
}

allVariantProducts: (root, args, ctx) => {
const slugMap = createSlugsMap(
root.isVariantOf.items,
(args as SlugsMapArgs).dominantVariantName ?? root.variations[0]?.name,
root.isVariantOf.linkText
)

return Object.values(slugMap).map(slug => getProductLoader([{ key: 'slug', value: slug }], ctx))
}
}
2 changes: 2 additions & 0 deletions packages/api/src/typeDefs/skuVariants.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type SkuVariants {
considered the dominant one.
"""
availableVariations(dominantVariantName: String): FormattedVariants

allVariantProducts: [StoreProduct]
}

"""
Expand Down
4 changes: 2 additions & 2 deletions packages/core/@generated/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const documents = {
types.ProductSummary_ProductFragmentDoc,
'\n fragment Filter_facets on StoreFacet {\n ... on StoreFacetRange {\n key\n label\n\n min {\n selected\n absolute\n }\n\n max {\n selected\n absolute\n }\n\n __typename\n }\n ... on StoreFacetBoolean {\n key\n label\n values {\n label\n value\n selected\n quantity\n }\n\n __typename\n }\n }\n':
types.Filter_FacetsFragmentDoc,
'\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n':
'\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n additionalProperty {\n value\n name\n }\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n id: productID\n sku\n name\n additionalProperty {\n value\n name\n }\n gtin\n description\n unitMultiplier\n image {\n url\n alternateName\n }\n }\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n':
types.ProductDetailsFragment_ProductFragmentDoc,
'\n fragment ClientManyProducts on Query {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n ) {\n products {\n pageInfo {\n totalCount\n }\n }\n }\n }\n':
types.ClientManyProductsFragmentDoc,
Expand Down Expand Up @@ -76,7 +76,7 @@ export function gql(
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(
source: '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n'
source: '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n additionalProperty {\n value\n name\n }\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n id: productID\n sku\n name\n additionalProperty {\n value\n name\n }\n gtin\n description\n unitMultiplier\n image {\n url\n alternateName\n }\n }\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n'
): typeof import('./graphql').ProductDetailsFragment_ProductFragmentDoc
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
Expand Down
Loading
Loading