Skip to content

Commit dad1974

Browse files
Feat: Expose a way to pass in custom fetch function (@W-19973347@) (#246)
* expose way to pass in custom fetch function * add unit test * update README * update CHANGELOG.md * remove unused import * bump size * update fetch implementation * fix unit test * update tests
1 parent 427d7ad commit dad1974

File tree

14 files changed

+189
-35
lines changed

14 files changed

+189
-35
lines changed

.github/workflows/publish-preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
run: |
2323
BASE_VERSION=$(node -p "require('./package.json').version.split('-')[0]")
2424
DATE=$(date -u +%Y%m%d%H%M%S)
25-
NEW_VERSION="$BASE_VERSION-nightly-$DATE"
25+
NEW_VERSION="$BASE_VERSION-unstable-$DATE"
2626
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
2727
npm version --no-git-tag-version "$NEW_VERSION"
2828
- run: yarn run renderTemplates

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Enhancements
66

7-
- Use native node fetch available in node 18+ instead of `node-fetch` polyfill [#214](https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/pull/214)
7+
- Allow developers to pass in custom fetch implementation via `clientConfig` [#246](https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/pull/246)
88
- Support subpath imports for individual APIs and named imports [#219](https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/pull/219)
99

1010
### Bug Fixes

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,45 @@ const helpers = require('commerce-sdk-isomorphic/helpers');
149149

150150
**Note:** While subpath imports reduce initial bundle size, using them for all APIs will result in a larger total bundle size due to duplicated dependencies required for standalone operation.
151151

152+
#### Custom Fetch function
153+
154+
You can provide your own custom fetch function to intercept, log, or modify all SDK requests. This is useful for:
155+
- **Request/Response Logging**: Track all API calls for debugging and monitoring
156+
- **Request Interception**: Add custom headers, modify request URLs, or implement custom retry logic
157+
- **Error Handling**: Add custom error processing or transformation before responses reach your application
158+
- **Performance Monitoring**: Measure request/response times and track API performance metrics
159+
160+
**Example with Logging:**
161+
```javascript
162+
// Custom fetch function with detailed logging
163+
const customFetch = async (url, options) => {
164+
console.log(`[SDK Request] ${options?.method || 'GET'} ${url}`);
165+
console.log('[SDK Request Headers]', options?.headers);
166+
if (options?.body) {
167+
console.log('[SDK Request Body]', options.body);
168+
}
169+
170+
const startTime = Date.now();
171+
const response = await fetch(url, options);
172+
const duration = Date.now() - startTime;
173+
174+
console.log(`[SDK Response] ${response.status} ${response.statusText} (${duration}ms)`);
175+
console.log('[SDK Response Headers]', Object.fromEntries(response.headers.entries()));
176+
177+
return response;
178+
};
179+
180+
const config = {
181+
parameters: {
182+
clientId: '<your-client-id>',
183+
organizationId: '<your-org-id>',
184+
shortCode: '<your-short-code>',
185+
siteId: '<your-site-id>',
186+
},
187+
fetch: customFetch,
188+
};
189+
```
190+
152191
#### Fetch Options
153192
154193
You can configure how the SDK makes requests using the `fetchOptions` parameter. It is passed to [node-fetch](https://github.com/node-fetch/node-fetch/1#api) on the server and [whatwg-fetch](https://github.github.io/fetch/) on browser.
@@ -340,6 +379,24 @@ console.log("categoriesResult: ", categoriesResult);
340379
341380
**NOTE: In the next major version release, path parameters will be single encoded by default**
342381
382+
## Unstable Releases
383+
384+
**⚠️ Important: Unstable/preview releases are experimental and not officially supported.**
385+
386+
Preview releases (e.g., preview, unstable, or pre-release versions) are provided for experimental purposes and early testing of upcoming features. These releases:
387+
388+
- **Are not intended for production use** - Do not use unstable releases in production environments
389+
- **May contain breaking changes** - API signatures, behavior, and structure may change without notice
390+
- **Are not officially supported** - No support, bug fixes, or security patches are guaranteed
391+
- **May have incomplete features** - Functionality may be partially implemented or subject to change
392+
393+
**Use stable releases for production applications.** Only use unstable releases for:
394+
- Testing upcoming features in development environments
395+
- Providing feedback on new functionality before official release
396+
- Experimental integrations that are not mission-critical
397+
398+
For production deployments, always use the latest stable release version available on npm.
399+
343400
## License Information
344401
345402
The Commerce SDK Isomorphic is licensed under BSD-3-Clause license. See the [license](./LICENSE.txt) for details.

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module.exports = {
1414
'!scripts/generateFileList.ts',
1515
'!scripts/updateApis.ts',
1616
'!scripts/generateVersionTable.ts',
17-
'!src/static/fileList.ts',
17+
'!scripts/fileList.ts',
1818
'!<rootDir>/node_modules/',
1919
],
2020
coverageReporters: ['text'],

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
},
168168
"dependencies": {
169169
"nanoid": "^3.3.8",
170+
"node-fetch": "2.6.13",
170171
"seedrandom": "^3.0.5"
171172
},
172173
"devDependencies": {
@@ -209,7 +210,6 @@
209210
"jest-environment-jsdom-sixteen": "1.0.3",
210211
"lint-staged": "10.5.4",
211212
"nock": "^13.2.8",
212-
"node-fetch": "2.6.13",
213213
"npm-pack-all": "^1.12.7",
214214
"postcss-preset-env": "6.7.1",
215215
"prettier": "^2.7.1",
@@ -250,7 +250,7 @@
250250
},
251251
{
252252
"path": "commerce-sdk-isomorphic-with-deps.tgz",
253-
"maxSize": "2.42 MB"
253+
"maxSize": "2.5 MB"
254254
}
255255
],
256256
"proxy": "https://SHORTCODE.api.commercecloud.salesforce.com"

scripts/utils.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,11 @@ describe('test downloadLatestApis script', () => {
3030
downloadLatestApis('category:Visibility = "External"', '/tmp')
3131
).rejects.toThrow('Failed to download API specs: It failed.');
3232
});
33+
34+
it('rejects with non-Error when download fails', async () => {
35+
jest.spyOn(download, 'downloadRestApis').mockRejectedValue('It failed.');
36+
await expect(
37+
downloadLatestApis('category:Visibility = "External"', '/tmp')
38+
).rejects.toBe('It failed.');
39+
});
3340
});

