Skip to content

Commit

Permalink
accept slug on Query.product
Browse files Browse the repository at this point in the history
  • Loading branch information
tlgimenes committed Jun 7, 2022
1 parent 779174d commit 8c25db1
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 121 deletions.
1 change: 1 addition & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { typeDefs } from './typeDefs'
import type { Options as OptionsVTEX } from './platforms/vtex'

export * from './__generated__/schema'
export * from './platforms/errors'

export type Options = OptionsVTEX

Expand Down
45 changes: 45 additions & 0 deletions packages/api/src/platforms/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
type ErrorType = 'BadRequestError' | 'NotFoundError' | 'RedirectError'

interface Extension {
type: ErrorType
status: number
}

class FastStoreError<T extends Extension = Extension> extends Error {
constructor(public extensions: T, message?: string) {
super(message)
this.name = 'FastStoreError'
}
}

export class BadRequestError extends FastStoreError {
constructor(message?: string) {
super({ status: 400, type: 'BadRequestError' }, message)
}
}

export class NotFoundError extends FastStoreError {
constructor(message?: string) {
super({ status: 404, type: 'NotFoundError' }, message)
}
}

export class RedirectError extends FastStoreError<
Extension & { location: string; status: 301 | 302 }
> {
constructor(status: 301 | 302, location: string, message?: string) {
super({ status, location, type: 'RedirectError' }, message)
}
}

export const isFastStoreError = (error: any): error is FastStoreError =>
error?.name === 'FastStoreError'

export const isRedirectError = (error: any): error is RedirectError =>
error?.extensions?.type === 'RedirectError'

export const isNotFoundError = (error: any): error is NotFoundError =>
error?.extensions?.type === 'NotFoundError'

export const isBadRequestError = (error: any): error is BadRequestError =>
error?.extensions?.type === 'BadRequestError'
8 changes: 8 additions & 0 deletions packages/api/src/platforms/vtex/clients/commerce/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from './types/Simulation'
import type { Session } from './types/Session'
import type { Channel } from '../../utils/channel'
import type { SearchResult } from './types/SearchResult'

const BASE_INIT = {
method: 'POST',
Expand Down Expand Up @@ -152,5 +153,12 @@ export const VtexCommerce = (
body: '{}',
})
},
search: {
slug: (slug: string): Promise<SearchResult[]> => {
return fetchAPI(
`${base}/api/catalog_system/pub/products/search/${slug}/p`
)
},
},
}
}
154 changes: 154 additions & 0 deletions packages/api/src/platforms/vtex/clients/commerce/types/SearchResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
export interface SearchResult {
productId: string
productName: string
brand: string
brandId: number
brandImageUrl: null
linkText: string
productReference: string
productReferenceCode: null
categoryId: string
productTitle: string
metaTagDescription: string
releaseDate: Date
clusterHighlights: any
productClusters: ProductClusters
searchableClusters: any
categories: string[]
categoriesIds: string[]
link: string
specification01: string[]
specifications: string[]
allSpecifications: string[]
allSpecificationsGroups: string[]
description: string
items: Item[]
}

export interface Item {
itemId: string
name: string
nameComplete: string
complementName: string
ean: string
referenceId: ReferenceId[]
measurementUnit: string
unitMultiplier: number
modalType: null
isKit: boolean
images: Image[]
sellers: Seller[]
videos: any[]
estimatedDateArrival: null
}

export interface Image {
imageId: string
imageLabel: string
imageTag: string
imageUrl: string
imageText: string
imageLastModified: Date
}

export interface ReferenceId {
key: string
value: string
}

export interface Seller {
sellerId: string
sellerName: string
addToCartLink: string
sellerDefault: boolean
commertialOffer: CommertialOffer
}

export interface CommertialOffer {
deliverySlaSamplesPerRegion: DeliverySlaSamplesPerRegion
installments: Installment[]
discountHighLight: any[]
giftSkuIds: any[]
teasers: any[]
buyTogether: any[]
itemMetadataAttachment: any[]
price: number
listPrice: number
priceWithoutDiscount: number
rewardValue: number
priceValidUntil: Date
availableQuantity: number
isAvailable: boolean
tax: number
deliverySlaSamples: DeliverySlaSample[]
getInfoErrorMessage: null
cacheVersionUsedToCallCheckout: string
paymentOptions: PaymentOptions
}

export interface DeliverySlaSample {
deliverySlaPerTypes: any[]
region: null
}

export interface DeliverySlaSamplesPerRegion {
the0: DeliverySlaSample
}

export interface Installment {
value: number
interestRate: number
totalValuePlusInterestRate: number
numberOfInstallments: number
paymentSystemName: string
paymentSystemGroupName: string
name: string
}

export interface PaymentOptions {
installmentOptions: InstallmentOption[]
paymentSystems: PaymentSystem[]
payments: any[]
giftCards: any[]
giftCardMessages: any[]
availableAccounts: any[]
availableTokens: any[]
}

export interface InstallmentOption {
paymentSystem: string
bin: null
paymentName: string
paymentGroupName: string
value: number
installments: InstallmentElement[]
}

export interface InstallmentElement {
count: number
hasInterestRate: boolean
interestRate: number
value: number
total: number
sellerMerchantInstallments?: InstallmentElement[]
id?: string
}

