Skip to content

Commit

Permalink
canonical
Browse files Browse the repository at this point in the history
  • Loading branch information
tlgimenes committed Jun 10, 2022
1 parent 9e88a7e commit ebd4b72
Show file tree
Hide file tree
Showing 14 changed files with 111 additions and 56 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
34 changes: 34 additions & 0 deletions packages/api/src/platforms/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 const isFastStoreError = (error: any): error is FastStoreError =>
error?.name === 'FastStoreError'

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

export const isBadRequestError = (error: any): error is BadRequestError =>
error?.extensions?.type === 'BadRequestError'
10 changes: 9 additions & 1 deletion packages/api/src/platforms/vtex/clients/commerce/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context, Options } from '../../index'
import { fetchAPI } from '../fetch'
import type { Product } from '../search/types/ProductSearchResult'
import type { Context, Options } from '../../index'
import type { Brand } from './types/Brand'
import type { CategoryTree } from './types/CategoryTree'
import type { OrderForm, OrderFormInputItem } from './types/OrderForm'
Expand Down Expand Up @@ -152,5 +153,12 @@ export const VtexCommerce = (
body: '{}',
})
},
search: {
slug: (slug: string): Promise<Product[]> => {
return fetchAPI(
`${base}/api/catalog_system/pub/products/search/${slug}/p`
)
},
},
}
}
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
23 changes: 5 additions & 18 deletions packages/api/src/platforms/vtex/loaders/sku.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import DataLoader from 'dataloader'

