@faststore/api 1.5.8 → 1.5.12

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 (30) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/__generated__/schema.d.ts +7 -0
  3. package/dist/api.cjs.development.js +78 -39
  4. package/dist/api.cjs.development.js.map +1 -1
  5. package/dist/api.cjs.production.min.js +1 -1
  6. package/dist/api.cjs.production.min.js.map +1 -1
  7. package/dist/api.esm.js +78 -39
  8. package/dist/api.esm.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/platforms/vtex/clients/commerce/types/Portal.d.ts +11 -2
  11. package/dist/platforms/vtex/clients/search/types/ProductSearchResult.d.ts +1 -1
  12. package/dist/platforms/vtex/index.d.ts +2 -2
  13. package/dist/platforms/vtex/loaders/collection.d.ts +6 -0
  14. package/dist/platforms/vtex/loaders/index.d.ts +1 -0
  15. package/dist/platforms/vtex/resolvers/collection.d.ts +2 -2
  16. package/dist/platforms/vtex/resolvers/query.d.ts +1 -1
  17. package/package.json +4 -3
  18. package/src/__generated__/schema.ts +8 -0
  19. package/src/platforms/vtex/clients/commerce/types/Portal.ts +13 -8
  20. package/src/platforms/vtex/clients/search/types/ProductSearchResult.ts +1 -1
  21. package/src/platforms/vtex/loaders/collection.ts +50 -0
  22. package/src/platforms/vtex/loaders/index.ts +3 -0
  23. package/src/platforms/vtex/resolvers/collection.ts +15 -17
  24. package/src/platforms/vtex/resolvers/product.ts +5 -0
  25. package/src/platforms/vtex/resolvers/productGroup.ts +10 -0
  26. package/src/platforms/vtex/resolvers/query.ts +16 -24
  27. package/src/typeDefs/index.ts +2 -0
  28. package/src/typeDefs/product.graphql +1 -0
  29. package/src/typeDefs/productGroup.graphql +1 -0
  30. package/src/typeDefs/propertyValue.graphql +4 -0
@@ -0,0 +1,50 @@
1
+ import DataLoader from 'dataloader'
2
+ import pLimit from 'p-limit'
3
+
4
+ import { NotFoundError } from '../utils/errors'
5
+ import type { CollectionPageType } from '../clients/commerce/types/Portal'
6
+ import type { Options } from '..'
7
+ import type { Clients } from '../clients'
8
+
9
+ // Limits concurrent requests to 20 so that they don't timeout
10
+ const CONCURRENT_REQUESTS_MAX = 20
11
+
12
+ const collectionPageTypes = new Set([
13
+ 'brand',
14
+ 'category',
15
+ 'department',
16
+ 'subcategory',
17
+ ] as const)
18
+
19
+ export const isCollectionPageType = (x: any): x is CollectionPageType =>
20
+ typeof x.pageType === 'string' &&
21
+ collectionPageTypes.has(x.pageType.toLowerCase())
22
+
23
+ export const getCollectionLoader = (_: Options, clients: Clients) => {
24
+ const limit = pLimit(CONCURRENT_REQUESTS_MAX)
25
+
26
+ const loader = async (
27
+ slugs: readonly string[]
28
+ ): Promise<CollectionPageType[]> => {
29
+ return Promise.all(
30
+ slugs.map((slug: string) =>
31
+ limit(async () => {
32
+ const page = await clients.commerce.catalog.portal.pagetype(slug)
33
+
34
+ if (isCollectionPageType(page)) {
35
+ return page
36
+ }
37
+
38
+ throw new NotFoundError(
39
+ `Catalog returned ${page.pageType} for slug: ${slug}. This usually happens when there is more than one category with the same name in the same category tree level.`
40
+ )
41
+ })
42
+ )
43
+ )
44
+ }
45
+
46
+ return new DataLoader<string, CollectionPageType>(loader, {
47
+ // DataLoader is being used to cache requests, not to batch them
48
+ batch: false,
49
+ })
50
+ }
@@ -1,5 +1,6 @@
1
1
  import { getSimulationLoader } from './simulation'
2
2
  import { getSkuLoader } from './sku'
3
+ import { getCollectionLoader } from './collection'
3
4
  import type { Context, Options } from '..'
4
5
 
5
6
  export type Loaders = ReturnType<typeof getLoaders>
@@ -7,9 +8,11 @@ export type Loaders = ReturnType<typeof getLoaders>
7
8
  export const getLoaders = (options: Options, { clients }: Context) => {
8
9
  const skuLoader = getSkuLoader(options, clients)
9
10
  const simulationLoader = getSimulationLoader(options, clients)
11
+ const collectionLoader = getCollectionLoader(options, clients)
10
12
 
11
13
  return {
12
14
  skuLoader,
13
15
  simulationLoader,
16
+ collectionLoader,
14
17
  }
15
18
  }
