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 24 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,5 +1,7 @@
## v2.0.0-dev (May 21, 2024)
- Upgrade to commerce-sdk-isomorphic v2.0.0 [#1794](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1794)
- 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
2 changes: 2 additions & 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,5 @@ 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'
export {useCustomMutation} from './useMutation'
10 changes: 10 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,12 @@ export type CacheUpdateMatrix<Client extends ApiClient> = {
? CacheUpdateGetter<MergedOptions<Client, Argument<Client[Method]>>, Data>
: never
}

type CustomEndpointArg = Parameters<typeof helpers.callCustomEndpoint>[0]
type CustomEndpointArgClientConfig = CustomEndpointArg['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 OptionalCustomEndpointClientConfig = Omit<CustomEndpointArg, 'clientConfig'> & {
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 {
ApiClient,
ApiMethod,
ApiOptions,
CacheUpdateGetter,
MergedOptions,
OptionalCustomEndpointClientConfig
} 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: OptionalCustomEndpointClientConfig,
mutationOptions?: UseMutationOptions
) => {
const config = useConfig()
const auth = useAuthContext()
const callCustomEndpointWithAuth = (options: OptionalCustomEndpointClientConfig) => {
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)
}
105 changes: 105 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,105 @@
/*
* 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: {
apiVersion: 'v1',
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: {
apiVersion: 'v1',
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: {
apiVersion: 'v1',
endpointPath,
apiName
}
},
rawResponse: false
})
})
await waitAndExpectSuccess(() => result.current)
expect(result.current.data).toEqual(mockRes)
})
})
Loading
Loading