@faststore/api 1.9.4 → 1.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/api.cjs.development.js +153 -45
  3. package/dist/api.cjs.development.js.map +1 -1
  4. package/dist/api.cjs.production.min.js +1 -1
  5. package/dist/api.cjs.production.min.js.map +1 -1
  6. package/dist/api.esm.js +149 -46
  7. package/dist/api.esm.js.map +1 -1
  8. package/dist/index.d.ts +6 -4
  9. package/dist/platforms/errors.d.ts +19 -0
  10. package/dist/platforms/vtex/clients/commerce/types/Portal.d.ts +1 -1
  11. package/dist/platforms/vtex/index.d.ts +5 -4
  12. package/dist/platforms/vtex/loaders/index.d.ts +1 -1
  13. package/dist/platforms/vtex/loaders/sku.d.ts +1 -2
  14. package/dist/platforms/vtex/resolvers/mutation.d.ts +3 -3
  15. package/dist/platforms/vtex/resolvers/offer.d.ts +1 -1
  16. package/dist/platforms/vtex/resolvers/seo.d.ts +1 -0
  17. package/dist/platforms/vtex/resolvers/validateCart.d.ts +3 -3
  18. package/dist/platforms/vtex/utils/canonical.d.ts +2 -0
  19. package/dist/platforms/vtex/utils/facets.d.ts +2 -0
  20. package/dist/platforms/vtex/utils/orderStatistics.d.ts +4 -0
  21. package/dist/platforms/vtex/utils/sku.d.ts +8 -0
  22. package/package.json +2 -2
  23. package/src/index.ts +1 -0
  24. package/src/platforms/errors.ts +34 -0
  25. package/src/platforms/vtex/clients/commerce/index.ts +1 -1
  26. package/src/platforms/vtex/clients/commerce/types/Portal.ts +1 -1
  27. package/src/platforms/vtex/loaders/collection.ts +1 -1
  28. package/src/platforms/vtex/loaders/sku.ts +6 -19
  29. package/src/platforms/vtex/resolvers/offer.ts +6 -3
  30. package/src/platforms/vtex/resolvers/product.ts +6 -4
  31. package/src/platforms/vtex/resolvers/query.ts +44 -1
  32. package/src/platforms/vtex/resolvers/seo.ts +2 -2
  33. package/src/platforms/vtex/resolvers/validateCart.ts +3 -3
  34. package/src/platforms/vtex/utils/canonical.ts +3 -0
  35. package/src/platforms/vtex/utils/facets.ts +6 -0
  36. package/src/platforms/vtex/utils/orderStatistics.ts +16 -0
  37. package/src/platforms/vtex/utils/sku.ts +26 -0
  38. package/dist/platforms/vtex/utils/errors.d.ts +0 -6
  39. package/src/platforms/vtex/utils/errors.ts +0 -13
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Options as OptionsVTEX } from './platforms/vtex';
2
2
  export * from './__generated__/schema';
3
+ export * from './platforms/errors';
3
4
  export declare type Options = OptionsVTEX;
4
5
  export declare const getTypeDefs: () => string;
