Skip to content
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

Merged
merged 25 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## v1.5.0-dev (Apr 17, 2024)
- Add `useCustomQuery` and `useCustomMutation` for SCAPI custom endpoint support [#1793](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1793)

## v1.4.2 (Apr 17, 2024)
- Update SLAS private proxy path [#1752](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1752)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ describe('Shopper Customers hooks', () => {
// 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([
'invalidateCustomerAuth', // DEPRECATED, not included
'authorizeCustomer', // DEPRECATED, not included
'authorizeTrustedSystem', // DEPRECATED, not included
Copy link
Collaborator Author

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

'registerExternalProfile', // TODO: Implement when the endpoint exits closed beta
'getExternalProfile' // TODO: Implement when the endpoint exits closed beta
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new method came from commerce-sdk-isomorphic@2

})
test('all mutations have cache update logic', () => {
// unimplemented = value in mutations enum, but no method in cache update matrix
Expand Down
1 change: 1 addition & 0 deletions packages/commerce-sdk-react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export {default as useEncUserId} from './useEncUserId'
export {default as useUsid} from './useUsid'
export {default as useCustomerId} from './useCustomerId'
export {default as useCustomerType} from './useCustomerType'
export {useCustomQuery} from './useQuery'
12 changes: 12 additions & 0 deletions packages/commerce-sdk-react/src/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ShopperSearch,
ShopperSeo
} from 'commerce-sdk-isomorphic'
import {helpers} from 'commerce-sdk-isomorphic'

// --- GENERAL UTILITIES --- //

Expand Down Expand Up @@ -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<
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type CustomEndpointArgClientConfig = Parameters<
type CustomEndpointParam = Parameters<typeof helpers.callCustomEndpoint>[0]
type CustomEndpointClientConfig = CustomEndpointParam['clientConfig']

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 CustomEndpointArgClientConfig to CustomEndpointParam?

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
}
84 changes: 84 additions & 0 deletions packages/commerce-sdk-react/src/hooks/useMutation.test.ts
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)
})
})
66 changes: 64 additions & 2 deletions packages/commerce-sdk-react/src/hooks/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Might be an opportunity to sort this alphabetically.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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'
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This answers my previous question. 👍

}
})
}
}

return useReactQueryMutation(callCustomEndpointWithAuth(apiOptions), mutationOptions)
}
102 changes: 102 additions & 0 deletions packages/commerce-sdk-react/src/hooks/useQuery.test.ts
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)
})
})
Loading
Loading