export interface PaymentSystem {
id: number
name: string
groupName: string
validator: null
stringId: string
template: string
requiresDocument: boolean
isCustom: boolean
description: null | string
requiresAuthentication: boolean
dueDate: Date
availablePayments: null
}

export interface ProductClusters {
the140: string
}
2 changes: 1 addition & 1 deletion packages/api/src/platforms/vtex/loaders/collection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DataLoader from 'dataloader'
import pLimit from 'p-limit'

import { NotFoundError } from '../utils/errors'
import { NotFoundError } from '../../errors'
import type { CollectionPageType } from '../clients/commerce/types/Portal'
import type { Options } from '..'
import type { Clients } from '../clients'
Expand Down
18 changes: 2 additions & 16 deletions packages/api/src/platforms/vtex/loaders/sku.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import DataLoader from 'dataloader'

import { BadRequestError } from '../utils/errors'
import { enhanceSku } from '../utils/enhanceSku'
import type { EnhancedSku } from '../utils/enhanceSku'
import type { Options } from '..'
import type { Clients } from '../clients'
import type { SelectedFacet } from '../utils/facets'

export const getSkuLoader = (_: Options, clients: Clients) => {
const loader = async (facetsList: readonly SelectedFacet[][]) => {
const skuIds = facetsList.map((facets) => {
const maybeFacet = facets.find(({ key }) => key === 'id')

if (!maybeFacet) {
throw new BadRequestError(
'Error while loading SKU. Needs to pass an id to selected facets'
)
}

return maybeFacet.value
})

const loader = async (skuIds: readonly string[]) => {
const { products } = await clients.search.products({
query: `sku:${skuIds.join(';')}`,
page: 0,
Expand All @@ -47,7 +33,7 @@ export const getSkuLoader = (_: Options, clients: Clients) => {
return skus
}

return new DataLoader<SelectedFacet[], EnhancedSku>(loader, {
return new DataLoader<string, EnhancedSku>(loader, {
maxBatchSize: 99, // Max allowed batch size of Search API
})
}
9 changes: 6 additions & 3 deletions packages/api/src/platforms/vtex/resolvers/offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ArrayElementType } from '../../../typings'
import type { EnhancedSku } from '../utils/enhanceSku'
import type { OrderFormItem } from '../clients/commerce/types/OrderForm'

type OrderFormProduct = OrderFormItem & { product: Promise<EnhancedSku> }
type OrderFormProduct = OrderFormItem & { product: EnhancedSku }
type SearchProduct = ArrayElementType<
ReturnType<typeof StoreAggregateOffer.offers>
>
Expand Down Expand Up @@ -96,13 +96,16 @@ export const StoreOffer: Record<string, Resolver<Root>> = {

return null
},
itemOffered: async (root) => {
itemOffered: (root) => {
if (isSearchItem(root)) {
return root.product
}

if (isOrderFormItem(root)) {
return { ...(await root.product), attachmentsValues: root.attachments }
return {
...root.product,
attachmentsValues: root.attachments,
}
}

return null
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/platforms/vtex/resolvers/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
name: ({ isVariantOf, name }) => name ?? isVariantOf.productName,
slug: ({ isVariantOf: { linkText }, itemId }) => getSlug(linkText, itemId),
description: ({ isVariantOf: { description } }) => description,
seo: ({ isVariantOf: { description, productName } }) => ({
seo: ({ isVariantOf: { description, productName, linkText, items } }) => ({
title: productName,
description,
canonical: `/${linkText}-${items[0].itemId}/p`,
}),
brand: ({ isVariantOf: { brand } }) => ({ name: brand }),
breadcrumbList: ({
Expand Down
34 changes: 33 additions & 1 deletion packages/api/src/platforms/vtex/resolvers/query.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { BadRequestError, RedirectError, NotFoundError } from '../../errors'
import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
import { enhanceSku } from '../utils/enhanceSku'
import {
findChannel,
findLocale,
findSkuId,
findSlug,
transformSelectedFacet,
} from '../utils/facets'
import { SORT_MAP } from '../utils/sort'
Expand All @@ -22,6 +25,8 @@ export const Query = {
// 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)
Expand All @@ -33,9 +38,36 @@ export const Query = {

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

return skuLoader.load(locator)
if (id) {
return skuLoader.load(id)
}

if (slug) {
try {
const skuId = slug?.split('-').pop() ?? ''
const sku = await skuLoader.load(skuId)

return sku
} catch (e) {
// On standard VTEX PDP routes /slug/p does not contain sku ids. We need
// to figure out the sku id before returning a product
const products = await commerce.search.slug(slug)
const product = products?.[0]
const skuId = product?.items[0]?.itemId
const location = skuId && `/${product.linkText}-${skuId}/p`

if (skuId == null) {
throw new NotFoundError(`Could not find product for slug ${slug}`)
}

throw new RedirectError(301, location)
}
}

throw new BadRequestError(`Missing slug or id`)
},
collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => {
const {
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/platforms/vtex/resolvers/seo.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Resolver } from '..'

type Root = { title?: string; description?: string }
type Root = { title?: string; description?: string; canonical?: string }

export const StoreSeo: Record<string, Resolver<Root>> = {
title: ({ title }) => title ?? '',
description: ({ description }) => description ?? '',
canonical: ({ canonical }) => canonical ?? '',
titleTemplate: () => '',
canonical: () => '',
}
Loading

0 comments on commit 8c25db1

Please sign in to comment.