@@ -1,22 +1,20 @@
1
+ import { isCollectionPageType } from '../loaders/collection'
1
2
  import { slugify as baseSlugify } from '../utils/slugify'
2
3
  import type { Resolver } from '..'
3
4
  import type { Brand } from '../clients/commerce/types/Brand'
4
5
  import type { CategoryTree } from '../clients/commerce/types/CategoryTree'
5
- import type { PortalPagetype } from '../clients/commerce/types/Portal'
6
+ import type { CollectionPageType } from '../clients/commerce/types/Portal'
6
7
 
7
- type Root = Brand | (CategoryTree & { level: number }) | PortalPagetype
8
+ type Root = Brand | (CategoryTree & { level: number }) | CollectionPageType
8
9
 
9
10
  const isBrand = (x: any): x is Brand => x.type === 'brand'
10
11
 
11
- const isPortalPageType = (x: any): x is PortalPagetype =>
12
- typeof x.pageType === 'string'
13
-
14
12
  const slugify = (root: Root) => {
15
13
  if (isBrand(root)) {
16
- return baseSlugify(root.name)
14
+ return baseSlugify(root.name.toLowerCase())
17
15
  }
18
16
 
19
- if (isPortalPageType(root)) {
17
+ if (isCollectionPageType(root)) {
20
18
  return new URL(`https://${root.url}`).pathname.slice(1)
21
19
  }
22
20
 
@@ -27,7 +25,7 @@ export const StoreCollection: Record<string, Resolver<Root>> = {
27
25
  id: ({ id }) => id.toString(),
28
26
  slug: (root) => slugify(root),
29
27
  seo: (root) =>
30
- isBrand(root) || isPortalPageType(root)
28
+ isBrand(root) || isCollectionPageType(root)
31
29
  ? {
32
30
  title: root.title,
33
31
  description: root.metaTagDescription,
@@ -39,7 +37,7 @@ export const StoreCollection: Record<string, Resolver<Root>> = {
39
37
  type: (root) =>
40
38
  isBrand(root)
41
39
  ? 'Brand'
42
- : isPortalPageType(root)
40
+ : isCollectionPageType(root)
43
41
  ? root.pageType
44
42
  : root.level === 0
45
43
  ? 'Department'
@@ -51,7 +49,7 @@ export const StoreCollection: Record<string, Resolver<Root>> = {
51
49
  }
52
50
  : {
53
51
  selectedFacets: new URL(
54
- isPortalPageType(root) ? `https://${root.url}` : root.url
52
+ isCollectionPageType(root) ? `https://${root.url}` : root.url
55
53
  ).pathname
56
54
  .slice(1)
57
55
  .split('/')
@@ -62,7 +60,7 @@ export const StoreCollection: Record<string, Resolver<Root>> = {
62
60
  },
63
61
  breadcrumbList: async (root, _, ctx) => {
64
62
  const {
65
- clients: { commerce },
63
+ loaders: { collectionLoader },
66
64
  } = ctx
67
65
 
68
66
  const slug = slugify(root)
@@ -78,17 +76,17 @@ export const StoreCollection: Record<string, Resolver<Root>> = {
78
76
  segments.slice(0, index + 1).join('/')
79
77
  )
80
78
 
81
- const pageTypes = await Promise.all(
82
- slugs.map((s) => commerce.catalog.portal.pagetype(s))
79
+ const collections = await Promise.all(
80
+ slugs.map((s) => collectionLoader.load(s))
83
81
  )
84
82
 
85
83
  return {
86
- itemListElement: pageTypes.map((pageType, index) => ({
87
- item: new URL(`https://${pageType.url}`).pathname.toLowerCase(),
88
- name: pageType.name,
84
+ itemListElement: collections.map((collection, index) => ({
85
+ item: new URL(`https://${collection.url}`).pathname.toLowerCase(),
86
+ name: collection.name,
89
87
  position: index + 1,
90
88
  })),
91
- numberOfItems: pageTypes.length,
89
+ numberOfItems: collections.length,
92
90
  }
93
91
  },
94
92
  }
@@ -88,4 +88,9 @@ export const StoreProduct: Record<string, Resolver<Root>> = {
88
88
  return { ...simulation, product }
89
89
  },
90
90
  isVariantOf: ({ isVariantOf }) => isVariantOf,
91
+ additionalProperty: ({ attributes = [] }) =>
92
+ attributes.map((attribute) => ({
93
+ name: attribute.key,
94
+ value: attribute.value,
95
+ })),
91
96
  }
@@ -6,4 +6,14 @@ export const StoreProductGroup: Record<string, Resolver<Product>> = {
6
6
  hasVariant: (root) => root.skus.map((sku) => enhanceSku(sku, root)),
7
7
  productGroupID: ({ product }) => product,
8
8
  name: ({ name }) => name,
9
+ additionalProperty: ({ textAttributes = [], productSpecifications = [] }) => {
10
+ const specs = new Set(productSpecifications)
11
+
12
+ return textAttributes
13
+ .filter((attribute) => specs.has(attribute.labelKey))
14
+ .map((attribute) => ({
15
+ name: attribute.labelKey,
16
+ value: attribute.labelValue,
17
+ }))
18
+ },
9
19
  }
@@ -1,5 +1,4 @@
1
1
  import { enhanceSku } from '../utils/enhanceSku'
2
- import { NotFoundError } from '../utils/errors'
3
2
  import { transformSelectedFacet } from '../utils/facets'
4
3
  import { SORT_MAP } from '../utils/sort'
5
4
  import { StoreCollection } from './collection'
@@ -29,24 +28,12 @@ export const Query = {
29
28
 
30
29
  return skuLoader.load(locator.map(transformSelectedFacet))
31
30
  },
32
- collection: async (
33
- _: unknown,
34
- { slug }: QueryCollectionArgs,
35
- ctx: Context
36
- ) => {
31
+ collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => {
37
32
  const {
38
- clients: { commerce },
33
+ loaders: { collectionLoader },
39
34
  } = ctx
40
35
 
41
- const result = await commerce.catalog.portal.pagetype(slug)
42
-
43
- const whitelist = ['Brand', 'Category', 'Department', 'Subcategory']
44
-
45
- if (whitelist.includes(result.pageType)) {
46
- return result
47
- }
48
-
49
- throw new NotFoundError(`Not Found: ${slug}`)
36
+ return collectionLoader.load(slug)
50
37
  },
51
38
  search: async (
52
39
  _: unknown,
@@ -100,6 +87,7 @@ export const Query = {
100
87
  endCursor: products.total.toString(),
101
88
  totalCount: products.total,
102
89
  },
90
+ // after + index is bigger than after+first itself because of the array flat() above
103
91
  edges: skus.map((sku, index) => ({
104
92
  node: sku,
105
93
  cursor: (after + index).toString(),
@@ -140,20 +128,24 @@ export const Query = {
140
128
  ...categories,
141
129
  ]
142
130
 
131
+ const validCollections = collections
132
+ // Nullable slugs may cause one route to override the other
133
+ .filter((node) => Boolean(StoreCollection.slug(node, null, ctx, null)))
134
+
143
135
  return {
144
136
  pageInfo: {
145
- hasNextPage: false,
146
- hasPreviousPage: false,
137
+ hasNextPage: validCollections.length - after > first,
138
+ hasPreviousPage: after > 0,
147
139
  startCursor: '0',
148
- endCursor: '0',
140
+ endCursor: (
141
+ Math.min(first, validCollections.length - after) - 1
142
+ ).toString(),
149
143
  },
150
- edges: collections
151
- // Nullable slugs may cause one route to override the other
152
- .filter((node) => Boolean(StoreCollection.slug(node, null, ctx, null)))
153
- .slice(after, first)
144
+ edges: validCollections
145
+ .slice(after, after + first)
154
146
  .map((node, index) => ({
155
147
  node,
156
- cursor: index.toString(),
148
+ cursor: (after + index).toString(),
157
149
  })),
158
150
  }
159
151
  },
@@ -20,6 +20,7 @@ import Review from './review.graphql'
20
20
  import Seo from './seo.graphql'
21
21
  import Cart from './cart.graphql'
22
22
  import Status from './status.graphql'
23
+ import PropertyValue from './propertyValue.graphql'
23
24
 
24
25
  export const typeDefs = [
25
26
  Query,
@@ -42,6 +43,7 @@ export const typeDefs = [
42
43
  Order,
43
44
  Cart,
44
45
  Status,
46
+ PropertyValue,
45
47
  ]
46
48
  .map(print)
47
49
  .join('\n')
@@ -16,6 +16,7 @@ type StoreProduct {
16
16
  review: [StoreReview!]!
17
17
  aggregateRating: StoreAggregateRating!
18
18
  isVariantOf: StoreProductGroup!
19
+ additionalProperty: [StorePropertyValue!]!
19
20
  }
20
21
 
21
22
  input IStoreProduct {
@@ -2,4 +2,5 @@ type StoreProductGroup {
2
2
  hasVariant: [StoreProduct!]!
3
3
  productGroupID: String!
4
4
  name: String!
5
+ additionalProperty: [StorePropertyValue!]!
5
6
  }
@@ -0,0 +1,4 @@
1
+ type StorePropertyValue {
2
+ value: String!
3
+ name: String!
4
+ }