import { BadRequestError } from '../utils/errors'
import { enhanceSku } from '../utils/enhanceSku'
import { NotFoundError } from '../../errors'
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 @@ -39,15 +26,15 @@ export const getSkuLoader = (_: Options, clients: Clients) => {
const missingSkus = skus.filter((sku) => !sku)

if (missingSkus.length > 0) {
throw new Error(
`Search API did not return the following skus: ${missingSkus.join(',')}`
throw new NotFoundError(
`Search API did not found the following skus: ${missingSkus.join(',')}`
)
}

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
8 changes: 5 additions & 3 deletions packages/api/src/platforms/vtex/resolvers/product.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { canonicalFromProduct } from '../utils/canonical'
import { enhanceCommercialOffer } from '../utils/enhanceCommercialOffer'
import { bestOfferFirst } from '../utils/productStock'
import { slugify } from '../utils/slugify'
Expand Down Expand Up @@ -41,9 +42,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 } }) => ({
title: productName,
description,
seo: ({ isVariantOf }) => ({
title: isVariantOf.productName,
description: isVariantOf.description,
canonical: canonicalFromProduct(isVariantOf),
}),
brand: ({ isVariantOf: { brand } }) => ({ name: brand }),
breadcrumbList: ({
Expand Down
26 changes: 25 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 } 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,28 @@ export const Query = {

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

return skuLoader.load(locator)
const skuIdFromSlug = async (s: string) => {
// Standard VTEX PDP routes does not contain skuIds.
const [product] = await commerce.search.slug(s).catch(() => [])

if (product) {
return product.items[0].itemId
}

// We are not in a standard VTEX PDP route, this means we are in a /slug-skuId/p route
return s?.split('-').pop() ?? ''
}

const skuId = slug ? await skuIdFromSlug(slug) : id

if (skuId) {
return skuLoader.load(skuId)
}

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: () => '',
}
6 changes: 3 additions & 3 deletions packages/api/src/platforms/vtex/resolvers/validateCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,16 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
return isSameOrder && orderItemsAreSync
}

const orderFormToCart = (
const orderFormToCart = async (
form: OrderForm,
skuLoader: Context['loaders']['skuLoader']
) => {
return {
order: {
orderNumber: form.orderFormId,
acceptedOffer: form.items.map((item) => ({
acceptedOffer: form.items.map(async (item) => ({
...item,
product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
product: await skuLoader.load(item.id), // TODO: add channel
})),
},
messages: form.messages.map(({ text, status }) => ({
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/platforms/vtex/utils/canonical.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Product } from '../clients/search/types/ProductSearchResult'

export const canonicalFromProduct = ({ linkText }: Product) => `/${linkText}/p`
13 changes: 0 additions & 13 deletions packages/api/src/platforms/vtex/utils/errors.ts

This file was deleted.

6 changes: 6 additions & 0 deletions packages/api/src/platforms/vtex/utils/facets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export const transformSelectedFacet = ({ key, value }: SelectedFacet) => {
}
}

export const findSlug = (facets?: Maybe<SelectedFacet[]>) =>
facets?.find((x) => x.key === 'slug')?.value ?? null

export const findSkuId = (facets?: Maybe<SelectedFacet[]>) =>
facets?.find((x) => x.key === 'id')?.value ?? null

export const findLocale = (facets?: Maybe<SelectedFacet[]>) =>
facets?.find((x) => x.key === 'locale')?.value ?? null

Expand Down
22 changes: 11 additions & 11 deletions packages/api/test/__snapshots__/queries.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ Object {
"productID": "99988213",
"review": Array [],
"seo": Object {
"canonical": "",
"canonical": "/4k-philips-monitor/p",
"description": "4k Philips Monitor 27\\"",
"title": "4k Philips Monitor 27\\"",
"titleTemplate": "",
Expand Down Expand Up @@ -290,7 +290,7 @@ Object {
"productID": "99988211",
"review": Array [],
"seo": Object {
"canonical": "",
"canonical": "/aedle-vk1-headphone/p",
"description": "Aedle VK-1 L Headphone",
"title": "Aedle VK-1 L Headphone",
"titleTemplate": "",
Expand Down Expand Up @@ -348,7 +348,7 @@ Object {
"productID": "99988214",
"review": Array [],
"seo": Object {
"canonical": "",
"canonical": "/echo-dot-smart-speaker/p",
"description": "Echo Dot Smart Speaker",
"title": "Echo Dot Smart Speaker",
"titleTemplate": "",
Expand Down Expand Up @@ -406,7 +406,7 @@ Object {
"productID": "99988210",
"review": Array [],
"seo": Object {
"canonical": "",
"canonical": "/oculus-vr-headset/p",
"description": "Virtual reality kit",
"title": "Oculus VR Headset",
"titleTemplate": "",
Expand Down Expand Up @@ -464,7 +464,7 @@ Object {
"productID": "99988212",
"review": Array [],
"seo": Object {
"canonical": "",
"canonical": "/apple-magic-mouse/p",
"description": "Apple Magic Mouse",
"title": "Apple Magic Mouse",
"titleTemplate": "",
Expand Down Expand Up @@ -601,7 +601,7 @@ Object {
"productID": "64953394",
"review": Array [],
"seo": Object {
"canonical": "",
"canonical": "/unbranded-concrete-table-small/p",
"description": "Aut omnis nobis tenetur.",
"title": "Unbranded Concrete Table Small",
"titleTemplate": "",
Expand Down Expand Up @@ -771,7 +771,7 @@ Object {
"itemCondition": "https://schema.org/NewCondition",
"itemOffered": Object {
"seo": Object {
"canonical": "",
"canonical": "/licensed-cotton-hat-licensed/p",
"description": "Consequatur placeat optio adipisci aut voluptate excepturi.",
"title": "Licensed Cotton Hat Licensed",
"titleTemplate": "",
Expand Down Expand Up @@ -843,7 +843,7 @@ Object {
"itemCondition": "https://schema.org/NewCondition",
"itemOffered": Object {
"seo": Object {
"canonical": "",
"canonical": "/handmade-granite-computer-unbranded/p",
"description": "Ipsa in sequi incidunt dolores.",
"title": "Handmade Granite Computer Unbranded",
"titleTemplate": "",
Expand Down Expand Up @@ -929,7 +929,7 @@ Object {
"itemCondition": "https://schema.org/NewCondition",
"itemOffered": Object {
"seo": Object {
"canonical": "",
"canonical": "/small-cotton-cheese-3325400227651/p",
"description": "Dolor harum perferendis voluptatem tempora voluptatum ut et sapiente iure.",
"title": "Small Cotton Cheese",
"titleTemplate": "",
Expand Down Expand Up @@ -1001,7 +1001,7 @@ Object {
"itemCondition": "https://schema.org/NewCondition",
"itemOffered": Object {
"seo": Object {
"canonical": "",
"canonical": "/tasty-frozen-tuna-handmade/p",
"description": "Recusandae dolores alias.",
"title": "Tasty Frozen Tuna Handmade",
"titleTemplate": "",
Expand Down Expand Up @@ -1077,7 +1077,7 @@ Object {
"itemCondition": "https://schema.org/NewCondition",
"itemOffered": Object {
"seo": Object {
"canonical": "",
"canonical": "/sleek-metal-pizza/p",
"description": "Aliquam a cumque ratione voluptatem in.",
"title": "Sleek Metal Pizza",
"titleTemplate": "",
Expand Down

0 comments on commit ebd4b72

Please sign in to comment.