@faststore/api 1.10.30 → 1.11.3

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.
package/dist/index.d.ts CHANGED
@@ -71,6 +71,11 @@ export declare const getResolvers: (options: Options) => {
71
71
  }, unknown, any>>;
72
72
  StoreSearchResult: Record<string, import("./platforms/vtex").Resolver<Pick<import("./platforms/vtex/clients/search").SearchArgs, "sort" | "selectedFacets" | "hideUnavailableItems" | "query" | "page" | "count" | "fuzzy">, unknown, any>>;
73
73
  StorePropertyValue: Record<string, import("./platforms/vtex").Resolver<import("./__generated__/schema").IStorePropertyValue, unknown, any>>;
74
+ SkuVariants: Record<string, import("./platforms/vtex").Resolver<import("./platforms/vtex/clients/search/types/ProductSearchResult").Item & {
75
+ isVariantOf: import("./platforms/vtex/clients/search/types/ProductSearchResult").Product;
76
+ } & {
77
+ attachmentsValues?: import("./platforms/vtex/clients/commerce/types/OrderForm").Attachment[] | undefined;
78
+ }, unknown, any>>;
74
79
  ObjectOrString: import("graphql").GraphQLScalarType;
75
80
  Query: {
76
81
  product: (_: unknown, { locator }: import("./__generated__/schema").QueryProductArgs, ctx: import("./platforms/vtex").Context) => Promise<import("./platforms/vtex/utils/enhanceSku").EnhancedSku>;
@@ -101,6 +101,11 @@ export declare const getResolvers: (_: Options) => {
101
101
  }, unknown, any>>;
102
102
  StoreSearchResult: Record<string, Resolver<Pick<SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy">, unknown, any>>;
103
103
  StorePropertyValue: Record<string, Resolver<import("../..").IStorePropertyValue, unknown, any>>;
104
+ SkuVariants: Record<string, Resolver<import("./clients/search/types/ProductSearchResult").Item & {
105
+ isVariantOf: import("./clients/search/types/ProductSearchResult").Product;
106
+ } & {
107
+ attachmentsValues?: import("./clients/commerce/types/OrderForm").Attachment[] | undefined;
108
+ }, unknown, any>>;
104
109
  ObjectOrString: import("graphql").GraphQLScalarType;
105
110
  Query: {
106
111
  product: (_: unknown, { locator }: import("../..").QueryProductArgs, ctx: Context) => Promise<import("./utils/enhanceSku").EnhancedSku>;
@@ -0,0 +1,6 @@
1
+ import type { Resolver } from '..';
2
+ import type { PromiseType } from '../../../typings';
3
+ import type { StoreProduct } from './product';
4
+ declare type Root = PromiseType<ReturnType<typeof StoreProduct.isVariantOf>>;
5
+ export declare const SkuVariants: Record<string, Resolver<Root>>;
6
+ export {};
@@ -0,0 +1,15 @@
1
+ import { StoreProduct as StoreProductType } from '../../..';
2
+ import type { Product, Item } from '../clients/search/types/ProductSearchResult';
3
+ export declare type SkuVariants = StoreProductType[];
4
+ export declare type SkuVariantsByName = Record<string, Array<FormattedSkuVariant>>;
5
+ declare type FormattedSkuVariant = {
6
+ alt: string;
7
+ src: string;
8
+ label: string;
9
+ value: string;
10
+ };
11
+ export declare function createSlugsMap(variants: Item[], dominantVariantName: string, baseSlug: string): Record<string, string>;
12
+ export declare function getActiveSkuVariations(variations: Item['variations']): Record<string, string>;
13
+ export declare function getVariantsByName(skuSpecifications: Product['skuSpecifications']): Record<string, string[]>;
14
+ export declare function getFormattedVariations(variants: Item[], dominantVariantName: string, dominantVariantValue: string): Record<string, FormattedSkuVariant[]>;
15
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/api",
3
- "version": "1.10.30",
3
+ "version": "1.11.3",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "express-graphql": "^0.12.0",
38
38
  "graphql": "^15.6.0",
39
39
  "jest-transform-graphql": "^2.1.0",
40
- "shared": "^1.10.30",
40
+ "shared": "^1.10.34",
41
41
  "ts-jest": "25.5.1",
42
42
  "tsdx": "^0.14.1",
43
43
  "tslib": "^2.3.1",
@@ -46,5 +46,5 @@
46
46
  "peerDependencies": {
47
47
  "graphql": "^15.6.0"
48
48
  },
49
- "gitHead": "4541012c9a6b238f1082c2cb92c4d6410c663795"
49
+ "gitHead": "3e3bfc6bd9aa68ffc170b9979d5e3eeb5e06088e"
50
50
  }
@@ -9,7 +9,69 @@ export type Scalars = {
9
9
  Boolean: boolean;
10
10
  Int: number;
11
11
  Float: number;
12
+ /**
13
+ * Example:
14
+ *
15
+ * ```json
16
+ * {
17
+ * Color: 'Red', Size: '42'
18
+ * }
19
+ * ```
20
+ */
21
+ ActiveVariations: any;
22
+ /**
23
+ * Example:
24
+ *
25
+ * ```json
26
+ * {
27
+ * Color: [
28
+ * {
29
+ * src: "https://storecomponents.vtexassets.com/...",
30
+ * alt: "...",
31
+ * label: "...",
32
+ * value: "..."
33
+ * },
34
+ * {
35
+ * src: "https://storecomponents.vtexassets.com/...",
36
+ * alt: "...",
37
+ * label: "...",
38
+ * value: "..."
39
+ * }
40
+ * ],
41
+ * Size: [
42
+ * {
43
+ * src: "https://storecomponents.vtexassets.com/...",
44
+ * alt: "...",
45
+ * label: "...",
46
+ * value: "..."
47
+ * }
48
+ * ]
49
+ * }
50
+ * ```
51
+ */
52
+ FormattedVariants: any;
12
53
  ObjectOrString: any;
54
+ /**
55
+ * Example:
56
+ *
57
+ * ```json
58
+ * {
59
+ * 'Color-Red-Size-40': 'classic-shoes-37'
60
+ * }
61
+ * ```
62
+ */
63
+ SlugsMap: any;
64
+ /**
65
+ * Example:
66
+ *
67
+ * ```json
68
+ * {
69
+ * Color: [ "Red", "Blue", "Green" ],
70
+ * Size: [ "40", "41" ]
71
+ * }
72
+ * ```
73
+ */
74
+ VariantsByName: any;
13
75
  };
14
76
 
15
77
  /** Person data input to the newsletter. */
@@ -206,6 +268,37 @@ export type QuerySearchArgs = {
206
268
  term?: Maybe<Scalars['String']>;
207
269
  };
208
270
 
271
+ export type SkuVariants = {
272
+ __typename?: 'SkuVariants';
273
+ /** SKU property values for the current SKU. */
274
+ activeVariations?: Maybe<Scalars['ActiveVariations']>;
275
+ /** All available options for each SKU variant property, indexed by their name. */
276
+ allVariantsByName?: Maybe<Scalars['VariantsByName']>;
277
+ /**
278
+ * Available options for each varying SKU property, taking into account the
279
+ * `dominantVariantName` property. Returns all available options for the
280
+ * dominant property, and only options that can be combined with its current
281
+ * value for other properties.
282
+ */
283
+ availableVariations?: Maybe<Scalars['FormattedVariants']>;
284
+ /**
285
+ * Maps property value combinations to their respective SKU's slug. Enables
286
+ * us to retrieve the slug for the SKU that matches the currently selected
287
+ * variations in O(1) time.
288
+ */
289
+ slugsMap?: Maybe<Scalars['SlugsMap']>;
290
+ };
291
+
292
+
293
+ export type SkuVariantsAvailableVariationsArgs = {
294
+ dominantVariantName: Scalars['String'];
295
+ };
296
+
297
+
298
+ export type SkuVariantsSlugsMapArgs = {
299
+ dominantVariantName: Scalars['String'];
300
+ };
301
+
209
302
  /** Aggregate offer information, for a given SKU that is available to be fulfilled by multiple sellers. */
210
303
  export type StoreAggregateOffer = {
211
304
  __typename?: 'StoreAggregateOffer';
@@ -556,6 +649,12 @@ export type StoreProductGroup = {
556
649
  name: Scalars['String'];
557
650
  /** Product group ID. */
558
651
  productGroupID: Scalars['String'];
652
+ /**
653
+ * Object containing data structures to facilitate handling different SKU
654
+ * variant properties. Specially useful for implementing SKU selection
655
+ * components.
656
+ */
657
+ skuVariants?: Maybe<SkuVariants>;
559
658
  };
560
659
 
561
660
  /** Properties that can be associated with products and products groups. */
@@ -20,6 +20,7 @@ import { Query } from './resolvers/query'
20
20
  import { StoreReview } from './resolvers/review'
21
21
  import { StoreSearchResult } from './resolvers/searchResult'
22
22
  import { StoreSeo } from './resolvers/seo'
23
+ import { SkuVariants } from './resolvers/skuVariations'
23
24
  import ChannelMarshal from './utils/channel'
24
25
  import type { Loaders } from './loaders'
25
26
  import type { Clients } from './clients'
@@ -80,6 +81,7 @@ const Resolvers = {
80
81
  StoreProductGroup,
81
82
  StoreSearchResult,
82
83
  StorePropertyValue,
84
+ SkuVariants,
83
85
  ObjectOrString,
84
86
  Query,
85
87
  Mutation,
@@ -11,8 +11,11 @@ const isBrand = (x: any): x is Brand | CollectionPageType =>
11
11
  x.type === 'brand' ||
12
12
  (isCollectionPageType(x) && x.pageType.toLowerCase() === 'brand')
13
13
 
14
+ const isCollection = (x: Root): x is CollectionPageType =>
15
+ isCollectionPageType(x) && x.pageType.toLowerCase() === 'collection'
16
+
14
17
  const slugifyRoot = (root: Root) => {
15
- if (isBrand(root)) {
18
+ if (isBrand(root) || isCollection(root)) {
16
19
  return slugify(root.name)
17
20
  }
18
21
 
@@ -12,15 +12,16 @@ export const StoreProductGroup: Record<string, Resolver<Root>> = {
12
12
  hasVariant: (root) =>
13
13
  root.isVariantOf.items.map((item) => enhanceSku(item, root.isVariantOf)),
14
14
  productGroupID: ({ isVariantOf }) => isVariantOf.productId,
15
- name: ({ isVariantOf }) => isVariantOf.productName,
15
+ name: (root) => root.isVariantOf.productName,
16
+ skuVariants: (root) => root,
16
17
  additionalProperty: ({ isVariantOf: { specificationGroups } }) =>
17
18
  specificationGroups
18
- // filter sku specifications so we dont mess sku with product specs
19
+ // Filter sku specifications so we don't mix them with product specs.
19
20
  .filter(
20
21
  (specificationGroup) =>
21
22
  !BLOCKED_SPECIFICATIONS.has(specificationGroup.name)
22
23
  )
23
- // Transform specs back into product specs
24
+ // Transform specs back into product specs.
24
25
  .flatMap(({ specifications }) =>
25
26
  specifications.flatMap(({ name, values }) =>
26
27
  values.map((value) => ({
@@ -0,0 +1,47 @@
1
+ import type { Resolver } from '..'
2
+ import type { PromiseType } from '../../../typings'
3
+ import type { StoreProduct } from './product'
4
+ import {
5
+ createSlugsMap,
6
+ getActiveSkuVariations,
7
+ getFormattedVariations,
8
+ getVariantsByName,
9
+ } from '../utils/skuVariants'
10
+
11
+ type Root = PromiseType<ReturnType<typeof StoreProduct.isVariantOf>>
12
+
13
+ type SlugsMapArgs = {
14
+ dominantVariantName: string
15
+ }
16
+
17
+ export const SkuVariants: Record<string, Resolver<Root>> = {
18
+ activeVariations: (root) => getActiveSkuVariations(root.variations),
19
+ allVariantsByName: (root) =>
20
+ getVariantsByName(root.isVariantOf.skuSpecifications),
21
+
22
+ slugsMap: (root, args) =>
23
+ createSlugsMap(
24
+ root.isVariantOf.items,
25
+ // Since `dominantVariantProperty` is a required argument, we can safely
26
+ // access it.
27
+ (args as SlugsMapArgs).dominantVariantName,
28
+ root.isVariantOf.linkText
29
+ ),
30
+
31
+ availableVariations: (root, args) => {
32
+ // Since `dominantVariantProperty` is a required argument, we can safely
33
+ // access it.
34
+ const dominantVariantName = (args as SlugsMapArgs).dominantVariantName
35
+ const activeVariations = getActiveSkuVariations(root.variations)
36
+
37
+ const activeDominantVariationValue = activeVariations[dominantVariantName]
38
+
39
+ const filteredFormattedVariations = getFormattedVariations(
40
+ root.isVariantOf.items,
41
+ dominantVariantName,
42
+ activeDominantVariationValue
43
+ )
44
+
45
+ return filteredFormattedVariations
46
+ },
47
+ }
@@ -1,6 +1,12 @@
1
1
  import deepEquals from 'fast-deep-equal'
2
2
 
3
3
  import { md5 } from '../utils/md5'
4
+ import {
5
+ attachmentToPropertyValue,
6
+ getPropertyId,
7
+ VALUE_REFERENCES,
8
+ } from '../utils/propertyValue'
9
+
4
10
  import type {
5
11
  IStoreCart,
6
12
  IStoreOffer,
@@ -13,12 +19,6 @@ import type {
13
19
  OrderFormItem,
14
20
  } from '../clients/commerce/types/OrderForm'
15
21
  import type { Context } from '..'
16
- import {
17
- attachmentToPropertyValue,
18
- getPropertyId,
19
- VALUE_REFERENCES,
20
- } from '../utils/propertyValue'
21
-
22
22
  type Indexed<T> = T & { index?: number }
23
23
 
24
24
  const isAttachment = (value: IStorePropertyValue) =>
@@ -28,7 +28,7 @@ const getId = (item: IStoreOffer) =>
28
28
  [
29
29
  item.itemOffered.sku,
30
30
  item.seller.identifier,
31
- item.price,
31
+ item.price < 0.01 ? 'Gift' : undefined,
32
32
  item.itemOffered.additionalProperty
33
33
  ?.filter(isAttachment)
34
34
  .map(getPropertyId)
@@ -39,7 +39,7 @@ const getId = (item: IStoreOffer) =>
39
39
 
40
40
  const orderFormItemToOffer = (
41
41
  item: OrderFormItem,
42
- index?: number
42
+ index?: number,
43
43
  ): Indexed<IStoreOffer> => ({
44
44
  listPrice: item.listPrice / 100,
45
45
  price: item.sellingPrice / 100,
@@ -55,7 +55,7 @@ const orderFormItemToOffer = (
55
55
  })
56
56
 
57
57
  const offerToOrderItemInput = (
58
- offer: Indexed<IStoreOffer>
58
+ offer: Indexed<IStoreOffer>,
59
59
  ): OrderFormInputItem => ({
60
60
  quantity: offer.quantity,
61
61
  seller: offer.seller.identifier,
@@ -69,14 +69,18 @@ const offerToOrderItemInput = (
69
69
  })),
70
70
  })
71
71
 
72
- const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer> =>
72
+ const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer[]> =>
73
73
  offers.reduce((acc, item) => {
74
74
  const id = getId(item)
75
75
 
76
- acc.set(id, acc.get(id) ?? item)
76
+ if (!acc.has(id)) {
77
+ acc.set(id, [])
78
+ }
79
+
80
+ acc.get(id)?.push(item)
77
81
 
78
82
  return acc
79
- }, new Map<string, IStoreOffer>())
83
+ }, new Map<string, IStoreOffer[]>())
80
84
 
81
85
  const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
82
86
  const pick = (item: Indexed<IStoreOffer>, index: number) => ({
@@ -96,9 +100,42 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
96
100
  return isSameOrder && orderItemsAreSync
97
101
  }
98
102
 
103
+ const joinItems = (form: OrderForm) => {
104
+ const itemsById = form.items
105
+ .reduce((acc, item) => {
106
+ const id = getId(orderFormItemToOffer(item))
107
+
108
+ if (!acc[id]) {
109
+ acc[id] = []
110
+ }
111
+
112
+ acc[id].push(item)
113
+
114
+ return acc
115
+ }, {} as Record<string, OrderFormItem[]>)
116
+
117
+ return {
118
+ ...form,
119
+ items: Object.values(itemsById).map((items) => {
120
+ const [item] = items
121
+ const quantity = items.reduce((acc, i) => acc + i.quantity, 0)
122
+ const totalPrice = items.reduce(
123
+ (acc, i) => acc + i.quantity * i.sellingPrice,
124
+ 0,
125
+ )
126
+
127
+ return {
128
+ ...item,
129
+ quantity,
130
+ sellingPrice: totalPrice / quantity,
131
+ }
132
+ }),
133
+ }
134
+ }
135
+
99
136
  const orderFormToCart = async (
100
137
  form: OrderForm,
101
- skuLoader: Context['loaders']['skuLoader']
138
+ skuLoader: Context['loaders']['skuLoader'],
102
139
  ) => {
103
140
  return {
104
141
  order: {
@@ -119,7 +156,7 @@ const getOrderFormEtag = ({ items }: OrderForm) => md5(JSON.stringify(items))
119
156
 
120
157
  const setOrderFormEtag = async (
121
158
  form: OrderForm,
122
- commerce: Context['clients']['commerce']
159
+ commerce: Context['clients']['commerce'],
123
160
  ) => {
124
161
  try {
125
162
  const orderForm = await commerce.checkout.setCustomData({
@@ -132,7 +169,7 @@ const setOrderFormEtag = async (
132
169
  return orderForm
133
170
  } catch (err) {
134
171
  console.error(
135
- 'Error while setting custom data to orderForm.\n Make sure to add the following custom app to the orderForm: \n{"fields":["cartEtag"],"id":"faststore","major":1}.\n More info at: https://developers.vtex.com/vtex-rest-api/docs/customizable-fields-with-checkout-api'
172
+ 'Error while setting custom data to orderForm.\n Make sure to add the following custom app to the orderForm: \n{"fields":["cartEtag"],"id":"faststore","major":1}.\n More info at: https://developers.vtex.com/vtex-rest-api/docs/customizable-fields-with-checkout-api',
136
173
  )
137
174
 
138
175
  throw err
@@ -146,7 +183,7 @@ const setOrderFormEtag = async (
146
183
  */
147
184
  const isOrderFormStale = (form: OrderForm) => {
148
185
  const faststoreData = form.customData?.customApps.find(
149
- (app) => app.id === 'faststore'
186
+ (app) => app.id === 'faststore',
150
187
  )
151
188
 
152
189
  const oldEtag = faststoreData?.fields?.cartEtag
@@ -176,7 +213,7 @@ const isOrderFormStale = (form: OrderForm) => {
176
213
  export const validateCart = async (
177
214
  _: unknown,
178
215
  { cart: { order } }: { cart: IStoreCart },
179
- ctx: Context
216
+ ctx: Context,
180
217
  ) => {
181
218
  const { enableOrderFormSync } = ctx.storage.flags
182
219
  const { orderNumber, acceptedOffer } = order
@@ -197,7 +234,9 @@ export const validateCart = async (
197
234
  const isStale = isOrderFormStale(orderForm)
198
235
 
199
236
  if (isStale === true && orderNumber) {
200
- const newOrderForm = await setOrderFormEtag(orderForm, commerce)
237
+ const newOrderForm = await setOrderFormEtag(orderForm, commerce).then(
238
+ joinItems,
239
+ )
201
240
 
202
241
  return orderFormToCart(newOrderForm, skuLoader)
203
242
  }
@@ -206,37 +245,54 @@ export const validateCart = async (
206
245
  // Step2: Process items from both browser and checkout so they have the same shape
207
246
  const browserItemsById = groupById(acceptedOffer)
208
247
  const originItemsById = groupById(orderForm.items.map(orderFormItemToOffer))
209
- const browserItems = Array.from(browserItemsById.values()) // items on the user's browser
210
- const originItems = Array.from(originItemsById.values()) // items on the VTEX platform backend
248
+ const originItems = Array.from(originItemsById.entries()); // items on the VTEX platform backend
249
+ const browserItems = Array.from(browserItemsById.entries()); // items on the user's browser
211
250
 
212
251
  // Step3: Compute delta changes
213
- const { itemsToAdd, itemsToUpdate } = browserItems.reduce(
214
- (acc, item) => {
215
- const maybeOriginItem = originItemsById.get(getId(item))
216
-
217
- if (!maybeOriginItem) {
218
- acc.itemsToAdd.push(item)
219
- } else {
252
+ const { itemsToAdd, itemsToUpdate } = browserItems
253
+ .reduce(
254
+ (acc, [id, items]) => {
255
+ const maybeOriginItem = originItemsById.get(id)
256
+
257
+ // Adding new items to cart
258
+ if (!maybeOriginItem) {
259
+ items.forEach((item) => acc.itemsToAdd.push(item))
260
+
261
+ return acc
262
+ }
263
+
264
+ // Update existing items
265
+ const [head, ...tail] = maybeOriginItem
266
+ const totalQuantity = items.reduce(
267
+ (acc, curr) => acc + curr.quantity,
268
+ 0,
269
+ )
270
+
271
+ // set total quantity to first item
220
272
  acc.itemsToUpdate.push({
221
- ...maybeOriginItem,
222
- quantity: item.quantity,
273
+ ...head,
274
+ quantity: totalQuantity,
223
275
  })
224
- }
225
276
 
226
- return acc
227
- },
228
- {
229
- itemsToAdd: [] as IStoreOffer[],
230
- itemsToUpdate: [] as IStoreOffer[],
231
- }
232
- )
277
+ // Remove all the rest
278
+ tail.forEach((item) =>
279
+ acc.itemsToUpdate.push({ ...item, quantity: 0 })
280
+ )
281
+
282
+ return acc
283
+ },
284
+ {
285
+ itemsToAdd: [] as IStoreOffer[],
286
+ itemsToUpdate: [] as IStoreOffer[],
287
+ },
288
+ )
233
289
 
234
290
  const itemsToDelete = originItems
235
- .filter((item) => !browserItemsById.has(getId(item)))
236
- .map((item) => ({ ...item, quantity: 0 }))
291
+ .filter(([id]) => !browserItemsById.has(id))
292
+ .flatMap(([, items]) => items.map((item) => ({ ...item, quantity: 0 })))
237
293
 
238
294
  const changes = [...itemsToAdd, ...itemsToUpdate, ...itemsToDelete].map(
239
- offerToOrderItemInput
295
+ offerToOrderItemInput,
240
296
  )
241
297
 
242
298
  if (changes.length === 0) {
@@ -254,6 +310,7 @@ export const validateCart = async (
254
310
  .then((form) =>
255
311
  enableOrderFormSync ? setOrderFormEtag(form, commerce) : form
256
312
  )
313
+ .then(joinItems)
257
314
 
258
315
  // Step5: If no changes detected before/after updating orderForm, the order is validated
259
316
  if (equals(order, updatedOrderForm)) {