-
Notifications
You must be signed in to change notification settings - Fork 125
/
index.js
243 lines (216 loc) · 9.81 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/* eslint-disable no-unused-vars */
import * as sdk from 'commerce-sdk-isomorphic'
import {getAppOrigin} from 'pwa-kit-react-sdk/utils/url'
import {detectStorefrontPreview} from 'pwa-kit-react-sdk/ssr/universal/components/storefront-preview/utils'
import ShopperBaskets from './shopper-baskets'
import OcapiShopperOrders from './ocapi-shopper-orders'
import {isError} from './utils'
import Auth from './auth'
import EinsteinAPI from './einstein'
/**
* The configuration details for the connecting to the API.
* @typedef {Object} ClientConfig
* @property {string} [proxy] - URL to proxy fetch calls through.
* @property {string} [headers] - Request headers to be added to requests.
* @property {Object} [parameters] - API connection parameters for SDK.
* @property {string} [parameters.clientId]
* @property {string} [parameters.organizationId]
* @property {string} [parameters.shortCode]
* @property {string} [parameters.siteId]
* @property {string} [parameters.version]
*/
/**
* An object containing the customer's login credentials.
* @typedef {Object} CustomerCredentials
* @property {string} credentials.email
* @property {string} credentials.password
*/
/**
* Salesforce Customer object.
* {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/modules/shoppercustomers.html#customer}}
* @typedef {Object} Customer
*/
/**
* A wrapper class that proxies calls to the underlying commerce-sdk-isomorphic.
* The sdk class instances are created automatically with the given config.
*/
class CommerceAPI {
/**
* Create an instance of the API with the given config.
* @param {ClientConfig} config - The config used to instantiate SDK apis.
*/
constructor(config = {}) {
const {proxyPath, ...restConfig} = config
// Client-side requests should be proxied via the configured path.
const proxy = `${getAppOrigin()}${proxyPath}`
this._config = {proxy, ...restConfig}
this.auth = new Auth(this)
if (this._config.einsteinConfig?.einsteinId) {
this.einstein = new EinsteinAPI(this)
}
this.isStorefrontPreview = detectStorefrontPreview()
// A mapping of property names to the SDK class constructors we'll be
// providing instances for.
//
// NOTE: `sendLocale` and `sendCurrency` for sending locale and currency info to the API:
// - boolean, if you want to affect _all_ methods for a given API
// - OR an array (listing the API's methods), if you want to affect only certain methods of an API
const apiConfigs = {
shopperCustomers: {
api: sdk.ShopperCustomers,
sendLocale: false
},
shopperBaskets: {
api: ShopperBaskets,
sendLocale: false,
sendCurrency: ['createBasket']
},
shopperExperience: {
api: sdk.ShopperExperience
},
shopperGiftCertificates: {
api: sdk.ShopperGiftCertificates
},
shopperLogin: {api: sdk.ShopperLogin, sendLocale: false},
shopperOrders: {api: OcapiShopperOrders},
shopperProducts: {
api: sdk.ShopperProducts,
sendCurrency: ['getProduct', 'getProducts']
},
shopperPromotions: {
api: sdk.ShopperPromotions
},
shopperSearch: {
api: sdk.ShopperSearch,
sendCurrency: ['productSearch', 'getSearchSuggestions']
}
}
// Instantiate the SDK class proxies and create getters from our api mapping.
// The proxy handlers are called when accessing any of the mapped SDK class
// proxies, executing various pre-defined hooks for tapping into or modifying
// the outgoing method parameters and/or incoming SDK responses
const self = this
Object.keys(apiConfigs).forEach((key) => {
const SdkClass = apiConfigs[key].api
self._sdkInstances = {
...self._sdkInstances,
[key]: new Proxy(new SdkClass(this._config), {
get: function (obj, prop) {
if (typeof obj[prop] === 'function') {
return (...args) => {
const fetchOptions = args[0]
const {locale, currency} = self._config
if (fetchOptions.ignoreHooks) {
return obj[prop](...args)
}
// Inject the locale and currency to the API call via its parameters.
//
// NOTE: The commerce sdk isomorphic will complain if you pass parameters to
// it that it doesn't expect, this is why we only add the locale and currency
// to some of the API calls.
// By default we send the locale param and don't send the currency param.
const {sendLocale = true, sendCurrency = false} = apiConfigs[key]
const includeGlobalLocale = Array.isArray(sendLocale)
? sendLocale.includes(prop)
: !!sendLocale
const includeGlobalCurrency = Array.isArray(sendCurrency)
? sendCurrency.includes(prop)
: !!sendCurrency
fetchOptions.parameters = {
...(includeGlobalLocale ? {locale} : {}),
...(includeGlobalCurrency ? {currency} : {}),
// Allowing individual API calls to override the global locale/currency
...fetchOptions.parameters
}
return self.willSendRequest(prop, ...args).then((newArgs) => {
return obj[prop](...newArgs).then((res) =>
self.didReceiveResponse(res, newArgs)
)
})
}
}
return obj[prop]
}
})
}
Object.defineProperty(self, key, {
get() {
return self._sdkInstances[key]
}
})
})
this.getConfig = this.getConfig.bind(this)
}
/**
* Returns the api client configuration
* @returns {ClientConfig}
*/
getConfig() {
return this._config
}
/**
* Executed before every proxied method call to the SDK. Provides the method
* name and arguments. This can be overidden in a subclass to perform any
* logging or modifications to arguments before the request is sent.
* @param {string} methodName - The name of the sdk method that will be called.
* @param {...*} args - Original arguments for the SDK method.
* @returns {Promise<Array>} - Updated arguments that will be passed to the SDK method
*/
async willSendRequest(methodName, ...params) {
// We never need to modify auth request headers for these methods
if (
methodName === 'authenticateCustomer' ||
methodName === 'authorizeCustomer' ||
methodName === 'getAccessToken'
) {
return params
}
// If a login promise exists, we don't proceed unless it is resolved.
const pendingLogin = this.auth.pendingLogin
if (pendingLogin) {
await pendingLogin
}
// If the token is invalid (missing, past/nearing expiration), we issue
// a login call, which will attempt to refresh the token or get a new
// guest token. Once login is complete, we can proceed.
if (!this.auth.isTokenValid) {
// NOTE: Login will update `this.auth.authToken` with a fresh token
await this.auth.login()
}
// Apply the appropriate auth headers and return new options
const [fetchOptions, ...restParams] = params
const newFetchOptions = {
...fetchOptions,
headers: {...fetchOptions.headers, Authorization: this.auth.authToken},
// In Storefront Preview mode, add cache breaker for all SCAPI's requests.
// Otherwise, it's possible to get stale responses after the Shopper Context is set.
// (i.e. in this case, we optimize for accurate data, rather than performance/caching)
parameters: {
...fetchOptions.parameters,
...(this.isStorefrontPreview ? {c_cache_breaker: Date.now()} : {})
}
}
return [newFetchOptions, ...restParams]
}
/**
* Executed when receiving a response from an SDK request. The response data
* can be mutated or inspected before being passed back to the caller. Should
* be overidden in a subclass.
* @param {*} response - The response from the SDK method call.
* @param {Array} args - Original arguments for the SDK method.
* @returns {*} - The response to be passed back to original caller.
*/
didReceiveResponse(response, args) {
if (isError(response)) {
return {...response, isError: true, message: response.detail}
}
return response
}
}
export default CommerceAPI