src/static/clientConfig.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import {BaseUriParameters} from 'lib/helpers';
8-
import ClientConfig, {ClientConfigInit} from './clientConfig';
8+
import ClientConfig, {ClientConfigInit, FetchFunction} from './clientConfig';
99

1010
describe('ClientConfig constructor', () => {
1111
test('will throw if missing shortCode parameter', () => {
@@ -26,6 +26,7 @@ describe('ClientConfig constructor', () => {
2626
proxy: 'https://proxy.com',
2727
transformRequest: ClientConfig.defaults.transformRequest,
2828
throwOnBadResponse: false,
29+
fetch: fetch as FetchFunction,
2930
};
3031
expect(new ClientConfig(init)).toEqual({...init});
3132
});

src/static/clientConfig.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ type BrowserRequestInit = RequestInit;
1818
*/
1919
export type FetchOptions = NodeRequestInit & BrowserRequestInit;
2020

21+
export type FetchFunction = (
22+
input: RequestInfo,
23+
init?: FetchOptions | undefined
24+
) => Promise<Response>;
25+
2126
/**
2227
* Base options that can be passed to the `ClientConfig` class.
2328
*/
@@ -27,18 +32,14 @@ export interface ClientConfigInit<Params extends BaseUriParameters> {
2732
headers?: {[key: string]: string};
2833
parameters: Params;
2934
fetchOptions?: FetchOptions;
35+
fetch?: FetchFunction;
3036
transformRequest?: (
3137
data: unknown,
3238
headers: {[key: string]: string}
3339
) => Required<FetchOptions>['body'];
3440
throwOnBadResponse?: boolean;
3541
}
3642

37-
export type FetchFunction = (
38-
input: RequestInfo,
39-
init?: FetchOptions | undefined
40-
) => Promise<Response>;
41-
4243
/**
4344
* Configuration parameters common to Commerce SDK clients
4445
*/
@@ -55,6 +56,8 @@ export default class ClientConfig<Params extends BaseUriParameters>
5556

5657
public fetchOptions: FetchOptions;
5758

59+
public fetch?: FetchFunction;
60+
5861
public transformRequest: NonNullable<
5962
ClientConfigInit<Params>['transformRequest']
6063
>;
@@ -75,6 +78,8 @@ export default class ClientConfig<Params extends BaseUriParameters>
7578
this.transformRequest =
7679
config.transformRequest || ClientConfig.defaults.transformRequest;
7780

81+
this.fetch = config.fetch;
82+
7883
// Optional properties
7984
if (config.baseUri) {
8085
this.baseUri = config.baseUri;

src/static/helpers/customApi.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ export const callCustomEndpoint = async (args: {
148148
// The resulting endpointPath will be: "actions/categories/Special%2CSummer"
149149
if (currentEndpointPath.includes('/')) {
150150
// Normalize endpoint path by removing multiple consecutive slashes
151-
const segments = currentEndpointPath.split('/').filter(segment => segment !== '');
151+
const segments = currentEndpointPath
152+
.split('/')
153+
.filter(segment => segment !== '');
152154
newEndpointPath = '';
153155
segments.forEach((segment: string, index: number) => {
154156
const key = `endpointPathSegment${index}`;

src/static/helpers/environment.test.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,43 @@
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
87
import nodeFetch from 'node-fetch';
98
import {isNode, fetch} from './environment';
109

1110
const TIMEOUT = 7000; // 7 seconds
1211

13-
/* Just testing the node environment, testing the browser environment is too complex within the test cases. */
12+
jest.mock('node-fetch', () => {
13+
const actual = jest.requireActual<typeof import('node-fetch')>('node-fetch');
14+
const impl = (actual.default ?? actual) as (
15+
...args: unknown[]
16+
) => Promise<unknown>;
17+
const mock = jest.fn((...args: unknown[]) => impl(...args));
18+
return Object.assign(mock, {default: mock});
19+
});
1420

21+
/* Just testing the node environment, testing the browser environment is too complex within the test cases. */
1522
describe('Fetch', () => {
16-
test('Runs node-fetch if node', () => {
23+
const nodeFetchMock = nodeFetch as unknown as jest.Mock;
24+
25+
beforeEach(() => {
26+
nodeFetchMock.mockClear();
27+
});
28+
29+
test('delegates to node-fetch in node environments', async () => {
1730
expect(isNode).toBe(true);
18-
expect(fetch).toBe(nodeFetch);
31+
await fetch('https://example.com');
32+
expect(nodeFetchMock).toHaveBeenCalled();
1933
});
2034
test('Make sure the fetch that is imported is actually a function and not an object', () => {
2135
expect(typeof fetch).toBe('function');
2236
});
37+
test('reuses the resolved polyfill after the first call', async () => {
38+
await fetch('https://example.com?first');
39+
expect(nodeFetchMock).toHaveBeenCalledTimes(1);
40+
41+
await fetch('https://example.com?second');
42+
expect(nodeFetchMock).toHaveBeenCalledTimes(2);
43+
});
2344
test(
2445
'fetch actually works',
2546
() => {

0 commit comments

Comments
 (0)