5
6
  export declare const getResolvers: (options: Options) => {
@@ -48,6 +49,7 @@ export declare const getResolvers: (options: Options) => {
48
49
  StoreSeo: Record<string, import("./platforms/vtex").Resolver<{
49
50
  title?: string | undefined;
50
51
  description?: string | undefined;
52
+ canonical?: string | undefined;
51
53
  }, unknown, any>>;
52
54
  StoreFacet: Record<string, import("./platforms/vtex").Resolver<import("./platforms/vtex/clients/search/types/FacetSearchResult").Facet, unknown, any>>;
53
55
  StoreFacetValue: Record<string, import("./platforms/vtex").Resolver<import("./platforms/vtex/clients/search/types/FacetSearchResult").FacetValue, unknown, any>>;
@@ -56,7 +58,7 @@ export declare const getResolvers: (options: Options) => {
56
58
  } & {
57
59
  attachmentsValues?: import("./platforms/vtex/clients/commerce/types/OrderForm").Attachment[] | undefined;
58
60
  }> | (import("./platforms/vtex/clients/commerce/types/OrderForm").OrderFormItem & {
59
- product: Promise<import("./platforms/vtex/utils/enhanceSku").EnhancedSku>;
61
+ product: import("./platforms/vtex/utils/enhanceSku").EnhancedSku;
60
62
  }), unknown, any>>;
61
63
  StoreAggregateRating: Record<string, import("./platforms/vtex").Resolver<unknown, unknown, any>>;
62
64
  StoreReview: Record<string, import("./platforms/vtex").Resolver<unknown, unknown, any>>;
@@ -124,8 +126,8 @@ export declare const getResolvers: (options: Options) => {
124
126
  }, ctx: import("./platforms/vtex").Context) => Promise<{
125
127
  order: {
126
128
  orderNumber: string;
127
- acceptedOffer: {
128
- product: Promise<import("./platforms/vtex/utils/enhanceSku").EnhancedSku>;
129
+ acceptedOffer: Promise<{
130
+ product: import("./platforms/vtex/utils/enhanceSku").EnhancedSku;
129
131
  id: string;
130
132
  name: string;
131
133
  detailUrl: string;
@@ -169,7 +171,7 @@ export declare const getResolvers: (options: Options) => {
169
171
  total: number;
170
172
  };
171
173
  attachments: import("./platforms/vtex/clients/commerce/types/OrderForm").Attachment[];
172
- }[];
174
+ }>[];
173
175
  };
174
176
  messages: {
175
177
  text: any;
@@ -0,0 +1,19 @@
1
+ declare type ErrorType = 'BadRequestError' | 'NotFoundError' | 'RedirectError';
2
+ interface Extension {
3
+ type: ErrorType;
4
+ status: number;
5
+ }
6
+ declare class FastStoreError<T extends Extension = Extension> extends Error {
7
+ extensions: T;
8
+ constructor(extensions: T, message?: string);
9
+ }
10
+ export declare class BadRequestError extends FastStoreError {
11
+ constructor(message?: string);
12
+ }
13
+ export declare class NotFoundError extends FastStoreError {
14
+ constructor(message?: string);
15
+ }
16
+ export declare const isFastStoreError: (error: any) => error is FastStoreError<Extension>;
17
+ export declare const isNotFoundError: (error: any) => error is NotFoundError;
18
+ export declare const isBadRequestError: (error: any) => error is BadRequestError;
19
+ export {};
@@ -5,7 +5,7 @@ export interface CollectionPageType {
5
5
  url: string;
6
6
  title: string;
7
7
  metaTagDescription: string;
8
- pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory';
8
+ pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory' | 'Product';
9
9
  }
10
10
  export interface FallbackPageType {
11
11
  id: null;
@@ -77,6 +77,7 @@ export declare const getResolvers: (_: Options) => {
77
77
  StoreSeo: Record<string, Resolver<{
78
78
  title?: string | undefined;
79
79
  description?: string | undefined;
80
+ canonical?: string | undefined;
80
81
  }, unknown, any>>;
81
82
  StoreFacet: Record<string, Resolver<import("./clients/search/types/FacetSearchResult").Facet, unknown, any>>;
82
83
  StoreFacetValue: Record<string, Resolver<import("./clients/search/types/FacetSearchResult").FacetValue, unknown, any>>;
@@ -85,7 +86,7 @@ export declare const getResolvers: (_: Options) => {
85
86
  } & {
86
87
  attachmentsValues?: import("./clients/commerce/types/OrderForm").Attachment[] | undefined;
87
88
  }> | (import("./clients/commerce/types/OrderForm").OrderFormItem & {
88
- product: Promise<import("./utils/enhanceSku").EnhancedSku>;
89
+ product: import("./utils/enhanceSku").EnhancedSku;
89
90
  }), unknown, any>>;
90
91
  StoreAggregateRating: Record<string, Resolver<unknown, unknown, any>>;
91
92
  StoreReview: Record<string, Resolver<unknown, unknown, any>>;
@@ -153,8 +154,8 @@ export declare const getResolvers: (_: Options) => {
153
154
  }, ctx: Context) => Promise<{
154
155
  order: {
155
156
  orderNumber: string;
156
- acceptedOffer: {
157
- product: Promise<import("./utils/enhanceSku").EnhancedSku>;
157
+ acceptedOffer: Promise<{
158
+ product: import("./utils/enhanceSku").EnhancedSku;
158
159
  id: string;
159
160
  name: string;
160
161
  detailUrl: string;
@@ -198,7 +199,7 @@ export declare const getResolvers: (_: Options) => {
198
199
  total: number;
199
200
  };
200
201
  attachments: import("./clients/commerce/types/OrderForm").Attachment[];
201
- }[];
202
+ }>[];
202
203
  };
203
204
  messages: {
204
205
  text: any;
@@ -1,7 +1,7 @@
1
1
  import type { Context, Options } from '..';
2
2
  export declare type Loaders = ReturnType<typeof getLoaders>;
3
3
  export declare const getLoaders: (options: Options, { clients }: Context) => {
4
- skuLoader: import("dataloader")<import("../utils/facets").SelectedFacet[], import("../utils/enhanceSku").EnhancedSku, import("../utils/facets").SelectedFacet[]>;
4
+ skuLoader: import("dataloader")<string, import("../utils/enhanceSku").EnhancedSku, string>;
5
5
  simulationLoader: import("dataloader")<import("../clients/commerce/types/Simulation").PayloadItem[], import("../clients/commerce/types/Simulation").Simulation, import("../clients/commerce/types/Simulation").PayloadItem[]>;
6
6
  collectionLoader: import("dataloader")<string, import("../clients/commerce/types/Portal").CollectionPageType, string>;
7
7
  };
@@ -2,5 +2,4 @@ import DataLoader from 'dataloader';
2
2
  import type { EnhancedSku } from '../utils/enhanceSku';
3
3
  import type { Options } from '..';
4
4
  import type { Clients } from '../clients';
5
- import type { SelectedFacet } from '../utils/facets';
6
- export declare const getSkuLoader: (_: Options, clients: Clients) => DataLoader<SelectedFacet[], EnhancedSku, SelectedFacet[]>;
5
+ export declare const getSkuLoader: (_: Options, clients: Clients) => DataLoader<string, EnhancedSku, string>;
@@ -4,8 +4,8 @@ export declare const Mutation: {
4
4
  }, ctx: import("..").Context) => Promise<{
5
5
  order: {
6
6
  orderNumber: string;
7
- acceptedOffer: {
8
- product: Promise<import("../utils/enhanceSku").EnhancedSku>;
7
+ acceptedOffer: Promise<{
8
+ product: import("../utils/enhanceSku").EnhancedSku;
9
9
  id: string;
10
10
  name: string;
11
11
  detailUrl: string;
@@ -49,7 +49,7 @@ export declare const Mutation: {
49
49
  total: number;
50
50
  };
51
51
  attachments: import("../clients/commerce/types/OrderForm").Attachment[];
52
- }[];
52
+ }>[];
53
53
  };
54
54
  messages: {
55
55
  text: any;
@@ -4,7 +4,7 @@ import type { ArrayElementType } from '../../../typings';
4
4
  import type { EnhancedSku } from '../utils/enhanceSku';
5
5
  import type { OrderFormItem } from '../clients/commerce/types/OrderForm';
6
6
  declare type OrderFormProduct = OrderFormItem & {
7
- product: Promise<EnhancedSku>;
7
+ product: EnhancedSku;
8
8
  };
9
9
  declare type SearchProduct = ArrayElementType<ReturnType<typeof StoreAggregateOffer.offers>>;
10
10
  declare type Root = SearchProduct | OrderFormProduct;
@@ -2,6 +2,7 @@ import type { Resolver } from '..';
2
2
  declare type Root = {
3
3
  title?: string;
4
4
  description?: string;
5
+ canonical?: string;
5
6
  };
6
7
  export declare const StoreSeo: Record<string, Resolver<Root>>;
7
8
  export {};
@@ -18,8 +18,8 @@ export declare const validateCart: (_: unknown, { cart: { order } }: {
18
18
  }, ctx: Context) => Promise<{
19
19
  order: {
20
20
  orderNumber: string;
21
- acceptedOffer: {
22
- product: Promise<import("../utils/enhanceSku").EnhancedSku>;
21
+ acceptedOffer: Promise<{
22
+ product: import("../utils/enhanceSku").EnhancedSku;
23
23
  id: string;
24
24
  name: string;
25
25
  detailUrl: string;
@@ -63,7 +63,7 @@ export declare const validateCart: (_: unknown, { cart: { order } }: {
63
63
  total: number;
64
64
  };
65
65
  attachments: import("../clients/commerce/types/OrderForm").Attachment[];
66
- }[];
66
+ }>[];
67
67
  };
68
68
  messages: {
69
69
  text: any;
@@ -0,0 +1,2 @@
1
+ import type { Product } from '../clients/search/types/ProductSearchResult';
2
+ export declare const canonicalFromProduct: ({ linkText }: Product) => string;
@@ -13,5 +13,7 @@ export declare const transformSelectedFacet: ({ key, value }: SelectedFacet) =>
13
13
  key: string;
14
14
  value: string;
15
15
  };
16
+ export declare const findSlug: (facets?: SelectedFacet[] | null | undefined) => string | null;
17
+ export declare const findSkuId: (facets?: SelectedFacet[] | null | undefined) => string | null;
16
18
  export declare const findLocale: (facets?: SelectedFacet[] | null | undefined) => string | null;
17
19
  export declare const findChannel: (facets?: SelectedFacet[] | null | undefined) => string | null;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * More info at: https://en.wikipedia.org/wiki/Order_statistic
3
+ */
4
+ export declare const min: <T>(array: T[], cmp: (a: T, b: T) => number) => T;
@@ -0,0 +1,8 @@
1
+ import type { Item } from '../clients/search/types/ProductSearchResult';
2
+ /**
3
+ * This function implements Portal heuristics for returning the best sku for a product.
4
+ *
5
+ * The best sku is the one with the best (cheapest available) offer
6
+ * */
7
+ export declare const pickBestSku: (skus: Item[]) => Item;
8
+ export declare const isValidSkuId: (skuId: string) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/api",
3
- "version": "1.9.4",
3
+ "version": "1.9.7",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -44,5 +44,5 @@
44
44
  "peerDependencies": {
45
45
  "graphql": "^15.6.0"
46
46
  },
47
- "gitHead": "830f839f0d3ca50c1d54c8c1efa8d8aecef3db92"
47
+ "gitHead": "a0c8cbd82dc0d83b4ca9f186fd974e09a2bbd768"
48
48
  }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import { typeDefs } from './typeDefs'
8
8
  import type { Options as OptionsVTEX } from './platforms/vtex'
9
9
 
10
10
  export * from './__generated__/schema'
11
+ export * from './platforms/errors'
11
12
 
12
13
  export type Options = OptionsVTEX
13
14
 
@@ -0,0 +1,34 @@
1
+ type ErrorType = 'BadRequestError' | 'NotFoundError' | 'RedirectError'
2
+
3
+ interface Extension {
4
+ type: ErrorType
5
+ status: number
6
+ }
7
+
8
+ class FastStoreError<T extends Extension = Extension> extends Error {
9
+ constructor(public extensions: T, message?: string) {
10
+ super(message)
11
+ this.name = 'FastStoreError'
12
+ }
13
+ }
14
+
15
+ export class BadRequestError extends FastStoreError {
16
+ constructor(message?: string) {
17
+ super({ status: 400, type: 'BadRequestError' }, message)
18
+ }
19
+ }
20
+
21
+ export class NotFoundError extends FastStoreError {
22
+ constructor(message?: string) {
23
+ super({ status: 404, type: 'NotFoundError' }, message)
24
+ }
25
+ }
26
+
27
+ export const isFastStoreError = (error: any): error is FastStoreError =>
28
+ error?.name === 'FastStoreError'
29
+
30
+ export const isNotFoundError = (error: any): error is NotFoundError =>
31
+ error?.extensions?.type === 'NotFoundError'
32
+
33
+ export const isBadRequestError = (error: any): error is BadRequestError =>
34
+ error?.extensions?.type === 'BadRequestError'
@@ -1,5 +1,5 @@
1
- import type { Context, Options } from '../../index'
2
1
  import { fetchAPI } from '../fetch'
2
+ import type { Context, Options } from '../../index'
3
3
  import type { Brand } from './types/Brand'
4
4
  import type { CategoryTree } from './types/CategoryTree'
5
5
  import type { OrderForm, OrderFormInputItem } from './types/OrderForm'
@@ -6,7 +6,7 @@ export interface CollectionPageType {
6
6
  url: string
7
7
  title: string
8
8
  metaTagDescription: string
9
- pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory'
9
+ pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory' | 'Product'
10
10
  }
11
11
 
12
12
  export interface FallbackPageType {
@@ -1,7 +1,7 @@
1
1
  import DataLoader from 'dataloader'
2
2
  import pLimit from 'p-limit'
3
3
 
4
- import { NotFoundError } from '../utils/errors'
4
+ import { NotFoundError } from '../../errors'
5
5
  import type { CollectionPageType } from '../clients/commerce/types/Portal'
6
6
  import type { Options } from '..'
7
7
  import type { Clients } from '../clients'
@@ -1,26 +1,13 @@
1
1
  import DataLoader from 'dataloader'
2
2
 
3
- import { BadRequestError } from '../utils/errors'
4
3
  import { enhanceSku } from '../utils/enhanceSku'
4
+ import { NotFoundError } from '../../errors'
5
5
  import type { EnhancedSku } from '../utils/enhanceSku'
6
6
  import type { Options } from '..'
7
7
  import type { Clients } from '../clients'
8
- import type { SelectedFacet } from '../utils/facets'
9
8
 
10
9
  export const getSkuLoader = (_: Options, clients: Clients) => {
11
- const loader = async (facetsList: readonly SelectedFacet[][]) => {
12
- const skuIds = facetsList.map((facets) => {
13
- const maybeFacet = facets.find(({ key }) => key === 'id')
14
-
15
- if (!maybeFacet) {
16
- throw new BadRequestError(
17
- 'Error while loading SKU. Needs to pass an id to selected facets'
18
- )
19
- }
20
-
21
- return maybeFacet.value
22
- })
23
-
10
+ const loader = async (skuIds: readonly string[]) => {
24
11
  const { products } = await clients.search.products({
25
12
  query: `sku:${skuIds.join(';')}`,
26
13
  page: 0,
@@ -36,18 +23,18 @@ export const getSkuLoader = (_: Options, clients: Clients) => {
36
23
  }, {} as Record<string, EnhancedSku>)
37
24
 
38
25
  const skus = skuIds.map((skuId) => skuBySkuId[skuId])
39
- const missingSkus = skus.filter((sku) => !sku)
26
+ const missingSkus = skuIds.filter((skuId) => !skuBySkuId[skuId])
40
27
 
41
28
  if (missingSkus.length > 0) {
42
- throw new Error(
43
- `Search API did not return the following skus: ${missingSkus.join(',')}`
29
+ throw new NotFoundError(
30
+ `Search API did not found the following skus: ${missingSkus.join(',')}`
44
31
  )
45
32
  }
46
33
 
47
34
  return skus
48
35
  }
49
36
 
50
- return new DataLoader<SelectedFacet[], EnhancedSku>(loader, {
37
+ return new DataLoader<string, EnhancedSku>(loader, {
51
38
  maxBatchSize: 99, // Max allowed batch size of Search API
52
39
  })
53
40
  }
@@ -11,7 +11,7 @@ import type { ArrayElementType } from '../../../typings'
11
11
  import type { EnhancedSku } from '../utils/enhanceSku'
12
12
  import type { OrderFormItem } from '../clients/commerce/types/OrderForm'
13
13
 
14
- type OrderFormProduct = OrderFormItem & { product: Promise<EnhancedSku> }
14
+ type OrderFormProduct = OrderFormItem & { product: EnhancedSku }
15
15
  type SearchProduct = ArrayElementType<
16
16
  ReturnType<typeof StoreAggregateOffer.offers>
17
17
  >
@@ -96,13 +96,16 @@ export const StoreOffer: Record<string, Resolver<Root>> = {
96
96
 
97
97
  return null
98
98
  },
99
- itemOffered: async (root) => {
99
+ itemOffered: (root) => {
100
100
  if (isSearchItem(root)) {
101
101
  return root.product
102
102
  }
103
103
 
104
104
  if (isOrderFormItem(root)) {
105
- return { ...(await root.product), attachmentsValues: root.attachments }
105
+ return {
106
+ ...root.product,
107
+ attachmentsValues: root.attachments,
108
+ }
106
109
  }
107
110
 
108
111
  return null
@@ -1,3 +1,4 @@
1
+ import { canonicalFromProduct } from '../utils/canonical'
1
2
  import { enhanceCommercialOffer } from '../utils/enhanceCommercialOffer'
2
3
  import { bestOfferFirst } from '../utils/productStock'
3
4
  import { slugify } from '../utils/slugify'
@@ -41,9 +42,10 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
41
42
  name: ({ isVariantOf, name }) => name ?? isVariantOf.productName,
42
43
  slug: ({ isVariantOf: { linkText }, itemId }) => getSlug(linkText, itemId),
43
44
  description: ({ isVariantOf: { description } }) => description,
44
- seo: ({ isVariantOf: { description, productName } }) => ({
45
- title: productName,
46
- description,
45
+ seo: ({ isVariantOf }) => ({
46
+ title: isVariantOf.productName,
47
+ description: isVariantOf.description,
48
+ canonical: canonicalFromProduct(isVariantOf),
47
49
  }),
48
50
  brand: ({ isVariantOf: { brand } }) => ({ name: brand }),
49
51
  breadcrumbList: ({
@@ -85,7 +87,7 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
85
87
  aggregateRating: () => ({}),
86
88
  offers: (root) =>
87
89
  root.sellers
88
- .flatMap((seller) =>
90
+ .map((seller) =>
89
91
  enhanceCommercialOffer({
90
92
  offer: seller.commertialOffer,
91
93
  seller,
@@ -1,8 +1,11 @@
1
+ import { NotFoundError, BadRequestError } from '../../errors'
1
2
  import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
2
3
  import { enhanceSku } from '../utils/enhanceSku'
3
4
  import {
4
5
  findChannel,
5
6
  findLocale,
7
+ findSkuId,
8
+ findSlug,
6
9
  transformSelectedFacet,
7
10
  } from '../utils/facets'
8
11
  import { SORT_MAP } from '../utils/sort'
@@ -16,12 +19,15 @@ import type {
16
19
  } from '../../../__generated__/schema'
17
20
  import type { CategoryTree } from '../clients/commerce/types/CategoryTree'
18
21
  import type { Context } from '../index'
22
+ import { isValidSkuId, pickBestSku } from '../utils/sku'
19
23
 
20
24
  export const Query = {
21
25
  product: async (_: unknown, { locator }: QueryProductArgs, ctx: Context) => {
22
26
  // Insert channel in context for later usage
23
27
  const channel = findChannel(locator)
24
28
  const locale = findLocale(locator)
29
+ const id = findSkuId(locator)
30
+ const slug = findSlug(locator)
25
31
 
26
32
  if (channel) {
27
33
  mutateChannelContext(ctx, channel)
@@ -33,9 +39,46 @@ export const Query = {
33
39
 
34
40
  const {
35
41
  loaders: { skuLoader },
42
+ clients: { commerce, search },
36
43
  } = ctx
37
44
 
38
- return skuLoader.load(locator)
45
+ try {
46
+ const skuId = id ?? slug?.split('-').pop() ?? ''
47
+
48
+ if (!isValidSkuId(skuId)) {
49
+ throw new Error('Invalid SkuId')
50
+ }
51
+
52
+ const sku = await skuLoader.load(skuId)
53
+
54
+ return sku
55
+ } catch (err) {
56
+ if (slug == null) {
57
+ throw new BadRequestError('Missing slug or id')
58
+ }
59
+
60
+ const route = await commerce.catalog.portal.pagetype(`${slug}/p`)
61
+
62
+ if (route.pageType !== 'Product' || !route.id) {
63
+ throw new NotFoundError(`No product found for slug ${slug}`)
64
+ }
65
+
66
+ const {
67
+ products: [product],
68
+ } = await search.products({
69
+ page: 0,
70
+ count: 1,
71
+ query: `product:${route.id}`,
72
+ })
73
+
74
+ if (!product) {
75
+ throw new NotFoundError(`No product found for id ${route.id}`)
76
+ }
77
+
78
+ const sku = pickBestSku(product.items)
79
+
80
+ return enhanceSku(sku, product)
81
+ }
39
82
  },
40
83
  collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => {
41
84
  const {
@@ -1,10 +1,10 @@
1
1
  import type { Resolver } from '..'
2
2
 
3
- type Root = { title?: string; description?: string }
3
+ type Root = { title?: string; description?: string; canonical?: string }
4
4
 
5
5
  export const StoreSeo: Record<string, Resolver<Root>> = {
6
6
  title: ({ title }) => title ?? '',
7
7
  description: ({ description }) => description ?? '',
8
+ canonical: ({ canonical }) => canonical ?? '',
8
9
  titleTemplate: () => '',
9
- canonical: () => '',
10
10
  }
@@ -96,16 +96,16 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
96
96
  return isSameOrder && orderItemsAreSync
97
97
  }
98
98
 
99
- const orderFormToCart = (
99
+ const orderFormToCart = async (
100
100
  form: OrderForm,
101
101
  skuLoader: Context['loaders']['skuLoader']
102
102
  ) => {
103
103
  return {
104
104
  order: {
105
105
  orderNumber: form.orderFormId,
106
- acceptedOffer: form.items.map((item) => ({
106
+ acceptedOffer: form.items.map(async (item) => ({
107
107
  ...item,
108
- product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
108
+ product: await skuLoader.load(item.id), // TODO: add channel
109
109
  })),
110
110
  },
111
111
  messages: form.messages.map(({ text, status }) => ({
@@ -0,0 +1,3 @@
1
+ import type { Product } from '../clients/search/types/ProductSearchResult'
2
+
3
+ export const canonicalFromProduct = ({ linkText }: Product) => `/${linkText}/p`
@@ -34,6 +34,12 @@ export const transformSelectedFacet = ({ key, value }: SelectedFacet) => {
34
34
  }
35
35
  }
36
36
 
37
+ export const findSlug = (facets?: Maybe<SelectedFacet[]>) =>
38
+ facets?.find((x) => x.key === 'slug')?.value ?? null
39
+
40
+ export const findSkuId = (facets?: Maybe<SelectedFacet[]>) =>
41
+ facets?.find((x) => x.key === 'id')?.value ?? null
42
+
37
43
  export const findLocale = (facets?: Maybe<SelectedFacet[]>) =>
38
44
  facets?.find((x) => x.key === 'locale')?.value ?? null
39
45
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * More info at: https://en.wikipedia.org/wiki/Order_statistic
3
+ */
4
+
5
+ // O(n) search to find the max of an array
6
+ export const min = <T>(array: T[], cmp: (a: T, b: T) => number) => {
7
+ let best = 0
8
+
9
+ for (let curr = 1; curr < array.length; curr++) {
10
+ if (cmp(array[best], array[curr]) > 0) {
11
+ best = curr
12
+ }
13
+ }
14
+
15
+ return array[best]
16
+ }
@@ -0,0 +1,26 @@
1
+ import { min } from './orderStatistics'
2
+ import { bestOfferFirst } from './productStock'
3
+ import type { Item } from '../clients/search/types/ProductSearchResult'
4
+
5
+ /**
6
+ * This function implements Portal heuristics for returning the best sku for a product.
7
+ *
8
+ * The best sku is the one with the best (cheapest available) offer
9
+ * */
10
+ export const pickBestSku = (skus: Item[]) => {
11
+ const offersBySku = skus.flatMap((sku) =>
12
+ sku.sellers.map((seller) => ({
13
+ offer: seller.commertialOffer,
14
+ sku,
15
+ }))
16
+ )
17
+
18
+ const best = min(offersBySku, ({ offer: o1 }, { offer: o2 }) =>
19
+ bestOfferFirst(o1, o2)
20
+ )
21
+
22
+ return best.sku
23
+ }
24
+
25
+ export const isValidSkuId = (skuId: string) =>
26
+ skuId !== '' && !Number.isNaN(Number(skuId))
@@ -1,6 +0,0 @@
1
- export declare class BadRequestError extends Error {
2
- constructor(message: string);
3
- }
4
- export declare class NotFoundError extends Error {
5
- constructor(message: string);
6
- }
@@ -1,13 +0,0 @@
1
- export class BadRequestError extends Error {
2
- constructor(message: string) {
3
- super(message)
4
- this.name = 'BadRequestError'
5
- }
6
- }
7
-
8
- export class NotFoundError extends Error {
9
- constructor(message: string) {
10
- super(message)
11
- this.name = 'NotFoundError'
12
- }
13
- }