-
Notifications
You must be signed in to change notification settings - Fork 130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[commerce-sdk-react] SCAPI Custom endpoint support #1793
Changes from 12 commits
9943fee
2cc05db
31a716c
15e0712
51c4fb7
6de641a
3ee10de
e2c4ab6
2c4551f
25fe405
5c51521
a3906c0
b32ec50
57d39cc
b988313
717109b
1cfc058
4c3ec0f
dfdd49c
08ad401
9ba471a
b2b3a69
0185552
08b8bd9
84b4b4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,7 @@ describe('Shopper Orders hooks', () => { | |
const unimplemented = getUnimplementedEndpoints(ShopperOrders, queries, mutations) | ||
// If this test fails: create a new query hook, add the endpoint to the mutations enum, | ||
// or add it to the `expected` array with a comment explaining "TODO" or "never" (and why). | ||
expect(unimplemented).toEqual([]) | ||
expect(unimplemented).toEqual(['guestOrderLookup']) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. new method came from |
||
}) | ||
test('all mutations have cache update logic', () => { | ||
// unimplemented = value in mutations enum, but no method in cache update matrix | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -18,6 +18,7 @@ import { | |||||||
ShopperSearch, | ||||||||
ShopperSeo | ||||||||
} from 'commerce-sdk-isomorphic' | ||||||||
import {helpers} from 'commerce-sdk-isomorphic' | ||||||||
|
||||||||
// --- GENERAL UTILITIES --- // | ||||||||
|
||||||||
|
@@ -208,3 +209,14 @@ export type CacheUpdateMatrix<Client extends ApiClient> = { | |||||||
? CacheUpdateGetter<MergedOptions<Client, Argument<Client[Method]>>, Data> | ||||||||
: never | ||||||||
} | ||||||||
|
||||||||
type CustomEndpointArg = Parameters<typeof helpers.callCustomEndpoint>[0] | ||||||||
type CustomEndpointArgClientConfig = Parameters< | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry i'm not quite understanding this diff, are you suggesting to rename This type is a partial of the entire param just to extract the client config. |
||||||||
typeof helpers.callCustomEndpoint | ||||||||
>[0]['clientConfig'] | ||||||||
// The commerce-sdk-isomorphic custom endpoint helper REQUIRES clientConfig as mandatory argument | ||||||||
// But we inject the configs for users from the provider, so this custom type is created | ||||||||
// to make clientConfig optional when calling useCustomQuery/useCustomMutation | ||||||||
export type CustomEndpointArgClientConfigOptional = Omit<CustomEndpointArg, 'clientConfig'> & { | ||||||||
kevinxh marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
clientConfig?: CustomEndpointArgClientConfig | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* | ||
* Copyright (c) 2024, Salesforce, 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 | ||
*/ | ||
|
||
import nock from 'nock' | ||
import {act} from '@testing-library/react' | ||
import { | ||
mockMutationEndpoints, | ||
renderHookWithProviders, | ||
waitAndExpectSuccess, | ||
DEFAULT_TEST_CONFIG | ||
} from '../test-utils' | ||
import {useCustomMutation} from './useMutation' | ||
|
||
jest.mock('../auth/index.ts', () => { | ||
const {default: mockAuth} = jest.requireActual('../auth/index.ts') | ||
mockAuth.prototype.ready = jest.fn().mockResolvedValue({access_token: 'access_token'}) | ||
return mockAuth | ||
}) | ||
|
||
describe('useCustomMutation', () => { | ||
beforeEach(() => nock.cleanAll()) | ||
test('useCustomMutation returns data on success', async () => { | ||
const mockRes = {data: '123'} | ||
const apiName = 'hello-world' | ||
mockMutationEndpoints(apiName, mockRes) | ||
const {result} = renderHookWithProviders(() => { | ||
const clientConfig = { | ||
parameters: { | ||
clientId: 'CLIENT_ID', | ||
siteId: 'SITE_ID', | ||
organizationId: 'ORG_ID', | ||
shortCode: 'SHORT_CODE' | ||
}, | ||
proxy: 'http://localhost:8888/mobify/proxy/api' | ||
} | ||
return useCustomMutation({ | ||
options: { | ||
method: 'POST', | ||
customApiPathParameters: { | ||
endpointPath: 'test-hello-world', | ||
apiName | ||
}, | ||
body: {test: '123'} | ||
}, | ||
clientConfig, | ||
rawResponse: false | ||
}) | ||
}) | ||
expect(result.current.data).toBeUndefined() | ||
act(() => result.current.mutate()) | ||
await waitAndExpectSuccess(() => result.current) | ||
expect(result.current.data).toEqual(mockRes) | ||
}) | ||
test('clientConfig is optional, default to CommerceApiProvider configs', async () => { | ||
const mockRes = {data: '123'} | ||
const apiName = 'hello-world' | ||
const endpointPath = 'test-hello-world' | ||
mockMutationEndpoints( | ||
`${apiName}/v1/organizations/${DEFAULT_TEST_CONFIG.organizationId}/${endpointPath}`, | ||
mockRes | ||
) | ||
const {result} = renderHookWithProviders(() => { | ||
return useCustomMutation({ | ||
options: { | ||
method: 'POST', | ||
customApiPathParameters: { | ||
endpointPath: 'test-hello-world', | ||
apiName | ||
}, | ||
body: {test: '123'} | ||
}, | ||
rawResponse: false | ||
}) | ||
}) | ||
expect(result.current.data).toBeUndefined() | ||
act(() => result.current.mutate()) | ||
await waitAndExpectSuccess(() => result.current) | ||
expect(result.current.data).toEqual(mockRes) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,22 @@ | |
* 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 | ||
*/ | ||
import {useMutation as useReactQueryMutation, useQueryClient} from '@tanstack/react-query' | ||
import {CacheUpdateGetter, ApiOptions, ApiMethod, ApiClient, MergedOptions} from './types' | ||
import { | ||
useMutation as useReactQueryMutation, | ||
useQueryClient, | ||
UseMutationOptions | ||
} from '@tanstack/react-query' | ||
import {helpers} from 'commerce-sdk-isomorphic' | ||
import useAuthContext from './useAuthContext' | ||
import useConfig from './useConfig' | ||
import { | ||
CacheUpdateGetter, | ||
ApiOptions, | ||
ApiMethod, | ||
ApiClient, | ||
MergedOptions, | ||
CustomEndpointArgClientConfigOptional | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: Might be an opportunity to sort this alphabetically. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds good |
||
} from './types' | ||
import {useAuthorizationHeader} from './useAuthorizationHeader' | ||
import useCustomerId from './useCustomerId' | ||
import {mergeOptions, updateCache} from './utils' | ||
|
@@ -38,3 +52,51 @@ export const useMutation = < | |
} | ||
}) | ||
} | ||
|
||
/** | ||
* A hook for SCAPI custom endpoint mutations. | ||
* | ||
* Besides calling custom endpoint, this hook does a few things for better DX. | ||
* 1. inject access token | ||
* 2. merge SCAPI client configurations from the CommerceApiProvider | ||
* @param apiOptions - Options passed through to commerce-sdk-isomorphic | ||
* @param mutationOptions - Options passed through to @tanstack/react-query | ||
* @returns A TanStack Query mutation hook with data from the custom API endpoint. | ||
*/ | ||
export const useCustomMutation = ( | ||
apiOptions: CustomEndpointArgClientConfigOptional, | ||
mutationOptions?: UseMutationOptions | ||
) => { | ||
const config = useConfig() | ||
const auth = useAuthContext() | ||
const callCustomEndpointWithAuth = (options: CustomEndpointArgClientConfigOptional) => { | ||
return async () => { | ||
const clientConfig = options.clientConfig || {} | ||
const clientHeaders = config.headers || {} | ||
const {access_token} = await auth.ready() | ||
return await helpers.callCustomEndpoint({ | ||
...options, | ||
options: { | ||
...options.options, | ||
headers: { | ||
Authorization: `Bearer ${access_token}`, | ||
...clientHeaders, | ||
...options.options?.headers | ||
} | ||
}, | ||
clientConfig: { | ||
parameters: { | ||
clientId: config.clientId, | ||
siteId: config.siteId, | ||
organizationId: config.organizationId, | ||
shortCode: config.organizationId | ||
}, | ||
proxy: config.proxy, | ||
...clientConfig | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This answers my previous question. 👍 |
||
} | ||
}) | ||
} | ||
} | ||
|
||
return useReactQueryMutation(callCustomEndpointWithAuth(apiOptions), mutationOptions) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* Copyright (c) 2024, Salesforce, 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 | ||
*/ | ||
|
||
import nock from 'nock' | ||
import { | ||
mockQueryEndpoint, | ||
renderHookWithProviders, | ||
waitAndExpectSuccess, | ||
DEFAULT_TEST_CONFIG | ||
} from '../test-utils' | ||
import {useCustomQuery} from './useQuery' | ||
|
||
jest.mock('../auth/index.ts', () => { | ||
const {default: mockAuth} = jest.requireActual('../auth/index.ts') | ||
mockAuth.prototype.ready = jest.fn().mockResolvedValue({access_token: 'access_token'}) | ||
return mockAuth | ||
}) | ||
|
||
describe('useCustomQuery', () => { | ||
beforeEach(() => nock.cleanAll()) | ||
afterEach(() => { | ||
expect(nock.pendingMocks()).toHaveLength(0) | ||
}) | ||
test('useCustomQuery returns data on success', async () => { | ||
const mockRes = {data: '123'} | ||
const apiName = 'hello-world' | ||
mockQueryEndpoint(apiName, mockRes) | ||
const {result} = renderHookWithProviders(() => { | ||
const clientConfig = { | ||
parameters: { | ||
clientId: 'CLIENT_ID', | ||
siteId: 'SITE_ID', | ||
organizationId: 'ORG_ID', | ||
shortCode: 'SHORT_CODE' | ||
}, | ||
proxy: 'http://localhost:8888/mobify/proxy/api' | ||
} | ||
return useCustomQuery({ | ||
options: { | ||
method: 'GET', | ||
customApiPathParameters: { | ||
endpointPath: 'test-hello-world', | ||
apiName | ||
} | ||
}, | ||
clientConfig, | ||
rawResponse: false | ||
}) | ||
}) | ||
await waitAndExpectSuccess(() => result.current) | ||
expect(result.current.data).toEqual(mockRes) | ||
}) | ||
test('clientConfig is optional, default to CommerceApiProvider configs', async () => { | ||
const mockRes = {data: '123'} | ||
const apiName = 'hello-world' | ||
const endpointPath = 'test-hello-world' | ||
mockQueryEndpoint( | ||
`${apiName}/v1/organizations/${DEFAULT_TEST_CONFIG.organizationId}/${endpointPath}`, | ||
mockRes | ||
) | ||
const {result} = renderHookWithProviders(() => { | ||
return useCustomQuery({ | ||
options: { | ||
method: 'GET', | ||
customApiPathParameters: { | ||
endpointPath, | ||
apiName | ||
} | ||
}, | ||
rawResponse: false | ||
}) | ||
}) | ||
await waitAndExpectSuccess(() => result.current) | ||
expect(result.current.data).toEqual(mockRes) | ||
}) | ||
test('query defaults to GET request', async () => { | ||
const mockRes = {data: '123'} | ||
const apiName = 'hello-world' | ||
const endpointPath = 'test-hello-world' | ||
mockQueryEndpoint( | ||
`${apiName}/v1/organizations/${DEFAULT_TEST_CONFIG.organizationId}/${endpointPath}`, | ||
mockRes | ||
) | ||
const {result} = renderHookWithProviders(() => { | ||
return useCustomQuery({ | ||
options: { | ||
customApiPathParameters: { | ||
endpointPath, | ||
apiName | ||
} | ||
}, | ||
rawResponse: false | ||
}) | ||
}) | ||
await waitAndExpectSuccess(() => result.current) | ||
expect(result.current.data).toEqual(mockRes) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deprecated method removed from
commerce-sdk-isomorphic@2