From 465bf53cbd7146fdc5890ecb4bc7f8933158a01e Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 3 Feb 2023 16:29:24 -0500 Subject: [PATCH 001/122] Merge hooks changes from wjh/simplify-hooks --- .../src/hooks/ShopperBaskets/config.ts | 79 +++ .../src/hooks/ShopperBaskets/index.ts | 2 +- .../src/hooks/ShopperBaskets/mutation.test.ts | 56 +-- .../src/hooks/ShopperBaskets/mutation.ts | 470 +----------------- .../src/hooks/ShopperBaskets/query.ts | 294 ++++++----- .../src/hooks/Test/config.ts | 79 +++ .../src/hooks/Test/index.ts | 8 + .../src/hooks/Test/mutation.ts | 65 +++ .../src/hooks/Test/query.ts | 234 +++++++++ .../commerce-sdk-react/src/hooks/Test/temp.ts | 40 ++ .../commerce-sdk-react/src/hooks/types.ts | 97 ++-- .../src/hooks/useAuthorizationHeader.ts | 24 + .../src/hooks/useMutation.ts | 30 +- .../commerce-sdk-react/src/hooks/useQuery.ts | 45 +- .../commerce-sdk-react/src/hooks/utils.ts | 98 ++-- 15 files changed, 908 insertions(+), 713 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts create mode 100644 packages/commerce-sdk-react/src/hooks/Test/config.ts create mode 100644 packages/commerce-sdk-react/src/hooks/Test/index.ts create mode 100644 packages/commerce-sdk-react/src/hooks/Test/mutation.ts create mode 100644 packages/commerce-sdk-react/src/hooks/Test/query.ts create mode 100644 packages/commerce-sdk-react/src/hooks/Test/temp.ts create mode 100644 packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts new file mode 100644 index 0000000000..f7b9b858cb --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023, 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 {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' +import {ApiClients, CacheUpdateMatrix} from '../types' +import {CacheUpdateMatrixElement} from '../utils' + +const updateBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { + // TODO: we're missing headers, rawResponse -> not only {basketId} + const arg = {basketId} + return basketId + ? { + update: [['/baskets', basketId, arg]] + } + : {} +} + +const removeBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { + const arg = {basketId} + return basketId + ? { + remove: [['/baskets', basketId, arg]] + } + : {} +} + +const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdateMatrixElement => { + // TODO: should we use arg here or not? The invalidate method does not need exact query key. + const arg = {customerId} + return customerId + ? { + invalidate: [['/customers', customerId, '/baskets', arg]] + } + : {} +} + +const updateBasketFromRequest = ( + customerId: string | null, + params: { + parameters: ShopperBasketsTypes.Basket + } +): CacheUpdateMatrixElement => ({ + ...updateBasketQuery(params.parameters.basketId), + ...invalidateCustomerBasketsQuery(customerId) +}) + +const updateBasketFromResponse = ( + customerId: string | null, + params: unknown, // not used, + response: ShopperBasketsTypes.Basket +): CacheUpdateMatrixElement => ({ + ...updateBasketQuery(response.basketId), + ...invalidateCustomerBasketsQuery(customerId) +}) + +export const cacheUpdateMatrix: CacheUpdateMatrix = { + addCouponToBasket: updateBasketFromRequest, + addItemToBasket: updateBasketFromRequest, + removeItemFromBasket: updateBasketFromRequest, + addPaymentInstrumentToBasket: updateBasketFromRequest, + createBasket: updateBasketFromResponse, // Response! + deleteBasket: (customerId, params): CacheUpdateMatrixElement => ({ + ...invalidateCustomerBasketsQuery(customerId), + ...removeBasketQuery(params.parameters.basketId) + }), + mergeBasket: updateBasketFromResponse, // Response! + removeCouponFromBasket: updateBasketFromRequest, + removePaymentInstrumentFromBasket: updateBasketFromRequest, + updateBasket: updateBasketFromRequest, + updateBillingAddressForBasket: updateBasketFromRequest, + updateCustomerForBasket: updateBasketFromRequest, + updateItemInBasket: updateBasketFromRequest, + updatePaymentInstrumentInBasket: updateBasketFromRequest, + updateShippingAddressForShipment: updateBasketFromRequest, + updateShippingMethodForShipment: updateBasketFromRequest +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.ts index 388e07d6ee..1a13c64feb 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 28788ff7b9..8200156e55 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 @@ -13,6 +13,8 @@ import { assertUpdateQuery, DEFAULT_TEST_HOST, mockMutationEndpoints, + NEW_DATA, + OLD_DATA, renderHookWithProviders } from '../../test-utils' import { @@ -21,7 +23,7 @@ import { useShopperBasketsMutation } from './mutation' import {useBasket} from './query' -import {useCustomerBaskets} from '../ShopperCustomers' +import {useCustomerBaskets} from '../ShopperCustomers/query' import {CacheUpdateMatrixElement} from '../utils' const CUSTOMER_ID = 'CUSTOMER_ID' @@ -42,10 +44,7 @@ jest.mock('../useCustomerId.ts', () => { return jest.fn().mockReturnValue(CUSTOMER_ID) }) -type MutationPayloads = { - [key in ShopperBasketsMutationType]?: {body: any; parameters: any} -} -const mutationPayloads: MutationPayloads = { +const mutationPayloads = { updateBasket: { parameters: {basketId: BASKET_ID}, body: {} @@ -110,27 +109,9 @@ const mutationPayloads: MutationPayloads = { parameters: {basketId: BASKET_ID, shipmentId: SHIPMENT_ID}, body: {id: '001'} } -} -const oldCustomerBaskets = { - total: 1, - baskets: [{basketId: BASKET_ID, hello: 'world'}] -} - -const newCustomerBaskets = { - total: 1, - baskets: [{basketId: BASKET_ID, hello: 'world_modified'}] -} +} as const -const oldBasket = { - basketId: BASKET_ID, - hello: 'world' -} - -const newBasket = { - basketId: BASKET_ID, - hello: 'world_modified' -} -const tests = (Object.keys(mutationPayloads) as ShopperBasketsMutationType[]).map( +const tests = (Object.keys(mutationPayloads) as Array).map( (mutationName) => { const payload = mutationPayloads[mutationName] @@ -140,11 +121,7 @@ const tests = (Object.keys(mutationPayloads) as ShopperBasketsMutationType[]).ma { name: 'success', assertions: async () => { - mockMutationEndpoints( - '/checkout/shopper-baskets/', - {errorResponse: 200}, - newBasket - ) + mockMutationEndpoints('/checkout/shopper-baskets/') mockRelatedQueries() const {result, waitForValueToChange} = renderHookWithProviders(() => { @@ -171,24 +148,21 @@ const tests = (Object.keys(mutationPayloads) as ShopperBasketsMutationType[]).ma await waitForValueToChange(() => result.current.mutation.isSuccess) expect(result.current.mutation.isSuccess).toBe(true) + // On successful mutation, the query cache gets updated too. Let's assert it. const cacheUpdateMatrix = getCacheUpdateMatrix(CUSTOMER_ID) // @ts-ignore const matrixElement = cacheUpdateMatrix[mutationName](payload, {}) const {invalidate, update, remove}: CacheUpdateMatrixElement = matrixElement - const assertionData = { - basket: newBasket, - customerBaskets: newCustomerBaskets - } update?.forEach(({name}) => { // @ts-ignore - assertUpdateQuery(result.current.queries[name], assertionData[name]) + assertUpdateQuery(result.current.queries[name], NEW_DATA) }) invalidate?.forEach(({name}) => { // @ts-ignore - assertInvalidateQuery(result.current.queries[name], oldCustomerBaskets) + assertInvalidateQuery(result.current.queries[name], OLD_DATA) }) remove?.forEach(({name}) => { @@ -244,24 +218,24 @@ const mockRelatedQueries = () => { .get((uri) => { return uri.includes(basketEndpoint) }) - .reply(200, oldBasket) + .reply(200, OLD_DATA) nock(DEFAULT_TEST_HOST) .persist() .get((uri) => { return uri.includes(basketEndpoint) }) - .reply(200, newBasket) + .reply(200, NEW_DATA) // For get customer basket nock(DEFAULT_TEST_HOST) .get((uri) => { return uri.includes(customerEndpoint) }) - .reply(200, oldCustomerBaskets) + .reply(200, OLD_DATA) nock(DEFAULT_TEST_HOST) .persist() .get((uri) => { return uri.includes(customerEndpoint) }) - .reply(200, newCustomerBaskets) + .reply(200, NEW_DATA) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts index 0925c06668..1831a49bc1 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts @@ -1,483 +1,65 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType} from '../types' +import {ApiClients, Argument, DataType, ApiMethod} from '../types' import {useMutation} from '../useMutation' -import {MutationFunction, UseMutationResult, useQueryClient} from '@tanstack/react-query' -import {CacheUpdateMatrixElement, NotImplementedError, updateCache} from '../utils' -import useCustomerId from '../useCustomerId' +import {UseMutationResult} from '@tanstack/react-query' +import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' type Client = ApiClients['shopperBaskets'] -type CustomerClient = ApiClients['shopperCustomers'] export const ShopperBasketsMutations = { - /** - * Creates a new basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=createBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#createbasket} for more information on the parameters and returned data type. - */ CreateBasket: 'createBasket', - /** - * Transfer the previous shopper's basket to the current shopper by updating the basket's owner. No other values change. You must obtain the shopper authorization token via SLAS, and it must contain both the previous and current shopper IDs. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=mergeBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#mergebasket} for more information on the parameters and returned data type. - */ TransferBasket: 'transferBasket', - /** - * Merge data from the previous shopper's basket into the current shopper's active basket and delete the previous shopper's basket. This endpoint doesn't merge Personally Identifiable Information (PII). You must obtain the shopper authorization token via SLAS, and it must contain both the previous and current shopper IDs. After the merge, all basket amounts are recalculated and totaled, including lookups for prices, taxes, shipping, and promotions. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=mergeBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#mergebasket} for more information on the parameters and returned data type. - */ MergeBasket: 'mergeBasket', - /** - * Removes a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=deleteBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#deletebasket} for more information on the parameters and returned data type. - */ DeleteBasket: 'deleteBasket', - /** - * Updates a basket. Only the currency of the basket, source code, the custom properties of the basket, and the shipping items will be considered. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatebasket} for more information on the parameters and returned data type. - */ UpdateBasket: 'updateBasket', - /** - * Sets the billing address of a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateBillingAddressForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatebillingaddressforbasket} for more information on the parameters and returned data type. - */ UpdateBillingAddressForBasket: 'updateBillingAddressForBasket', - /** - * Adds a coupon to an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addCouponToBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addcoupontobasket} for more information on the parameters and returned data type. - */ AddCouponToBasket: 'addCouponToBasket', - /** - * Removes a coupon from the basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeCouponFromBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removecouponfrombasket} for more information on the parameters and returned data type. - */ RemoveCouponFromBasket: 'removeCouponFromBasket', - /** - * Sets customer information for an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateCustomerForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatecustomerforbasket} for more information on the parameters and returned data type. - */ UpdateCustomerForBasket: 'updateCustomerForBasket', - /** - * - * * WARNING: This method is not implemented. - * - * Adds a gift certificate item to an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addGiftCertificateItemToBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addgiftcertificateitemtobasket} for more information on the parameters and returned data type. - */ AddGiftCertificateItemToBasket: 'addGiftCertificateItemToBasket', - /** - * Deletes a gift certificate item from an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeGiftCertificateItemFromBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removegiftcertificateitemfrombasket} for more information on the parameters and returned data type. - */ RemoveGiftCertificateItemFromBasket: 'removeGiftCertificateItemFromBasket', - /** - * Updates a gift certificate item of an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateGiftCertificateItemInBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updategiftcertificateiteminbasket} for more information on the parameters and returned data type. - */ UpdateGiftCertificateItemInBasket: 'updateGiftCertificateItemInBasket', - /** - * Adds new items to a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addItemToBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#additemtobasket} for more information on the parameters and returned data type. - */ AddItemToBasket: 'addItemToBasket', - /** - * Removes a product item from the basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeItemFromBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removeitemfrombasket} for more information on the parameters and returned data type. - */ RemoveItemFromBasket: 'removeItemFromBasket', - /** - * Updates an item in a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateItemInBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateiteminbasket} for more information on the parameters and returned data type. - */ UpdateItemInBasket: 'updateItemInBasket', - /** - * This method allows you to apply external taxation data to an existing basket to be able to pass tax rates and optional values for a specific taxable line item. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addTaxesForBasketItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addtaxesforbasketitem} for more information on the parameters and returned data type. - */ AddTaxesForBasketItem: 'addTaxesForBasketItem', - /** - * Adds a payment instrument to a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addPaymentInstrumentToBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addpaymentinstrumenttobasket} for more information on the parameters and returned data type. - */ AddPaymentInstrumentToBasket: 'addPaymentInstrumentToBasket', - /** - * Removes a payment instrument of a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removePaymentInstrumentFromBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removepaymentinstrumentfrombasket} for more information on the parameters and returned data type. - */ RemovePaymentInstrumentFromBasket: 'removePaymentInstrumentFromBasket', - /** - * Updates payment instrument of an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updatePaymentInstrumentInBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatepaymentinstrumentinbasket} for more information on the parameters and returned data type. - */ UpdatePaymentInstrumentInBasket: 'updatePaymentInstrumentInBasket', - /** - * This method allows you to put an array of priceBookIds to an existing basket, which will be used for basket calculation. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addPriceBooksToBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addpricebookstobasket} for more information on the parameters and returned data type. - */ AddPriceBooksToBasket: 'addPriceBooksToBasket', - /** - * Creates a new shipment for a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=createShipmentForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#createshipmentforbasket} for more information on the parameters and returned data type. - */ CreateShipmentForBasket: 'createShipmentForBasket', - /** - * Removes a specified shipment and all associated product, gift certificate, shipping, and price adjustment line items from a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeShipmentFromBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removeshipmentfrombasket} for more information on the parameters and returned data type. - */ RemoveShipmentFromBasket: 'removeShipmentFromBasket', - /** - * Updates a shipment for a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateShipmentForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateshipmentforbasket} for more information on the parameters and returned data type. - */ UpdateShipmentForBasket: 'updateShipmentForBasket', - /** - * Sets a shipping address of a specific shipment of a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateShippingAddressForShipment} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateshippingaddressforshipment} for more information on the parameters and returned data type. - */ UpdateShippingAddressForShipment: 'updateShippingAddressForShipment', - /** - * Sets a shipping method to a specific shipment of a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateShippingMethodForShipment} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateshippingmethodforshipment} for more information on the parameters and returned data type. - */ UpdateShippingMethodForShipment: 'updateShippingMethodForShipment', - /** - * This method allows you to apply external taxation data to an existing basket to be able to pass tax rates and optional values for all taxable line items. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addTaxesForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addtaxesforbasket} for more information on the parameters and returned data type. - */ AddTaxesForBasket: 'addTaxesForBasket' } as const -type UseShopperBasketsMutationHeaders = NonNullable>['headers'] -type UseShopperBasketsMutationArg = { - headers?: UseShopperBasketsMutationHeaders - rawResponse?: boolean - action: ShopperBasketsMutationType -} - -type ShopperBasketsClient = ApiClients['shopperBaskets'] -export type ShopperBasketsMutationType = - (typeof ShopperBasketsMutations)[keyof typeof ShopperBasketsMutations] - -/** - * @private - */ -export const getCacheUpdateMatrix = (customerId: string | null) => { - const updateBasketQuery = ( - basketId?: string, - response?: DataType - ) => { - // TODO: we're missing headers, rawResponse -> not only {basketId} - const arg = {basketId} - - return basketId - ? { - update: [ - { - name: 'basket', - key: ['/baskets', basketId, arg], - updater: () => response - }, - { - // Since we use baskets from customer basket query, we need to update it for any basket mutation - name: 'customerBaskets', - key: ['/customers', customerId, '/baskets', {customerId}], - updater: ( - oldData: NonNullable> - ) => { - // do not update if responded basket does not exist inside existing customer baskets - if ( - !oldData?.baskets?.some( - (basket) => basket.basketId === response?.basketId - ) - ) { - return undefined - } - const updatedBaskets = oldData.baskets?.map( - (basket: DataType) => { - return basket?.basketId === basketId ? response : basket - } - ) - return { - ...oldData, - baskets: updatedBaskets - } - } - } - ] - } - : {} - } - - const removeBasketQuery = (basketId?: string) => { - const arg = {basketId} - return basketId - ? { - remove: [ - { - name: 'basket', - key: ['/baskets', basketId, arg] - } - ] - } - : {} - } - - const invalidateCustomerBasketsQuery = (customerId: string | null) => { - // TODO: should we use arg here or not? The invalidate method does not need exact query key. - const arg = {customerId} - return customerId - ? { - invalidate: [ - { - name: 'customerBaskets', - key: ['/customers', customerId, '/baskets', arg] - } - ] - } - : {} - } - - return { - addCouponToBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - addItemToBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - removeItemFromBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params?.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - addPaymentInstrumentToBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - createBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return { - // we want to re-fetch basket data in this case to get the basket total and other baskets data - ...invalidateCustomerBasketsQuery(customerId) - } - }, - deleteBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params?.parameters.basketId - - return { - ...invalidateCustomerBasketsQuery(customerId), - ...removeBasketQuery(basketId) - } - }, - mergeBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return { - ...invalidateCustomerBasketsQuery(customerId) - } - }, - removeCouponFromBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params?.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - removePaymentInstrumentFromBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params?.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - updateBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - updateBillingAddressForBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - updateCustomerForBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - updateItemInBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - updatePaymentInstrumentInBasket: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - updateShippingAddressForShipment: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - }, - updateShippingMethodForShipment: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const basketId = params.parameters.basketId - - return { - ...updateBasketQuery(basketId, response) - } - } - } -} - -export const SHOPPER_BASKETS_NOT_IMPLEMENTED = [ - 'addGiftCertificateItemToBasket', - 'addPriceBooksToBasket', - 'addTaxesForBasket', - 'addTaxesForBasketItem', - 'createShipmentForBasket', - 'removeGiftCertificateItemFromBasket', - 'removeShipmentFromBasket', - 'transferBasket', - 'updateGiftCertificateItemInBasket', - 'updateShipmentForBasket' -] - -/** - * A hook for performing mutations with the Shopper Baskets API. - */ -export function useShopperBasketsMutation( - arg: UseShopperBasketsMutationArg -): UseMutationResult< - DataType | Response, - Error, - Argument -> { - const {headers, rawResponse, action} = arg - - type Params = Argument - type Data = DataType - - if (SHOPPER_BASKETS_NOT_IMPLEMENTED.includes(action)) { - NotImplementedError() - } - const queryClient = useQueryClient() - const customerId = useCustomerId() - - const cacheUpdateMatrix = getCacheUpdateMatrix(customerId) - - return useMutation( - (params, apiClients) => { - const method = apiClients['shopperBaskets'][action] as MutationFunction - return ( - method.call as ( - apiClient: ShopperBasketsClient, - params: Params, - rawResponse: boolean | undefined - ) => any - )(apiClients['shopperBaskets'], {...params, headers}, rawResponse) - }, - { - onSuccess: (data, params) => { - updateCache(queryClient, action, cacheUpdateMatrix, data, params) - } - } - ) +export type ShopperBasketsMutation = typeof ShopperBasketsMutations[keyof typeof ShopperBasketsMutations] + +export function useShopperBasketsMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + const {shopperBaskets: client} = useCommerceApi() + // Directly calling `client[mutation(options)` doesn't work, because the methods don't fully + // overlap. Adding in this type assertion fixes that, but I don't understand why. I'm fairly + // confident, though, that it is safe, because it seems like we're mostly re-defining what we + // already have. + const method = (options: Argument) => + (client[mutation] as ApiMethod, DataType>)( + options + ) + + return useMutation({method, getCacheUpdates}) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts index 2c4bb516ff..978b81aa9d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts @@ -1,22 +1,16 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' import {ApiClients, Argument, DataType} from '../types' +import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {NotImplementedError} from './../utils' type Client = ApiClients['shopperBaskets'] -type UseBasketParameters = NonNullable>['parameters'] -type UseBasketHeaders = NonNullable>['headers'] -type UseBasketArg = { - headers?: UseBasketHeaders - rawResponse?: boolean -} & UseBasketParameters /** * A hook for `ShopperBaskets#getBasket`. * Gets a basket. @@ -24,39 +18,41 @@ type UseBasketArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getbasket} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useBasket( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useBasket( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useBasket( - arg: UseBasketArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const defaultOptions = { - enabled: !!parameters.basketId - } - return useQuery( - ['/baskets', parameters.basketId, arg], - (_, {shopperBaskets}) => shopperBaskets.getBasket({parameters, headers}, rawResponse), - {...defaultOptions, ...options} - ) -} +export const useBasket = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => client.getBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '?', + 'siteId', + parameters.siteId, + 'locale', + parameters.locale, + // Full parameters last for easy lookup + parameters + ] as const -type UsePaymentMethodsForBasketParameters = NonNullable< - Argument ->['parameters'] -type UsePaymentMethodsForBasketHeaders = NonNullable< - Argument ->['headers'] -type UsePaymentMethodsForBasketArg = { - headers?: UsePaymentMethodsForBasketHeaders - rawResponse?: boolean -} & UsePaymentMethodsForBasketParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperBaskets#getPaymentMethodsForBasket`. * Gets applicable payment methods for an existing basket considering the open payment amount only. @@ -64,54 +60,88 @@ type UsePaymentMethodsForBasketArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpaymentmethodsforbasket} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePaymentMethodsForBasket( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function usePaymentMethodsForBasket( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function usePaymentMethodsForBasket( - arg: UsePaymentMethodsForBasketArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const defaultOptions = { - enabled: !!parameters.basketId - } +export const usePaymentMethodsForBasket = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPaymentMethodsForBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/payment-methods', + '?', + 'siteId', + parameters.siteId, + 'locale', + parameters.locale, + // Full parameters last for easy lookup + parameters + ] as const - return useQuery( - ['/baskets', parameters.basketId, '/payment-methods', arg], - (_, {shopperBaskets}) => - shopperBaskets.getPaymentMethodsForBasket({parameters, headers}, rawResponse), - {...defaultOptions, ...options} - ) + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } - /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperBaskets#getPriceBooksForBasket`. * Gets applicable price books for an existing basket. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPriceBooksForBasket} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpricebooksforbasket} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePriceBooksForBasket(): void { - NotImplementedError() -} +export const usePriceBooksForBasket = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPriceBooksForBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/price-books', + '?', + 'siteId', + parameters.siteId, + // Full parameters last for easy lookup + parameters + ] as const -type UseShippingMethodsForShipmentParameters = NonNullable< - Argument ->['parameters'] -type UseShippingMethodsForShipmentHeaders = NonNullable< - Argument ->['headers'] -type UseShippingMethodsForShipmentArg = { - headers?: UseShippingMethodsForShipmentHeaders - rawResponse?: boolean -} & UseShippingMethodsForShipmentParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperBaskets#getShippingMethodsForShipment`. * Gets the applicable shipping methods for a certain shipment of a basket. @@ -119,54 +149,86 @@ type UseShippingMethodsForShipmentArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getshippingmethodsforshipment} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useShippingMethodsForShipment( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useShippingMethodsForShipment( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useShippingMethodsForShipment( - arg: UseShippingMethodsForShipmentArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const defaultOptions = { - enabled: !!parameters.basketId && !!parameters.shipmentId - } - return useQuery( +export const useShippingMethodsForShipment = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => + client.getShippingMethodsForShipment(arg) + const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => [ - '/baskets', + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', parameters.basketId, - '/shipments', + '/shipments/', parameters.shipmentId, '/shipping-methods', - arg - ], - (_, {shopperBaskets}) => - shopperBaskets.getShippingMethodsForShipment({parameters, headers}, rawResponse), - {...defaultOptions, ...options} - ) -} + '?', + 'siteId', + parameters.siteId, + 'locale', + parameters.locale, + // Full parameters last for easy lookup + parameters + ] as const + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperBaskets#getTaxesFromBasket`. * This method gives you the external taxation data set by the PUT taxes API. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getTaxesFromBasket} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#gettaxesfrombasket} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useTaxesFromBasket(): void { - NotImplementedError() -} +export const useTaxesFromBasket = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => client.getTaxesFromBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/taxes', + '?', + 'siteId', + parameters.siteId, + // Full parameters last for easy lookup + parameters + ] as const -export { - useBasket, - usePaymentMethodsForBasket, - usePriceBooksForBasket, - useShippingMethodsForShipment, - useTaxesFromBasket + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts new file mode 100644 index 0000000000..f7b9b858cb --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/Test/config.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023, 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 {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' +import {ApiClients, CacheUpdateMatrix} from '../types' +import {CacheUpdateMatrixElement} from '../utils' + +const updateBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { + // TODO: we're missing headers, rawResponse -> not only {basketId} + const arg = {basketId} + return basketId + ? { + update: [['/baskets', basketId, arg]] + } + : {} +} + +const removeBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { + const arg = {basketId} + return basketId + ? { + remove: [['/baskets', basketId, arg]] + } + : {} +} + +const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdateMatrixElement => { + // TODO: should we use arg here or not? The invalidate method does not need exact query key. + const arg = {customerId} + return customerId + ? { + invalidate: [['/customers', customerId, '/baskets', arg]] + } + : {} +} + +const updateBasketFromRequest = ( + customerId: string | null, + params: { + parameters: ShopperBasketsTypes.Basket + } +): CacheUpdateMatrixElement => ({ + ...updateBasketQuery(params.parameters.basketId), + ...invalidateCustomerBasketsQuery(customerId) +}) + +const updateBasketFromResponse = ( + customerId: string | null, + params: unknown, // not used, + response: ShopperBasketsTypes.Basket +): CacheUpdateMatrixElement => ({ + ...updateBasketQuery(response.basketId), + ...invalidateCustomerBasketsQuery(customerId) +}) + +export const cacheUpdateMatrix: CacheUpdateMatrix = { + addCouponToBasket: updateBasketFromRequest, + addItemToBasket: updateBasketFromRequest, + removeItemFromBasket: updateBasketFromRequest, + addPaymentInstrumentToBasket: updateBasketFromRequest, + createBasket: updateBasketFromResponse, // Response! + deleteBasket: (customerId, params): CacheUpdateMatrixElement => ({ + ...invalidateCustomerBasketsQuery(customerId), + ...removeBasketQuery(params.parameters.basketId) + }), + mergeBasket: updateBasketFromResponse, // Response! + removeCouponFromBasket: updateBasketFromRequest, + removePaymentInstrumentFromBasket: updateBasketFromRequest, + updateBasket: updateBasketFromRequest, + updateBillingAddressForBasket: updateBasketFromRequest, + updateCustomerForBasket: updateBasketFromRequest, + updateItemInBasket: updateBasketFromRequest, + updatePaymentInstrumentInBasket: updateBasketFromRequest, + updateShippingAddressForShipment: updateBasketFromRequest, + updateShippingMethodForShipment: updateBasketFromRequest +} diff --git a/packages/commerce-sdk-react/src/hooks/Test/index.ts b/packages/commerce-sdk-react/src/hooks/Test/index.ts new file mode 100644 index 0000000000..1a13c64feb --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/Test/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2023, 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 + */ +export * from './mutation' +export * from './query' diff --git a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts new file mode 100644 index 0000000000..f3a84bfdce --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType, ApiMethod} from '../types' +import {useMutation} from '../useMutation' +import {UseMutationResult} from '@tanstack/react-query' +import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' + +type Client = ApiClients['shopperBaskets'] + +export const ShopperBasketsMutations = { + CreateBasket: 'createBasket', + TransferBasket: 'transferBasket', + MergeBasket: 'mergeBasket', + DeleteBasket: 'deleteBasket', + UpdateBasket: 'updateBasket', + UpdateBillingAddressForBasket: 'updateBillingAddressForBasket', + AddCouponToBasket: 'addCouponToBasket', + RemoveCouponFromBasket: 'removeCouponFromBasket', + UpdateCustomerForBasket: 'updateCustomerForBasket', + AddGiftCertificateItemToBasket: 'addGiftCertificateItemToBasket', + RemoveGiftCertificateItemFromBasket: 'removeGiftCertificateItemFromBasket', + UpdateGiftCertificateItemInBasket: 'updateGiftCertificateItemInBasket', + AddItemToBasket: 'addItemToBasket', + RemoveItemFromBasket: 'removeItemFromBasket', + UpdateItemInBasket: 'updateItemInBasket', + AddTaxesForBasketItem: 'addTaxesForBasketItem', + AddPaymentInstrumentToBasket: 'addPaymentInstrumentToBasket', + RemovePaymentInstrumentFromBasket: 'removePaymentInstrumentFromBasket', + UpdatePaymentInstrumentInBasket: 'updatePaymentInstrumentInBasket', + AddPriceBooksToBasket: 'addPriceBooksToBasket', + CreateShipmentForBasket: 'createShipmentForBasket', + RemoveShipmentFromBasket: 'removeShipmentFromBasket', + UpdateShipmentForBasket: 'updateShipmentForBasket', + UpdateShippingAddressForShipment: 'updateShippingAddressForShipment', + UpdateShippingMethodForShipment: 'updateShippingMethodForShipment', + AddTaxesForBasket: 'addTaxesForBasket' +} as const + +export type ShopperBasketsMutation = typeof ShopperBasketsMutations[keyof typeof ShopperBasketsMutations] + +export function useShopperBasketsMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + const {shopperBaskets: client} = useCommerceApi() + // Directly calling `client[mutation(options)` doesn't work, because the methods don't fully + // overlap. Adding in this type assertion fixes that, but I don't understand why. I'm fairly + // confident, though, that it is safe, because it seems like we're mostly re-defining what we + // already have. + const method = (opts: Argument) => + (client[mutation] as ApiMethod, DataType>)( + opts + ) + + return useMutation({method, getCacheUpdates}) +} diff --git a/packages/commerce-sdk-react/src/hooks/Test/query.ts b/packages/commerce-sdk-react/src/hooks/Test/query.ts new file mode 100644 index 0000000000..978b81aa9d --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/Test/query.ts @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2023, 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' +import {ApiClients, Argument, DataType} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' + +type Client = ApiClients['shopperBaskets'] + +/** + * A hook for `ShopperBaskets#getBasket`. + * Gets a basket. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getBasket} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getbasket} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useBasket = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => client.getBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '?', + 'siteId', + parameters.siteId, + 'locale', + parameters.locale, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperBaskets#getPaymentMethodsForBasket`. + * Gets applicable payment methods for an existing basket considering the open payment amount only. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPaymentMethodsForBasket} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpaymentmethodsforbasket} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const usePaymentMethodsForBasket = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPaymentMethodsForBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/payment-methods', + '?', + 'siteId', + parameters.siteId, + 'locale', + parameters.locale, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperBaskets#getPriceBooksForBasket`. + * Gets applicable price books for an existing basket. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPriceBooksForBasket} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpricebooksforbasket} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const usePriceBooksForBasket = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPriceBooksForBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/price-books', + '?', + 'siteId', + parameters.siteId, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperBaskets#getShippingMethodsForShipment`. + * Gets the applicable shipping methods for a certain shipment of a basket. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getShippingMethodsForShipment} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getshippingmethodsforshipment} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useShippingMethodsForShipment = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => + client.getShippingMethodsForShipment(arg) + const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/shipments/', + parameters.shipmentId, + '/shipping-methods', + '?', + 'siteId', + parameters.siteId, + 'locale', + parameters.locale, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperBaskets#getTaxesFromBasket`. + * This method gives you the external taxation data set by the PUT taxes API. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getTaxesFromBasket} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#gettaxesfrombasket} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useTaxesFromBasket = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperBaskets: client} = useCommerceApi() + const method = (arg: Argument) => client.getTaxesFromBasket(arg) + const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = >(parameters: T) => + [ + 'https://', + parameters.shortCode, + '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', + parameters.version, + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/taxes', + '?', + 'siteId', + parameters.siteId, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/Test/temp.ts b/packages/commerce-sdk-react/src/hooks/Test/temp.ts new file mode 100644 index 0000000000..ee34447524 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/Test/temp.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023, 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 + */ + +// Temp test file to validate that types are working as expected + +import {cacheUpdateMatrix} from './config' +import {useShopperBasketsMutation} from './mutation' +import {useBasket} from './query' + +const query = useBasket({ + parameters: { + basketId: 'basketId' + } +}) +const queryData = query.data + +const updates = cacheUpdateMatrix.removeItemFromBasket?.( + 'customerId', + { + parameters: { + basketId: 'basketId', + itemId: 'itemId' + } + }, + {basketId: 'basketId'} +) +if (updates) { + const {invalidate} = updates +} + +const mutation = useShopperBasketsMutation('addCouponToBasket') +const {data: mutationData, mutate} = mutation +mutate({ + body: {code: 'code'}, + parameters: {basketId: 'basketId'} +}) diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 1455d09c27..cc074c049f 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -1,20 +1,24 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ShopperBaskets} from 'commerce-sdk-isomorphic' -import {ShopperContexts} from 'commerce-sdk-isomorphic' -import {ShopperCustomers} from 'commerce-sdk-isomorphic' -import {ShopperDiscoverySearch} from 'commerce-sdk-isomorphic' -import {ShopperGiftCertificates} from 'commerce-sdk-isomorphic' -import {ShopperLogin} from 'commerce-sdk-isomorphic' -import {ShopperOrders} from 'commerce-sdk-isomorphic' -import {ShopperProducts} from 'commerce-sdk-isomorphic' -import {ShopperPromotions} from 'commerce-sdk-isomorphic' -import {ShopperSearch} from 'commerce-sdk-isomorphic' -import {QueryKey, QueryFunctionContext} from '@tanstack/react-query' +import {QueryKey} from '@tanstack/react-query' +import { + ShopperBaskets, + ShopperContexts, + ShopperCustomers, + ShopperDiscoverySearch, + ShopperGiftCertificates, + ShopperLogin, + ShopperOrders, + ShopperProducts, + ShopperPromotions, + ShopperSearch +} from 'commerce-sdk-isomorphic' + +// --- API CLIENTS --- // export type ApiClientConfigParams = { clientId: string @@ -36,35 +40,64 @@ export interface ApiClients { shopperSearch: ShopperSearch } +export type ApiClient = ApiClients[keyof ApiClients] + +// --- API HELPERS --- // + +/** + * Generic signature of the options objects used by commerce-sdk-isomorphic + */ +export type ApiOptions< + Parameters extends Record = Record, + Headers extends Record = Record, + Body extends Record = Record +> = { + parameters?: Parameters + headers?: Headers + body?: Body +} + +/** + * Generic signature of API methods exported by commerce-sdk-isomorphic + */ +export type ApiMethod = { + (options: Options): Promise +} + /** * The first argument of a function. */ -export type Argument unknown> = Parameters[0] +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Argument unknown> = NonNullable[0]> /** * The data type returned by a commerce-sdk-isomorphic method when the raw response * flag is not set. */ -export type DataType Promise> = T extends ( - arg: any -) => Promise +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DataType> = T extends ApiMethod ? R : never -/** - * Modified version of React Query's Mutation Function. Added a second argument - * API clients. - */ -export type IMutationFunction = ( - variables: TVariables, - apiClients: ApiClients -) => Promise +// --- CACHE HELPERS --- // -/** - * Modified version of React Query's Query Function. Added a second argument - * API clients. - */ -export type IQueryFunction = ( - context: QueryFunctionContext, - apiClients: ApiClients -) => Promise +export interface CacheUpdate { + update?: Array + invalidate?: Array + remove?: Array +} + +export type CacheUpdateGetter = ( + customerId: string | null, + params: Options, + response: Data +) => CacheUpdate + +export type CacheUpdateMatrix = { + // It feels like we should be able to do , but that + // results in some methods being `never`, so we just use Argument<> later + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [Method in keyof Client]?: Client[Method] extends ApiMethod + ? CacheUpdateGetter, Data> + : never +} diff --git a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts new file mode 100644 index 0000000000..d3b252991a --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 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 {ApiOptions, ApiMethod} from './types' +import useAuth from './useAuth' + +export const useAuthorizationHeader = ( + fn: ApiMethod +): ApiMethod => { + const auth = useAuth() + return async (options: Opts) => { + const {access_token} = await auth.ready() + return await fn({ + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${access_token}` + } + }) + } +} diff --git a/packages/commerce-sdk-react/src/hooks/useMutation.ts b/packages/commerce-sdk-react/src/hooks/useMutation.ts index 1967a7f30e..e62283b675 100644 --- a/packages/commerce-sdk-react/src/hooks/useMutation.ts +++ b/packages/commerce-sdk-react/src/hooks/useMutation.ts @@ -1,17 +1,27 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 useAuthenticatedClient from './useAuthenticatedClient' -import {useMutation as useReactQueryMutation, UseMutationOptions} from '@tanstack/react-query' -import {IMutationFunction} from './types' +import {useMutation as useReactQueryMutation, useQueryClient} from '@tanstack/react-query' +import {CacheUpdateGetter, ApiOptions, ApiMethod} from './types' +import {useAuthorizationHeader} from './useAuthorizationHeader' +import useCustomerId from './useCustomerId' +import {updateCache} from './utils' -export const useMutation = ( - fn: IMutationFunction, - options?: Omit, 'mutationFn'> -) => { - const authenticatedFn = useAuthenticatedClient(fn) - return useReactQueryMutation(authenticatedFn, options) +export const useMutation = (hookConfig: { + method: ApiMethod + getCacheUpdates: CacheUpdateGetter +}) => { + const queryClient = useQueryClient() + const customerId = useCustomerId() + const authenticatedMethod = useAuthorizationHeader(hookConfig.method) + + return useReactQueryMutation(authenticatedMethod, { + onSuccess: (data, options) => { + const cacheUpdates = hookConfig.getCacheUpdates(customerId, options, data) + updateCache(queryClient, cacheUpdates, data) + } + }) } diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index 1200b55162..14976b859e 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -1,18 +1,45 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {useQuery as useReactQuery, UseQueryOptions, QueryKey} from '@tanstack/react-query' -import useAuthenticatedClient from './useAuthenticatedClient' -import {IQueryFunction} from './types' +import {useAuthorizationHeader} from './useAuthorizationHeader' +import {ApiClient, ApiMethod, ApiOptions} from './types' +import {hasAllKeys} from './utils' -export const useQuery = ( - queryKey: QueryKey, - fn: IQueryFunction, - queryOptions?: UseQueryOptions +export const useQuery = < + Options extends Omit, + Client extends ApiClient, + Data, + Err, + QK extends QueryKey +>( + apiOptions: Options, + queryOptions: UseQueryOptions, + hookConfig: { + client: Client + method: ApiMethod + getQueryKey: (parameters: Options['parameters']) => QK + requiredParameters: ReadonlyArray + enabled?: boolean + } ) => { - const authenticatedFn = useAuthenticatedClient(fn) - return useReactQuery(queryKey, authenticatedFn, queryOptions) + const parameters = { + ...hookConfig.client.clientConfig.parameters, + ...apiOptions.parameters + } + const authenticatedMethod = useAuthorizationHeader(hookConfig.method) + return useReactQuery( + // End user can override queryKey if they really want to... + queryOptions.queryKey ?? hookConfig.getQueryKey(parameters), + () => authenticatedMethod(apiOptions), + { + enabled: + hookConfig.enabled !== false && + hasAllKeys(parameters, hookConfig.requiredParameters), + ...queryOptions + } + ) } diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index aaa9bfec08..7d0099415f 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -1,86 +1,64 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {QueryClient, QueryKey, Updater} from '@tanstack/react-query' -import {ApiClients, Argument, DataType} from './types' -import {ShopperCustomersMutationType} from './ShopperCustomers' -import {ShopperOrdersMutationType} from './ShopperOrders' -import {ShopperBasketsMutationType} from './ShopperBaskets' +import {QueryClient, QueryKey} from '@tanstack/react-query' +import {CacheUpdate} from './types' -const isObject = (item: unknown) => - typeof item === 'object' && !Array.isArray(item) && item !== null +const isObject = (item: unknown): item is object => typeof item === 'object' && item !== null -//TODO: update data type for updater when needed -export interface QueryKeyMap { - name: string - key: QueryKey - updater?: Updater< - DataType | undefined, - DataType | undefined - > -} - -export interface CacheUpdateMatrixElement { - update?: Array - invalidate?: Array - remove?: Array -} - -// TODO: Add more endpoints types as needed -export type CombinedMutationTypes = - | ShopperOrdersMutationType - | ShopperCustomersMutationType - | ShopperBasketsMutationType - -type CacheUpdateMatrix = { - [key in CombinedMutationTypes]?: (data: any, param: any) => CacheUpdateMatrixElement +const deepEqual = (a: unknown, b: unknown): boolean => { + if (!isObject(a) || !isObject(b)) return a === b + const aEntries = Object.entries(a) + const bEntries = Object.entries(b) + if (aEntries.length !== bEntries.length) return false + return aEntries.every(([aKey, aValue], index) => { + const [bKey, bValue] = bEntries[index] + return aKey === bKey && deepEqual(aValue, bValue) + }) } -// TODO: Add more api clients as needed -export type Client = ApiClients['shopperOrders'] & - ApiClients['shopperCustomers'] & - ApiClients['shopperBaskets'] +/** + * Determines whether the cache query key starts with the same values as the lookup query key + * @param cacheQueryKey query key from the cache + * @param lookupQueryKey partial query key to validate match against + * @returns boolean + */ +const isMatchingKey = (cacheQueryKey: QueryKey, lookupQueryKey: QueryKey): boolean => + lookupQueryKey.every((item, index) => deepEqual(item, cacheQueryKey[index])) -export const updateCache = ( +export const updateCache = ( queryClient: QueryClient, - action: Action, - cacheUpdateMatrix: CacheUpdateMatrix, - response: DataType, - params: Argument + cacheUpdates: CacheUpdate, + response: unknown ) => { - const isMatchingKey = (cacheQuery: {queryKey: {[x: string]: any}}, queryKey: QueryKey) => - queryKey.every((item, index) => - isObject(item) && isObject(cacheQuery.queryKey[index]) - ? Object.entries(cacheQuery.queryKey[index]).sort().toString() === - Object.entries(item as Record) - .sort() - .toString() - : item === cacheQuery.queryKey[index] - ) - - // STEP 1. Update data inside query cache for the matching queryKeys, and updater - cacheUpdateMatrix[action]?.(params, response)?.update?.map(({key: queryKey, updater}) => { - queryClient.setQueryData(queryKey, updater) + // STEP 1. Update data inside query cache for the matching queryKeys + cacheUpdates.update?.forEach((queryKey) => { + queryClient.setQueryData(queryKey, response) }) // STEP 2. Invalidate cache entries with the matching queryKeys - cacheUpdateMatrix[action]?.(params, response)?.invalidate?.map(({key: queryKey}) => { + cacheUpdates.invalidate?.forEach((queryKey) => { queryClient.invalidateQueries({ - predicate: (cacheQuery: any) => isMatchingKey(cacheQuery, queryKey) + predicate: (cacheQuery) => isMatchingKey(cacheQuery.queryKey, queryKey) }) }) // STEP 3. Remove cache entries with the matching queryKeys - cacheUpdateMatrix[action]?.(params, response)?.remove?.map(({key: queryKey}) => { + cacheUpdates.remove?.forEach((queryKey) => { queryClient.removeQueries({ - predicate: (cacheQuery: any) => isMatchingKey(cacheQuery, queryKey) + predicate: (cacheQuery) => isMatchingKey(cacheQuery.queryKey, queryKey) }) }) } -export const NotImplementedError = () => { - throw new Error('This method is not implemented.') +export class NotImplementedError extends Error { + constructor(method = 'This method') { + super(`${method} is not yet implemented.`) + } } + +export const hasAllKeys = (object: T, keys: ReadonlyArray): boolean => + keys.every((key) => object[key] !== undefined) From 6a036832e6e441e5dca8339efab0c401f81f6851 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 3 Feb 2023 16:33:08 -0500 Subject: [PATCH 002/122] lint fixes --- .../commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts | 3 ++- packages/commerce-sdk-react/src/hooks/Test/mutation.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts index 1831a49bc1..fc03892a7e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts @@ -42,7 +42,8 @@ export const ShopperBasketsMutations = { AddTaxesForBasket: 'addTaxesForBasket' } as const -export type ShopperBasketsMutation = typeof ShopperBasketsMutations[keyof typeof ShopperBasketsMutations] +export type ShopperBasketsMutation = + (typeof ShopperBasketsMutations)[keyof typeof ShopperBasketsMutations] export function useShopperBasketsMutation( mutation: Mutation diff --git a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts index f3a84bfdce..93b355ffee 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts @@ -42,7 +42,8 @@ export const ShopperBasketsMutations = { AddTaxesForBasket: 'addTaxesForBasket' } as const -export type ShopperBasketsMutation = typeof ShopperBasketsMutations[keyof typeof ShopperBasketsMutations] +export type ShopperBasketsMutation = + (typeof ShopperBasketsMutations)[keyof typeof ShopperBasketsMutations] export function useShopperBasketsMutation( mutation: Mutation From e9a3983adb9c4d1d38553989ded6bb5d15f5ebdc Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 3 Feb 2023 16:34:36 -0500 Subject: [PATCH 003/122] Use correct type. --- .../src/hooks/ShopperBaskets/config.ts | 14 +++++++------- .../commerce-sdk-react/src/hooks/Test/config.ts | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts index f7b9b858cb..187633861e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts @@ -6,9 +6,9 @@ */ import {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' import {ApiClients, CacheUpdateMatrix} from '../types' -import {CacheUpdateMatrixElement} from '../utils' +import {CacheUpdate} from '../types' -const updateBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { +const updateBasketQuery = (basketId?: string): CacheUpdate => { // TODO: we're missing headers, rawResponse -> not only {basketId} const arg = {basketId} return basketId @@ -18,7 +18,7 @@ const updateBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { : {} } -const removeBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { +const removeBasketQuery = (basketId?: string): CacheUpdate => { const arg = {basketId} return basketId ? { @@ -27,7 +27,7 @@ const removeBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { : {} } -const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdateMatrixElement => { +const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdate => { // TODO: should we use arg here or not? The invalidate method does not need exact query key. const arg = {customerId} return customerId @@ -42,7 +42,7 @@ const updateBasketFromRequest = ( params: { parameters: ShopperBasketsTypes.Basket } -): CacheUpdateMatrixElement => ({ +): CacheUpdate => ({ ...updateBasketQuery(params.parameters.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -51,7 +51,7 @@ const updateBasketFromResponse = ( customerId: string | null, params: unknown, // not used, response: ShopperBasketsTypes.Basket -): CacheUpdateMatrixElement => ({ +): CacheUpdate => ({ ...updateBasketQuery(response.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -62,7 +62,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, params): CacheUpdateMatrixElement => ({ + deleteBasket: (customerId, params): CacheUpdate => ({ ...invalidateCustomerBasketsQuery(customerId), ...removeBasketQuery(params.parameters.basketId) }), diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts index f7b9b858cb..187633861e 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/config.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/config.ts @@ -6,9 +6,9 @@ */ import {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' import {ApiClients, CacheUpdateMatrix} from '../types' -import {CacheUpdateMatrixElement} from '../utils' +import {CacheUpdate} from '../types' -const updateBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { +const updateBasketQuery = (basketId?: string): CacheUpdate => { // TODO: we're missing headers, rawResponse -> not only {basketId} const arg = {basketId} return basketId @@ -18,7 +18,7 @@ const updateBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { : {} } -const removeBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { +const removeBasketQuery = (basketId?: string): CacheUpdate => { const arg = {basketId} return basketId ? { @@ -27,7 +27,7 @@ const removeBasketQuery = (basketId?: string): CacheUpdateMatrixElement => { : {} } -const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdateMatrixElement => { +const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdate => { // TODO: should we use arg here or not? The invalidate method does not need exact query key. const arg = {customerId} return customerId @@ -42,7 +42,7 @@ const updateBasketFromRequest = ( params: { parameters: ShopperBasketsTypes.Basket } -): CacheUpdateMatrixElement => ({ +): CacheUpdate => ({ ...updateBasketQuery(params.parameters.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -51,7 +51,7 @@ const updateBasketFromResponse = ( customerId: string | null, params: unknown, // not used, response: ShopperBasketsTypes.Basket -): CacheUpdateMatrixElement => ({ +): CacheUpdate => ({ ...updateBasketQuery(response.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -62,7 +62,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, params): CacheUpdateMatrixElement => ({ + deleteBasket: (customerId, params): CacheUpdate => ({ ...invalidateCustomerBasketsQuery(customerId), ...removeBasketQuery(params.parameters.basketId) }), From 2602899660b06bec4a1a221fea8543be03651823 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 3 Feb 2023 16:34:58 -0500 Subject: [PATCH 004/122] Remove unnecessary file. --- .../src/hooks/useAuthenticatedClient.ts | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 packages/commerce-sdk-react/src/hooks/useAuthenticatedClient.ts diff --git a/packages/commerce-sdk-react/src/hooks/useAuthenticatedClient.ts b/packages/commerce-sdk-react/src/hooks/useAuthenticatedClient.ts deleted file mode 100644 index dec9847370..0000000000 --- a/packages/commerce-sdk-react/src/hooks/useAuthenticatedClient.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2022, 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 { - QueryFunctionContext, - QueryKey, - QueryFunction, - MutationFunction -} from '@tanstack/react-query' -import useAuth from './useAuth' -import useCommerceApi from './useCommerceApi' -import {ApiClients, IQueryFunction, IMutationFunction} from './types' - -/** - * This is a private method. It is a react hook that wraps queries / mutations with - * authenticated API clients. - * - * @internal - */ -function useAuthenticatedClient( - fn: IMutationFunction -): MutationFunction -function useAuthenticatedClient(fn: IQueryFunction): QueryFunction -function useAuthenticatedClient( - fn: IQueryFunction | IMutationFunction -) { - const auth = useAuth() - const apiClients = useCommerceApi() - const apiClientKeys = Object.keys(apiClients) as Array - return (variables: TVariables & QueryFunctionContext) => { - return auth - .ready() - .then(({access_token}) => { - apiClientKeys.forEach((client) => { - apiClients[client].clientConfig.headers = { - ...apiClients[client].clientConfig.headers, - Authorization: `Bearer ${access_token}` - } - }) - return apiClients - }) - .then((apiClients) => fn(variables, apiClients)) - } -} - -export default useAuthenticatedClient From 2b7d27f8cf40ee0348507c912afbd879e8d900d3 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 6 Feb 2023 13:00:12 -0500 Subject: [PATCH 005/122] Add support for updater in cache updates. --- .../src/hooks/Test/config.ts | 31 ++++++++++++------- .../src/hooks/Test/mutation.ts | 23 +++++++------- .../commerce-sdk-react/src/hooks/types.ts | 18 ++++++++--- .../src/hooks/useMutation.ts | 3 +- .../commerce-sdk-react/src/hooks/utils.ts | 16 ++++------ 5 files changed, 53 insertions(+), 38 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts index 187633861e..625bf1011d 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/config.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/config.ts @@ -8,31 +8,40 @@ import {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' import {ApiClients, CacheUpdateMatrix} from '../types' import {CacheUpdate} from '../types' -const updateBasketQuery = (basketId?: string): CacheUpdate => { +type Basket = ShopperBasketsTypes.Basket + +const updateBasketQuery = (basketId?: string): Pick, 'update'> => { // TODO: we're missing headers, rawResponse -> not only {basketId} const arg = {basketId} return basketId ? { - update: [['/baskets', basketId, arg]] + update: [ + { + queryKey: ['/baskets', basketId, arg], + updater: (basket) => basket // TODO + } + ] } : {} } -const removeBasketQuery = (basketId?: string): CacheUpdate => { +const removeBasketQuery = (basketId?: string): Pick, 'remove'> => { const arg = {basketId} return basketId ? { - remove: [['/baskets', basketId, arg]] + remove: [{queryKey: ['/baskets', basketId, arg]}] } : {} } -const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdate => { +const invalidateCustomerBasketsQuery = ( + customerId: string | null +): Pick, 'invalidate'> => { // TODO: should we use arg here or not? The invalidate method does not need exact query key. const arg = {customerId} return customerId ? { - invalidate: [['/customers', customerId, '/baskets', arg]] + invalidate: [{queryKey: ['/customers', customerId, '/baskets', arg]}] } : {} } @@ -40,9 +49,9 @@ const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdate const updateBasketFromRequest = ( customerId: string | null, params: { - parameters: ShopperBasketsTypes.Basket + parameters: Basket } -): CacheUpdate => ({ +): CacheUpdate => ({ ...updateBasketQuery(params.parameters.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -50,8 +59,8 @@ const updateBasketFromRequest = ( const updateBasketFromResponse = ( customerId: string | null, params: unknown, // not used, - response: ShopperBasketsTypes.Basket -): CacheUpdate => ({ + response: Basket +): CacheUpdate => ({ ...updateBasketQuery(response.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -62,7 +71,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, params): CacheUpdate => ({ + deleteBasket: (customerId, params): CacheUpdate => ({ ...invalidateCustomerBasketsQuery(customerId), ...removeBasketQuery(params.parameters.basketId) }), diff --git a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts index 93b355ffee..8fbcbd5543 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts @@ -4,7 +4,7 @@ * 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 {ApiClients, Argument, DataType, ApiMethod} from '../types' +import {ApiClients, Argument, DataType, ApiMethod, CacheUpdateGetter} from '../types' import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' @@ -52,15 +52,16 @@ export function useShopperBasketsMutation) => - (client[mutation] as ApiMethod, DataType>)( - opts - ) - - return useMutation({method, getCacheUpdates}) + type Options = Argument + type Data = DataType + return useMutation({ + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter + }) } diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index cc074c049f..fe037655ac 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -81,17 +81,25 @@ export type DataType> = T extends ApiMethod - invalidate?: Array - remove?: Array +export type CacheUpdateBase = { + queryKey: QueryKey // TODO: Can we do [...string, object]? +} + +export type CacheUpdateUpdate = CacheUpdateBase & { + updater: (data: Data | undefined) => Data | undefined +} + +export type CacheUpdate = { + update?: CacheUpdateUpdate[] + invalidate?: CacheUpdateBase[] + remove?: CacheUpdateBase[] } export type CacheUpdateGetter = ( customerId: string | null, params: Options, response: Data -) => CacheUpdate +) => CacheUpdate export type CacheUpdateMatrix = { // It feels like we should be able to do , but that diff --git a/packages/commerce-sdk-react/src/hooks/useMutation.ts b/packages/commerce-sdk-react/src/hooks/useMutation.ts index e62283b675..b6ec331792 100644 --- a/packages/commerce-sdk-react/src/hooks/useMutation.ts +++ b/packages/commerce-sdk-react/src/hooks/useMutation.ts @@ -21,7 +21,8 @@ export const useMutation = (hookConfig: { return useReactQueryMutation(authenticatedMethod, { onSuccess: (data, options) => { const cacheUpdates = hookConfig.getCacheUpdates(customerId, options, data) - updateCache(queryClient, cacheUpdates, data) + cacheUpdates.update?.forEach(({updater}) => updater) + updateCache(queryClient, cacheUpdates) } }) } diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 7d0099415f..e1d93e765b 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -29,25 +29,21 @@ const deepEqual = (a: unknown, b: unknown): boolean => { const isMatchingKey = (cacheQueryKey: QueryKey, lookupQueryKey: QueryKey): boolean => lookupQueryKey.every((item, index) => deepEqual(item, cacheQueryKey[index])) -export const updateCache = ( - queryClient: QueryClient, - cacheUpdates: CacheUpdate, - response: unknown -) => { +export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { // STEP 1. Update data inside query cache for the matching queryKeys - cacheUpdates.update?.forEach((queryKey) => { - queryClient.setQueryData(queryKey, response) + cacheUpdates.update?.forEach(({queryKey, updater}) => { + queryClient.setQueryData(queryKey, updater) }) // STEP 2. Invalidate cache entries with the matching queryKeys - cacheUpdates.invalidate?.forEach((queryKey) => { + cacheUpdates.invalidate?.forEach(({queryKey}) => { queryClient.invalidateQueries({ predicate: (cacheQuery) => isMatchingKey(cacheQuery.queryKey, queryKey) }) }) // STEP 3. Remove cache entries with the matching queryKeys - cacheUpdates.remove?.forEach((queryKey) => { + cacheUpdates.remove?.forEach(({queryKey}) => { queryClient.removeQueries({ predicate: (cacheQuery) => isMatchingKey(cacheQuery.queryKey, queryKey) }) @@ -56,7 +52,7 @@ export const updateCache = ( export class NotImplementedError extends Error { constructor(method = 'This method') { - super(`${method} is not yet implemented.`) + super(`${method} is not implemented.`) } } From 90dfab162a89b8ca905aa2163eced071a78fe3c7 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 7 Feb 2023 16:13:31 -0500 Subject: [PATCH 006/122] Sort imports. --- packages/commerce-sdk-react/src/hooks/Test/mutation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts index 8fbcbd5543..18288de1a8 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts @@ -4,7 +4,7 @@ * 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 {ApiClients, Argument, DataType, ApiMethod, CacheUpdateGetter} from '../types' +import {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType} from '../types' import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' From 69f2c1181bd73fa14f946608005e4f85acc4fa1d Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 8 Feb 2023 13:52:38 -0500 Subject: [PATCH 007/122] Change `updater` generic from being the same for all to being per function. --- .../src/hooks/Test/config.ts | 15 +++++----- .../commerce-sdk-react/src/hooks/types.ts | 16 +++++------ .../commerce-sdk-react/src/hooks/utils.ts | 28 +++++++++++++++++-- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts index 625bf1011d..278422e187 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/config.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/config.ts @@ -5,12 +5,11 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' -import {ApiClients, CacheUpdateMatrix} from '../types' -import {CacheUpdate} from '../types' +import {ApiClients, CacheUpdate, CacheUpdateMatrix} from '../types' type Basket = ShopperBasketsTypes.Basket -const updateBasketQuery = (basketId?: string): Pick, 'update'> => { +const updateBasketQuery = (basketId?: string): Pick => { // TODO: we're missing headers, rawResponse -> not only {basketId} const arg = {basketId} return basketId @@ -25,7 +24,7 @@ const updateBasketQuery = (basketId?: string): Pick, 'update : {} } -const removeBasketQuery = (basketId?: string): Pick, 'remove'> => { +const removeBasketQuery = (basketId?: string): Pick => { const arg = {basketId} return basketId ? { @@ -36,7 +35,7 @@ const removeBasketQuery = (basketId?: string): Pick, 'remove const invalidateCustomerBasketsQuery = ( customerId: string | null -): Pick, 'invalidate'> => { +): Pick => { // TODO: should we use arg here or not? The invalidate method does not need exact query key. const arg = {customerId} return customerId @@ -51,7 +50,7 @@ const updateBasketFromRequest = ( params: { parameters: Basket } -): CacheUpdate => ({ +): CacheUpdate => ({ ...updateBasketQuery(params.parameters.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -60,7 +59,7 @@ const updateBasketFromResponse = ( customerId: string | null, params: unknown, // not used, response: Basket -): CacheUpdate => ({ +): CacheUpdate => ({ ...updateBasketQuery(response.basketId), ...invalidateCustomerBasketsQuery(customerId) }) @@ -71,7 +70,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, params): CacheUpdate => ({ + deleteBasket: (customerId, params): CacheUpdate => ({ ...invalidateCustomerBasketsQuery(customerId), ...removeBasketQuery(params.parameters.basketId) }), diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index fe037655ac..095086eff2 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -75,9 +75,7 @@ export type Argument unknown> = NonNullable> = T extends ApiMethod - ? R - : never +export type DataType = T extends ApiMethod ? R : never // --- CACHE HELPERS --- // @@ -85,12 +83,12 @@ export type CacheUpdateBase = { queryKey: QueryKey // TODO: Can we do [...string, object]? } -export type CacheUpdateUpdate = CacheUpdateBase & { - updater: (data: Data | undefined) => Data | undefined +export type CacheUpdateUpdate = CacheUpdateBase & { + updater: (oldData: T) => T } -export type CacheUpdate = { - update?: CacheUpdateUpdate[] +export type CacheUpdate = { + update?: CacheUpdateUpdate[] invalidate?: CacheUpdateBase[] remove?: CacheUpdateBase[] } @@ -99,7 +97,7 @@ export type CacheUpdateGetter = ( customerId: string | null, params: Options, response: Data -) => CacheUpdate +) => CacheUpdate export type CacheUpdateMatrix = { // It feels like we should be able to do , but that @@ -109,3 +107,5 @@ export type CacheUpdateMatrix = { ? CacheUpdateGetter, Data> : never } + +export type ApiQueryKey = readonly [...path: string[], parameters: Record] diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index e1d93e765b..ed8d27fef4 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {QueryClient, QueryKey} from '@tanstack/react-query' -import {CacheUpdate} from './types' +import {ApiQueryKey, CacheUpdate} from './types' const isObject = (item: unknown): item is object => typeof item === 'object' && item !== null @@ -29,7 +29,7 @@ const deepEqual = (a: unknown, b: unknown): boolean => { const isMatchingKey = (cacheQueryKey: QueryKey, lookupQueryKey: QueryKey): boolean => lookupQueryKey.every((item, index) => deepEqual(item, cacheQueryKey[index])) -export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { +export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { // STEP 1. Update data inside query cache for the matching queryKeys cacheUpdates.update?.forEach(({queryKey, updater}) => { queryClient.setQueryData(queryKey, updater) @@ -58,3 +58,27 @@ export class NotImplementedError extends Error { export const hasAllKeys = (object: T, keys: ReadonlyArray): boolean => keys.every((key) => object[key] !== undefined) + +export const startsWith = + (search: readonly string[]) => + (queryKey: ApiQueryKey): boolean => + queryKey.length >= search.length && search.every((lookup, idx) => queryKey[idx] === lookup) + +export const endMatches = + (search: Record) => + (queryKey: ApiQueryKey): boolean => { + const parameters = queryKey[queryKey.length - 1] + if (typeof parameters !== 'object') return false + const searchEntries = Object.entries(search) + // Can't be a match if we're looking for more values than we have + if (searchEntries.length > Object.keys(parameters).length) return false + for (const [key, lookup] of searchEntries) { + if (parameters[key] !== lookup) return false + } + return true + } + +export const and = + (...funcs: Array<(...args: Args) => boolean>) => + (...args: Args) => + funcs.every((fn) => fn(...args)) From e7bd76d41b7dc573cae98a5a298db2d4e8525413 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 8 Feb 2023 14:09:07 -0500 Subject: [PATCH 008/122] Clean up cache update matrix. --- .../src/hooks/Test/config.ts | 56 ++++++++----------- .../commerce-sdk-react/src/hooks/types.ts | 11 ++-- .../commerce-sdk-react/src/hooks/utils.ts | 2 +- 3 files changed, 30 insertions(+), 39 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts index 278422e187..3ae3d0ffb8 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/config.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/config.ts @@ -5,59 +5,49 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' -import {ApiClients, CacheUpdate, CacheUpdateMatrix} from '../types' +import {ApiClients, ApiOptions, CacheUpdate, CacheUpdateMatrix} from '../types' type Basket = ShopperBasketsTypes.Basket const updateBasketQuery = (basketId?: string): Pick => { - // TODO: we're missing headers, rawResponse -> not only {basketId} - const arg = {basketId} - return basketId - ? { - update: [ - { - queryKey: ['/baskets', basketId, arg], - updater: (basket) => basket // TODO - } - ] - } - : {} + if (!basketId) return {} + return { + update: [ + { + queryKey: ['/baskets', basketId, {basketId}], + updater: (basket) => basket // TODO + } + ] + } } const removeBasketQuery = (basketId?: string): Pick => { - const arg = {basketId} - return basketId - ? { - remove: [{queryKey: ['/baskets', basketId, arg]}] - } - : {} + if (!basketId) return {} + return { + remove: [{queryKey: ['/baskets', basketId, {basketId}]}] + } } const invalidateCustomerBasketsQuery = ( customerId: string | null ): Pick => { - // TODO: should we use arg here or not? The invalidate method does not need exact query key. - const arg = {customerId} - return customerId - ? { - invalidate: [{queryKey: ['/customers', customerId, '/baskets', arg]}] - } - : {} + if (!customerId) return {} + return { + invalidate: [{queryKey: ['/customers', customerId, '/baskets', {customerId}]}] + } } const updateBasketFromRequest = ( customerId: string | null, - params: { - parameters: Basket - } + options: ApiOptions ): CacheUpdate => ({ - ...updateBasketQuery(params.parameters.basketId), + ...updateBasketQuery(options.parameters?.basketId), ...invalidateCustomerBasketsQuery(customerId) }) const updateBasketFromResponse = ( customerId: string | null, - params: unknown, // not used, + options: ApiOptions, // not used, response: Basket ): CacheUpdate => ({ ...updateBasketQuery(response.basketId), @@ -70,9 +60,9 @@ export const cacheUpdateMatrix: CacheUpdateMatrix removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, params): CacheUpdate => ({ + deleteBasket: (customerId, options): CacheUpdate => ({ ...invalidateCustomerBasketsQuery(customerId), - ...removeBasketQuery(params.parameters.basketId) + ...removeBasketQuery(options.parameters.basketId) }), mergeBasket: updateBasketFromResponse, // Response! removeCouponFromBasket: updateBasketFromRequest, diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 095086eff2..d15c7543fe 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -4,7 +4,6 @@ * 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 {QueryKey} from '@tanstack/react-query' import { ShopperBaskets, ShopperContexts, @@ -50,7 +49,7 @@ export type ApiClient = ApiClients[keyof ApiClients] export type ApiOptions< Parameters extends Record = Record, Headers extends Record = Record, - Body extends Record = Record + Body extends Record | unknown[] = Record | unknown[] > = { parameters?: Parameters headers?: Headers @@ -79,8 +78,12 @@ export type DataType = T extends ApiMethod ? R : nev // --- CACHE HELPERS --- // +export type ApiQueryKey = + // | readonly string[] // TODO: Is this needed? + readonly [...path: string[], parameters: Record] + export type CacheUpdateBase = { - queryKey: QueryKey // TODO: Can we do [...string, object]? + queryKey: ApiQueryKey } export type CacheUpdateUpdate = CacheUpdateBase & { @@ -107,5 +110,3 @@ export type CacheUpdateMatrix = { ? CacheUpdateGetter, Data> : never } - -export type ApiQueryKey = readonly [...path: string[], parameters: Record] diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index ed8d27fef4..710717bfff 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -26,7 +26,7 @@ const deepEqual = (a: unknown, b: unknown): boolean => { * @param lookupQueryKey partial query key to validate match against * @returns boolean */ -const isMatchingKey = (cacheQueryKey: QueryKey, lookupQueryKey: QueryKey): boolean => +const isMatchingKey = (cacheQueryKey: QueryKey, lookupQueryKey: ApiQueryKey): boolean => lookupQueryKey.every((item, index) => deepEqual(item, cacheQueryKey[index])) export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { From b0a8715a0f5be7b00f31be0dca1dec5116e970bf Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 8 Feb 2023 16:58:08 -0500 Subject: [PATCH 009/122] Rekerjigger cache logic to see what we can do with it. --- .../src/hooks/Test/config.ts | 57 ++++++++++++++----- .../commerce-sdk-react/src/hooks/types.ts | 16 +++--- .../commerce-sdk-react/src/hooks/utils.ts | 56 ++++-------------- 3 files changed, 64 insertions(+), 65 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts index 3ae3d0ffb8..411c6f7943 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/config.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/config.ts @@ -4,27 +4,55 @@ * 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 {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' -import {ApiClients, ApiOptions, CacheUpdate, CacheUpdateMatrix} from '../types' +import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' +import {ApiClients, ApiOptions, CacheUpdate, CacheUpdateMatrix, CacheUpdateUpdate} from '../types' +import {startsWith} from '../utils' type Basket = ShopperBasketsTypes.Basket +type CustomerBasketsResult = ShopperCustomersTypes.BasketsResult -const updateBasketQuery = (basketId?: string): Pick => { +const updateBasketQuery = ( + customerId: string | null, + basketId: string | undefined, + newBasket: Basket +): Pick => { if (!basketId) return {} - return { - update: [ - { - queryKey: ['/baskets', basketId, {basketId}], - updater: (basket) => basket // TODO + const update: Array | CacheUpdateUpdate> = [ + { + queryKey: ['/baskets', basketId, {basketId}], + updater: newBasket + } + ] + if (customerId) { + const updateCustomerBaskets: CacheUpdateUpdate = { + // Since we use baskets from customer basket query, we need to update it for any basket mutation + queryKey: ['/customers', customerId, '/baskets', {customerId}], + updater: (oldData) => { + // do not update if response basket is not part of existing customer baskets + if (!oldData?.baskets?.some((basket) => basket.basketId === basketId)) { + return undefined + } + const updatedBaskets = oldData.baskets.map((basket) => { + return basket.basketId === basketId ? newBasket : basket + }) + return { + ...oldData, + // TODO: Remove type assertion when RAML specs match + baskets: updatedBaskets as CustomerBasketsResult['baskets'] + } } - ] + } + update.push(updateCustomerBaskets) } + // TODO: This type assertion is so that we "forget" what type the updater uses. + // Is there a way to avoid the assertion? + return {update} as CacheUpdate } const removeBasketQuery = (basketId?: string): Pick => { if (!basketId) return {} return { - remove: [{queryKey: ['/baskets', basketId, {basketId}]}] + remove: [startsWith(['/baskets', basketId])] } } @@ -33,15 +61,16 @@ const invalidateCustomerBasketsQuery = ( ): Pick => { if (!customerId) return {} return { - invalidate: [{queryKey: ['/customers', customerId, '/baskets', {customerId}]}] + invalidate: [startsWith(['/customers', customerId, '/baskets'])] } } const updateBasketFromRequest = ( customerId: string | null, - options: ApiOptions + options: ApiOptions, + response: Basket ): CacheUpdate => ({ - ...updateBasketQuery(options.parameters?.basketId), + ...updateBasketQuery(customerId, options.parameters?.basketId, response), ...invalidateCustomerBasketsQuery(customerId) }) @@ -50,7 +79,7 @@ const updateBasketFromResponse = ( options: ApiOptions, // not used, response: Basket ): CacheUpdate => ({ - ...updateBasketQuery(response.basketId), + ...updateBasketQuery(customerId, response.basketId, response), ...invalidateCustomerBasketsQuery(customerId) }) diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index d15c7543fe..c410d24e61 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -4,6 +4,7 @@ * 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 {Query, Updater} from '@tanstack/react-query' import { ShopperBaskets, ShopperContexts, @@ -82,18 +83,19 @@ export type ApiQueryKey = // | readonly string[] // TODO: Is this needed? readonly [...path: string[], parameters: Record] -export type CacheUpdateBase = { +export type CacheUpdateUpdate = { queryKey: ApiQueryKey + updater: Updater } -export type CacheUpdateUpdate = CacheUpdateBase & { - updater: (oldData: T) => T -} +export type CacheUpdateInvalidate = (query: Query) => boolean + +export type CacheUpdateRemove = (query: Query) => boolean export type CacheUpdate = { - update?: CacheUpdateUpdate[] - invalidate?: CacheUpdateBase[] - remove?: CacheUpdateBase[] + update?: CacheUpdateUpdate[] + invalidate?: CacheUpdateInvalidate[] + remove?: CacheUpdateRemove[] } export type CacheUpdateGetter = ( diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 710717bfff..bb98ef48d0 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -4,50 +4,15 @@ * 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 {QueryClient, QueryKey} from '@tanstack/react-query' -import {ApiQueryKey, CacheUpdate} from './types' - -const isObject = (item: unknown): item is object => typeof item === 'object' && item !== null - -const deepEqual = (a: unknown, b: unknown): boolean => { - if (!isObject(a) || !isObject(b)) return a === b - const aEntries = Object.entries(a) - const bEntries = Object.entries(b) - if (aEntries.length !== bEntries.length) return false - return aEntries.every(([aKey, aValue], index) => { - const [bKey, bValue] = bEntries[index] - return aKey === bKey && deepEqual(aValue, bValue) - }) -} - -/** - * Determines whether the cache query key starts with the same values as the lookup query key - * @param cacheQueryKey query key from the cache - * @param lookupQueryKey partial query key to validate match against - * @returns boolean - */ -const isMatchingKey = (cacheQueryKey: QueryKey, lookupQueryKey: ApiQueryKey): boolean => - lookupQueryKey.every((item, index) => deepEqual(item, cacheQueryKey[index])) +import {Query, QueryClient} from '@tanstack/react-query' +import {CacheUpdate} from './types' export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { - // STEP 1. Update data inside query cache for the matching queryKeys - cacheUpdates.update?.forEach(({queryKey, updater}) => { + cacheUpdates.update?.forEach(({queryKey, updater}) => queryClient.setQueryData(queryKey, updater) - }) - - // STEP 2. Invalidate cache entries with the matching queryKeys - cacheUpdates.invalidate?.forEach(({queryKey}) => { - queryClient.invalidateQueries({ - predicate: (cacheQuery) => isMatchingKey(cacheQuery.queryKey, queryKey) - }) - }) - - // STEP 3. Remove cache entries with the matching queryKeys - cacheUpdates.remove?.forEach(({queryKey}) => { - queryClient.removeQueries({ - predicate: (cacheQuery) => isMatchingKey(cacheQuery.queryKey, queryKey) - }) - }) + ) + cacheUpdates.invalidate?.forEach((predicate) => queryClient.invalidateQueries({predicate})) + cacheUpdates.remove?.forEach((predicate) => queryClient.removeQueries({predicate})) } export class NotImplementedError extends Error { @@ -56,19 +21,22 @@ export class NotImplementedError extends Error { } } +export const isObject = (obj: unknown): obj is Record => + typeof obj === 'object' && obj !== null + export const hasAllKeys = (object: T, keys: ReadonlyArray): boolean => keys.every((key) => object[key] !== undefined) export const startsWith = (search: readonly string[]) => - (queryKey: ApiQueryKey): boolean => + ({queryKey}: Query): boolean => queryKey.length >= search.length && search.every((lookup, idx) => queryKey[idx] === lookup) export const endMatches = (search: Record) => - (queryKey: ApiQueryKey): boolean => { + ({queryKey}: Query): boolean => { const parameters = queryKey[queryKey.length - 1] - if (typeof parameters !== 'object') return false + if (!isObject(parameters)) return false const searchEntries = Object.entries(search) // Can't be a match if we're looking for more values than we have if (searchEntries.length > Object.keys(parameters).length) return false From 107cd5b16b59bfe75e2b2bc95f3f04489adf12b0 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 9 Feb 2023 14:35:01 -0500 Subject: [PATCH 010/122] Create options merge util. --- .../src/hooks/Test/mutation.ts | 5 ++-- .../commerce-sdk-react/src/hooks/types.ts | 10 ++++++-- .../src/hooks/useMutation.ts | 18 +++++++++++---- .../commerce-sdk-react/src/hooks/useQuery.ts | 7 ++---- .../commerce-sdk-react/src/hooks/utils.ts | 23 ++++++++++++++++++- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts index 18288de1a8..2dd86ede84 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts @@ -4,7 +4,7 @@ * 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType} from '../types' +import {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' @@ -61,7 +61,8 @@ export function useShopperBasketsMutation type Data = DataType return useMutation({ + client, method: (opts: Options) => (client[mutation] as ApiMethod)(opts), - getCacheUpdates: getCacheUpdates as CacheUpdateGetter + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> }) } diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 2fd3e5d196..6e44336d6c 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -50,9 +50,9 @@ export type ApiClient = ApiClients[keyof ApiClients] * Generic signature of the options objects used by commerce-sdk-isomorphic */ export type ApiOptions< - Parameters extends Record = Record, + Parameters extends object = Record, Headers extends Record = Record, - Body extends Record | unknown[] = Record | unknown[] + Body extends object | unknown[] | undefined = Record | unknown[] | undefined > = { parameters?: Parameters headers?: Headers @@ -114,3 +114,9 @@ export type CacheUpdateMatrix = { ? CacheUpdateGetter, Data> : never } + +export type MergedOptions = ApiOptions< + Client['clientConfig']['parameters'] & Options['parameters'], + Client['clientConfig']['headers'] & Options['headers'], + Options['body'] +> diff --git a/packages/commerce-sdk-react/src/hooks/useMutation.ts b/packages/commerce-sdk-react/src/hooks/useMutation.ts index b6ec331792..1734818415 100644 --- a/packages/commerce-sdk-react/src/hooks/useMutation.ts +++ b/packages/commerce-sdk-react/src/hooks/useMutation.ts @@ -5,14 +5,19 @@ * 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} from './types' +import {CacheUpdateGetter, ApiOptions, ApiMethod, ApiClient, MergedOptions} from './types' import {useAuthorizationHeader} from './useAuthorizationHeader' import useCustomerId from './useCustomerId' -import {updateCache} from './utils' +import {mergeOptions, updateCache} from './utils' -export const useMutation = (hookConfig: { +export const useMutation = < + Client extends ApiClient, + Options extends ApiOptions, + Data +>(hookConfig: { + client: Client method: ApiMethod - getCacheUpdates: CacheUpdateGetter + getCacheUpdates: CacheUpdateGetter, Data> }) => { const queryClient = useQueryClient() const customerId = useCustomerId() @@ -20,7 +25,10 @@ export const useMutation = (hookConfig: { return useReactQueryMutation(authenticatedMethod, { onSuccess: (data, options) => { - const cacheUpdates = hookConfig.getCacheUpdates(customerId, options, data) + // commerce-sdk-isomorphic merges `clientConfig` and `options` under the hood, + // so we also need to do that to get the "net" options that are actually sent to SCAPI. + const netOptions = mergeOptions(hookConfig.client, options) + const cacheUpdates = hookConfig.getCacheUpdates(customerId, netOptions, data) cacheUpdates.update?.forEach(({updater}) => updater) updateCache(queryClient, cacheUpdates) } diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index 14976b859e..60c841151c 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -7,7 +7,7 @@ import {useQuery as useReactQuery, UseQueryOptions, QueryKey} from '@tanstack/react-query' import {useAuthorizationHeader} from './useAuthorizationHeader' import {ApiClient, ApiMethod, ApiOptions} from './types' -import {hasAllKeys} from './utils' +import {hasAllKeys, mergeOptions} from './utils' export const useQuery = < Options extends Omit, @@ -26,10 +26,7 @@ export const useQuery = < enabled?: boolean } ) => { - const parameters = { - ...hookConfig.client.clientConfig.parameters, - ...apiOptions.parameters - } + const {parameters} = mergeOptions(hookConfig.client, apiOptions) const authenticatedMethod = useAuthorizationHeader(hookConfig.method) return useReactQuery( // End user can override queryKey if they really want to... diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index bb98ef48d0..e7e7fc135d 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {Query, QueryClient} from '@tanstack/react-query' -import {CacheUpdate} from './types' +import {ApiClient, ApiOptions, CacheUpdate, MergedOptions} from './types' export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { cacheUpdates.update?.forEach(({queryKey, updater}) => @@ -50,3 +50,24 @@ export const and = (...funcs: Array<(...args: Args) => boolean>) => (...args: Args) => funcs.every((fn) => fn(...args)) + +/** + * Merges headers and parameters from client config into the options, mimicking the behavior + * of commerce-sdk-isomorphic. + */ +export const mergeOptions = ( + client: Client, + options: Options +): MergedOptions => { + return { + ...options, + headers: { + ...client.clientConfig.headers, + ...options.headers + }, + parameters: { + ...client.clientConfig.parameters, + ...options.parameters + } + } +} From ab586ec0068aca21571eb59dfa60de6720c3a000 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 9 Feb 2023 15:08:43 -0500 Subject: [PATCH 011/122] Use merged options in query hooks and simplify query keys. --- .../src/hooks/Test/query.ts | 61 +++++-------------- .../commerce-sdk-react/src/hooks/types.ts | 27 ++++++-- .../commerce-sdk-react/src/hooks/useQuery.ts | 10 +-- 3 files changed, 40 insertions(+), 58 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/query.ts b/packages/commerce-sdk-react/src/hooks/Test/query.ts index 978b81aa9d..37981d8f03 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/query.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/query.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType} from '../types' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' @@ -27,21 +27,12 @@ export const useBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({parameters}: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, - '?', - 'siteId', - parameters.siteId, - 'locale', - parameters.locale, // Full parameters last for easy lookup parameters ] as const @@ -73,22 +64,15 @@ export const usePaymentMethodsForBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/payment-methods', - '?', - 'siteId', - parameters.siteId, - 'locale', - parameters.locale, // Full parameters last for easy lookup parameters ] as const @@ -117,20 +101,15 @@ export const usePriceBooksForBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/price-books', - '?', - 'siteId', - parameters.siteId, // Full parameters last for easy lookup parameters ] as const @@ -162,12 +141,10 @@ export const useShippingMethodsForShipment = ( const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', @@ -175,11 +152,6 @@ export const useShippingMethodsForShipment = ( '/shipments/', parameters.shipmentId, '/shipping-methods', - '?', - 'siteId', - parameters.siteId, - 'locale', - parameters.locale, // Full parameters last for easy lookup parameters ] as const @@ -207,20 +179,15 @@ export const useTaxesFromBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/taxes', - '?', - 'siteId', - parameters.siteId, // Full parameters last for easy lookup parameters ] as const diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 6e44336d6c..e62dad95b5 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -19,6 +19,14 @@ import { ShopperSearch } from 'commerce-sdk-isomorphic' +// --- GENERAL UTILITIES --- // + +/** + * Marks the given keys as required. + */ +// The outer Pick<...> is used to prettify the result type +type RequireKeys = Pick>, keyof T> + // --- API CLIENTS --- // export type ApiClientConfigParams = { @@ -79,6 +87,19 @@ export type Argument unknown> = NonNullable = T extends ApiMethod ? R : never +/** + * Merged headers and parameters from client config and options, mimicking the behavior + * of commerce-sdk-isomorphic. + */ +export type MergedOptions = RequireKeys< + ApiOptions< + NonNullable, + NonNullable, + Options['body'] + >, + 'parameters' | 'headers' +> + // --- CACHE HELPERS --- // export type ApiQueryKey = @@ -114,9 +135,3 @@ export type CacheUpdateMatrix = { ? CacheUpdateGetter, Data> : never } - -export type MergedOptions = ApiOptions< - Client['clientConfig']['parameters'] & Options['parameters'], - Client['clientConfig']['headers'] & Options['headers'], - Options['body'] -> diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index 60c841151c..b17d8d8806 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -6,7 +6,7 @@ */ import {useQuery as useReactQuery, UseQueryOptions, QueryKey} from '@tanstack/react-query' import {useAuthorizationHeader} from './useAuthorizationHeader' -import {ApiClient, ApiMethod, ApiOptions} from './types' +import {ApiClient, ApiMethod, ApiOptions, MergedOptions} from './types' import {hasAllKeys, mergeOptions} from './utils' export const useQuery = < @@ -21,21 +21,21 @@ export const useQuery = < hookConfig: { client: Client method: ApiMethod - getQueryKey: (parameters: Options['parameters']) => QK + getQueryKey: (options: MergedOptions) => QK requiredParameters: ReadonlyArray enabled?: boolean } ) => { - const {parameters} = mergeOptions(hookConfig.client, apiOptions) + const netOptions = mergeOptions(hookConfig.client, apiOptions) const authenticatedMethod = useAuthorizationHeader(hookConfig.method) return useReactQuery( // End user can override queryKey if they really want to... - queryOptions.queryKey ?? hookConfig.getQueryKey(parameters), + queryOptions.queryKey ?? hookConfig.getQueryKey(netOptions), () => authenticatedMethod(apiOptions), { enabled: hookConfig.enabled !== false && - hasAllKeys(parameters, hookConfig.requiredParameters), + hasAllKeys(netOptions.parameters, hookConfig.requiredParameters), ...queryOptions } ) From c28d2493d2f6d35c7d1124997cdfc8ade5b5a517 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 9 Feb 2023 15:15:51 -0500 Subject: [PATCH 012/122] Simplify endMatches logic. --- packages/commerce-sdk-react/src/hooks/utils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index e7e7fc135d..5e4f59cdf6 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -38,12 +38,11 @@ export const endMatches = const parameters = queryKey[queryKey.length - 1] if (!isObject(parameters)) return false const searchEntries = Object.entries(search) - // Can't be a match if we're looking for more values than we have - if (searchEntries.length > Object.keys(parameters).length) return false - for (const [key, lookup] of searchEntries) { - if (parameters[key] !== lookup) return false - } - return true + return ( + // Can't be a match if we're looking for more values than we have + searchEntries.length > Object.keys(parameters).length && + searchEntries.every(([key, lookup]) => parameters[key] === lookup) + ) } export const and = From 623e1de5454e31e107eb399bf7634dafa0981fd6 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 9 Feb 2023 16:52:32 -0500 Subject: [PATCH 013/122] Add predicate helper to match API config values. --- .../src/hooks/Test/config.ts | 41 +++++++++++++------ .../commerce-sdk-react/src/hooks/types.ts | 11 +++-- .../commerce-sdk-react/src/hooks/utils.ts | 33 +++++++++++++-- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts index 411c6f7943..9fb461d6f6 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/config.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/config.ts @@ -5,11 +5,20 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' -import {ApiClients, ApiOptions, CacheUpdate, CacheUpdateMatrix, CacheUpdateUpdate} from '../types' -import {startsWith} from '../utils' +import { + ApiClients, + ApiOptions, + CacheUpdate, + CacheUpdateMatrix, + CacheUpdateUpdate, + MergedOptions +} from '../types' +import {and, matchesApiConfig, pathStartsWith} from '../utils' +type Client = ApiClients['shopperBaskets'] type Basket = ShopperBasketsTypes.Basket type CustomerBasketsResult = ShopperCustomersTypes.BasketsResult +type BasketOptions = MergedOptions> const updateBasketQuery = ( customerId: string | null, @@ -49,49 +58,55 @@ const updateBasketQuery = ( return {update} as CacheUpdate } -const removeBasketQuery = (basketId?: string): Pick => { +const removeBasketQuery = ( + options: BasketOptions, + basketId?: string +): Pick => { if (!basketId) return {} return { - remove: [startsWith(['/baskets', basketId])] + remove: [and(matchesApiConfig(options), pathStartsWith(['/baskets', basketId]))] } } const invalidateCustomerBasketsQuery = ( - customerId: string | null + customerId: string | null, + options: BasketOptions ): Pick => { if (!customerId) return {} return { - invalidate: [startsWith(['/customers', customerId, '/baskets'])] + invalidate: [ + and(matchesApiConfig(options), pathStartsWith(['/customers', customerId, '/baskets'])) + ] } } const updateBasketFromRequest = ( customerId: string | null, - options: ApiOptions, + options: BasketOptions, response: Basket ): CacheUpdate => ({ ...updateBasketQuery(customerId, options.parameters?.basketId, response), - ...invalidateCustomerBasketsQuery(customerId) + ...invalidateCustomerBasketsQuery(customerId, options) }) const updateBasketFromResponse = ( customerId: string | null, - options: ApiOptions, // not used, + options: BasketOptions, response: Basket ): CacheUpdate => ({ ...updateBasketQuery(customerId, response.basketId, response), - ...invalidateCustomerBasketsQuery(customerId) + ...invalidateCustomerBasketsQuery(customerId, options) }) -export const cacheUpdateMatrix: CacheUpdateMatrix = { +export const cacheUpdateMatrix: CacheUpdateMatrix = { addCouponToBasket: updateBasketFromRequest, addItemToBasket: updateBasketFromRequest, removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! deleteBasket: (customerId, options): CacheUpdate => ({ - ...invalidateCustomerBasketsQuery(customerId), - ...removeBasketQuery(options.parameters.basketId) + ...invalidateCustomerBasketsQuery(customerId, options), + ...removeBasketQuery(options) }), mergeBasket: updateBasketFromResponse, // Response! removeCouponFromBasket: updateBasketFromRequest, diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index e62dad95b5..84af4ca01d 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -95,7 +95,10 @@ export type MergedOptions ApiOptions< NonNullable, NonNullable, - Options['body'] + // `body` may not exist on `Options`, in which case it is `unknown` here. Due to the type + // constraint in `ApiOptions`, that is not a valid value. We must replace it with `never` + // to indicate that the result type does not have a `body`. + unknown extends Options['body'] ? never : Options['body'] >, 'parameters' | 'headers' > @@ -123,15 +126,15 @@ export type CacheUpdate = { export type CacheUpdateGetter = ( customerId: string | null, - params: Options, + options: Options, response: Data ) => CacheUpdate -export type CacheUpdateMatrix = { +export type CacheUpdateMatrix = { // It feels like we should be able to do , but that // results in some methods being `never`, so we just use Argument<> later // eslint-disable-next-line @typescript-eslint/no-explicit-any [Method in keyof Client]?: Client[Method] extends ApiMethod - ? CacheUpdateGetter, Data> + ? CacheUpdateGetter>, Data> : never } diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 5e4f59cdf6..89926fc0f0 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -27,12 +27,12 @@ export const isObject = (obj: unknown): obj is Record => export const hasAllKeys = (object: T, keys: ReadonlyArray): boolean => keys.every((key) => object[key] !== undefined) -export const startsWith = +export const pathStartsWith = (search: readonly string[]) => ({queryKey}: Query): boolean => queryKey.length >= search.length && search.every((lookup, idx) => queryKey[idx] === lookup) -export const endMatches = +export const matchParametersStrict = (search: Record) => ({queryKey}: Query): boolean => { const parameters = queryKey[queryKey.length - 1] @@ -41,10 +41,32 @@ export const endMatches = return ( // Can't be a match if we're looking for more values than we have searchEntries.length > Object.keys(parameters).length && + // TODO: Support arrays - parameters can also be string | number searchEntries.every(([key, lookup]) => parameters[key] === lookup) ) } +const matchParameters = (parameters: Record, keys = Object.keys(parameters)) => { + const search: Record = {} + for (const key of keys) { + if (parameters[key] !== undefined) search[key] = parameters[key] + } + return matchParametersStrict(search) +} + +export const matchesApiConfig = (parameters: Record) => + matchParameters(parameters, [ + 'clientId', + 'currency', // TODO: maybe? + 'locale', // TODO: maybe? + 'organizationId', + 'shortCode', + 'siteId', + // Version is never used directly by us, but is set on the client config + // in `commerce-sdk-isomorphic`, so we include it here for completeness + 'version' + ]) + export const and = (...funcs: Array<(...args: Args) => boolean>) => (...args: Args) => @@ -58,7 +80,7 @@ export const mergeOptions = => { - return { + const merged = { ...options, headers: { ...client.clientConfig.headers, @@ -69,4 +91,9 @@ export const mergeOptions = Date: Thu, 9 Feb 2023 16:52:41 -0500 Subject: [PATCH 014/122] Fix temp test file. --- packages/commerce-sdk-react/src/hooks/Test/temp.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/commerce-sdk-react/src/hooks/Test/temp.ts b/packages/commerce-sdk-react/src/hooks/Test/temp.ts index ee34447524..2d6596fe92 100644 --- a/packages/commerce-sdk-react/src/hooks/Test/temp.ts +++ b/packages/commerce-sdk-react/src/hooks/Test/temp.ts @@ -21,7 +21,12 @@ const queryData = query.data const updates = cacheUpdateMatrix.removeItemFromBasket?.( 'customerId', { + headers: {}, parameters: { + shortCode: 'shortCode', + clientId: 'clientId', + siteId: 'siteId', + organizationId: 'organizationId', basketId: 'basketId', itemId: 'itemId' } From 80c0fe8c6b9e8ce78eaee83323d468aaae049246 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 15:36:44 -0500 Subject: [PATCH 015/122] Use consistent order of generic parameters. --- packages/commerce-sdk-react/src/hooks/useQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index b17d8d8806..b1cf6afc05 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -10,8 +10,8 @@ import {ApiClient, ApiMethod, ApiOptions, MergedOptions} from './types' import {hasAllKeys, mergeOptions} from './utils' export const useQuery = < - Options extends Omit, Client extends ApiClient, + Options extends Omit, Data, Err, QK extends QueryKey From 2bd6fc7d74e62b07cfc00be82d86c114f7b68d6e Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 15:38:22 -0500 Subject: [PATCH 016/122] Remove pointless line. --- packages/commerce-sdk-react/src/hooks/useMutation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/useMutation.ts b/packages/commerce-sdk-react/src/hooks/useMutation.ts index 1734818415..3bbe97aece 100644 --- a/packages/commerce-sdk-react/src/hooks/useMutation.ts +++ b/packages/commerce-sdk-react/src/hooks/useMutation.ts @@ -29,7 +29,6 @@ export const useMutation = < // so we also need to do that to get the "net" options that are actually sent to SCAPI. const netOptions = mergeOptions(hookConfig.client, options) const cacheUpdates = hookConfig.getCacheUpdates(customerId, netOptions, data) - cacheUpdates.update?.forEach(({updater}) => updater) updateCache(queryClient, cacheUpdates) } }) From 6f26e05dacdea3c05a05dad4dbe0b5e769dbf51b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 15:54:15 -0500 Subject: [PATCH 017/122] Use consistent generic parameter names. --- .../src/hooks/useAuthorizationHeader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts index d3b252991a..cc876e66e7 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts @@ -7,13 +7,13 @@ import {ApiOptions, ApiMethod} from './types' import useAuth from './useAuth' -export const useAuthorizationHeader = ( - fn: ApiMethod -): ApiMethod => { +export const useAuthorizationHeader = ( + method: ApiMethod +): ApiMethod => { const auth = useAuth() - return async (options: Opts) => { + return async (options) => { const {access_token} = await auth.ready() - return await fn({ + return await method({ ...options, headers: { ...options.headers, From 7632df11ce0c109f145bd5c1eddd77fb2bfe89da Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 16:06:12 -0500 Subject: [PATCH 018/122] Add comments. --- packages/commerce-sdk-react/src/hooks/types.ts | 13 +++++++++++++ .../src/hooks/useAuthorizationHeader.ts | 5 +++++ .../commerce-sdk-react/src/hooks/useMutation.ts | 5 +++++ packages/commerce-sdk-react/src/hooks/useQuery.ts | 7 +++++++ packages/commerce-sdk-react/src/hooks/utils.ts | 15 +++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 84af4ca01d..c082c2b992 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -105,31 +105,44 @@ export type MergedOptions // --- CACHE HELPERS --- // +/** + * Query key interface used by API query hooks. + */ export type ApiQueryKey = // | readonly string[] // TODO: Is this needed? readonly [...path: string[], parameters: Record] +/** + * Interface to update a cached API response. + * @property queryKey - The query key to update + * @property updater - Either the new data or a function that accepts old data and returns new data + */ export type CacheUpdateUpdate = { queryKey: ApiQueryKey updater: Updater } +/** Query predicate for queries to invalidate */ export type CacheUpdateInvalidate = (query: Query) => boolean +/** Query predicate for queries to remove */ export type CacheUpdateRemove = (query: Query) => boolean +/** Collection of updates to make to the cache when a request completes. */ export type CacheUpdate = { update?: CacheUpdateUpdate[] invalidate?: CacheUpdateInvalidate[] remove?: CacheUpdateRemove[] } +/** Generates a collection of cache updates to make for a given request. */ export type CacheUpdateGetter = ( customerId: string | null, options: Options, response: Data ) => CacheUpdate +/** Collection of cache update getters for each method of an API client. */ export type CacheUpdateMatrix = { // It feels like we should be able to do , but that // results in some methods being `never`, so we just use Argument<> later diff --git a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts index cc876e66e7..61c00d2808 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts @@ -7,6 +7,11 @@ import {ApiOptions, ApiMethod} from './types' import useAuth from './useAuth' +/** + * Creates a method that waits for authentication to complete and automatically includes an + * Authorization header when making requests. + * @param method Bound API method + */ export const useAuthorizationHeader = ( method: ApiMethod ): ApiMethod => { diff --git a/packages/commerce-sdk-react/src/hooks/useMutation.ts b/packages/commerce-sdk-react/src/hooks/useMutation.ts index 3bbe97aece..41f006f35e 100644 --- a/packages/commerce-sdk-react/src/hooks/useMutation.ts +++ b/packages/commerce-sdk-react/src/hooks/useMutation.ts @@ -10,6 +10,11 @@ import {useAuthorizationHeader} from './useAuthorizationHeader' import useCustomerId from './useCustomerId' import {mergeOptions, updateCache} from './utils' +/** + * Helper for mutation hooks, contains most of the logic in order to keep individual hooks small. + * @param hookConfig - Config values that vary per API endpoint + * @internal + */ export const useMutation = < Client extends ApiClient, Options extends ApiOptions, diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index b1cf6afc05..d988714acd 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -9,6 +9,13 @@ import {useAuthorizationHeader} from './useAuthorizationHeader' import {ApiClient, ApiMethod, ApiOptions, MergedOptions} from './types' import {hasAllKeys, mergeOptions} from './utils' +/** + * Helper for query hooks, contains most of the logic in order to keep individual hooks small. + * @param apiOptions - Options passed through to commerce-sdk-isomorphic + * @param queryOptions - Options passed through to @tanstack/react-query + * @param hookConfig - Config values that vary per API endpoint + * @internal + */ export const useQuery = < Client extends ApiClient, Options extends Omit, diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 89926fc0f0..97f50698b8 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -7,6 +7,7 @@ import {Query, QueryClient} from '@tanstack/react-query' import {ApiClient, ApiOptions, CacheUpdate, MergedOptions} from './types' +/** Applies the set of cache updates to the query client. */ export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { cacheUpdates.update?.forEach(({queryKey, updater}) => queryClient.setQueryData(queryKey, updater) @@ -15,23 +16,31 @@ export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) cacheUpdates.remove?.forEach((predicate) => queryClient.removeQueries({predicate})) } +/** Error thrown when a method is not implemented. */ export class NotImplementedError extends Error { constructor(method = 'This method') { super(`${method} is not implemented.`) } } +/** Determines whether a value is an object. */ export const isObject = (obj: unknown): obj is Record => typeof obj === 'object' && obj !== null +/** Determines whether a value has all of the given keys. */ export const hasAllKeys = (object: T, keys: ReadonlyArray): boolean => keys.every((key) => object[key] !== undefined) +/** Creates a query predicate that determines whether a query key starts with the given path segments. */ export const pathStartsWith = (search: readonly string[]) => ({queryKey}: Query): boolean => queryKey.length >= search.length && search.every((lookup, idx) => queryKey[idx] === lookup) +/** + * Creates a query predicate that determines whether the parameters of the query key exactly match + * the search object. + */ export const matchParametersStrict = (search: Record) => ({queryKey}: Query): boolean => { @@ -46,6 +55,10 @@ export const matchParametersStrict = ) } +/** + * Creates a query predicate that determines whether the parameters of the query key match the + * search object, for the subset of given keys present on the search object. + */ const matchParameters = (parameters: Record, keys = Object.keys(parameters)) => { const search: Record = {} for (const key of keys) { @@ -54,6 +67,7 @@ const matchParameters = (parameters: Record, keys = Object.keys return matchParametersStrict(search) } +/** Creates a query predicate that matches against common API config parameters. */ export const matchesApiConfig = (parameters: Record) => matchParameters(parameters, [ 'clientId', @@ -67,6 +81,7 @@ export const matchesApiConfig = (parameters: Record) => 'version' ]) +/** Creates a query predicate that returns true if all of the given predicates return true. */ export const and = (...funcs: Array<(...args: Args) => boolean>) => (...args: Args) => From 8ad426cb0a2d558b1fdacc5008f742c059973454 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 16:17:51 -0500 Subject: [PATCH 019/122] More comments. --- packages/commerce-sdk-react/src/hooks/useQuery.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index d988714acd..0ffebc8972 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -41,8 +41,11 @@ export const useQuery = < () => authenticatedMethod(apiOptions), { enabled: + // Individual hooks can provide `enabled` checks that are done in ADDITION to + // the required parameter check hookConfig.enabled !== false && hasAllKeys(netOptions.parameters, hookConfig.requiredParameters), + // End users can always completely OVERRIDE the default `enabled` check ...queryOptions } ) From 9e402c2904f8d60020834973491979d4e743d942 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 16:20:50 -0500 Subject: [PATCH 020/122] Remove test directory. --- .../src/hooks/Test/config.ts | 121 ----------- .../src/hooks/Test/index.ts | 8 - .../src/hooks/Test/mutation.ts | 68 ------ .../src/hooks/Test/query.ts | 201 ------------------ .../commerce-sdk-react/src/hooks/Test/temp.ts | 45 ---- 5 files changed, 443 deletions(-) delete mode 100644 packages/commerce-sdk-react/src/hooks/Test/config.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/Test/index.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/Test/mutation.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/Test/query.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/Test/temp.ts diff --git a/packages/commerce-sdk-react/src/hooks/Test/config.ts b/packages/commerce-sdk-react/src/hooks/Test/config.ts deleted file mode 100644 index 9fb461d6f6..0000000000 --- a/packages/commerce-sdk-react/src/hooks/Test/config.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2023, 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 {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' -import { - ApiClients, - ApiOptions, - CacheUpdate, - CacheUpdateMatrix, - CacheUpdateUpdate, - MergedOptions -} from '../types' -import {and, matchesApiConfig, pathStartsWith} from '../utils' - -type Client = ApiClients['shopperBaskets'] -type Basket = ShopperBasketsTypes.Basket -type CustomerBasketsResult = ShopperCustomersTypes.BasketsResult -type BasketOptions = MergedOptions> - -const updateBasketQuery = ( - customerId: string | null, - basketId: string | undefined, - newBasket: Basket -): Pick => { - if (!basketId) return {} - const update: Array | CacheUpdateUpdate> = [ - { - queryKey: ['/baskets', basketId, {basketId}], - updater: newBasket - } - ] - if (customerId) { - const updateCustomerBaskets: CacheUpdateUpdate = { - // Since we use baskets from customer basket query, we need to update it for any basket mutation - queryKey: ['/customers', customerId, '/baskets', {customerId}], - updater: (oldData) => { - // do not update if response basket is not part of existing customer baskets - if (!oldData?.baskets?.some((basket) => basket.basketId === basketId)) { - return undefined - } - const updatedBaskets = oldData.baskets.map((basket) => { - return basket.basketId === basketId ? newBasket : basket - }) - return { - ...oldData, - // TODO: Remove type assertion when RAML specs match - baskets: updatedBaskets as CustomerBasketsResult['baskets'] - } - } - } - update.push(updateCustomerBaskets) - } - // TODO: This type assertion is so that we "forget" what type the updater uses. - // Is there a way to avoid the assertion? - return {update} as CacheUpdate -} - -const removeBasketQuery = ( - options: BasketOptions, - basketId?: string -): Pick => { - if (!basketId) return {} - return { - remove: [and(matchesApiConfig(options), pathStartsWith(['/baskets', basketId]))] - } -} - -const invalidateCustomerBasketsQuery = ( - customerId: string | null, - options: BasketOptions -): Pick => { - if (!customerId) return {} - return { - invalidate: [ - and(matchesApiConfig(options), pathStartsWith(['/customers', customerId, '/baskets'])) - ] - } -} - -const updateBasketFromRequest = ( - customerId: string | null, - options: BasketOptions, - response: Basket -): CacheUpdate => ({ - ...updateBasketQuery(customerId, options.parameters?.basketId, response), - ...invalidateCustomerBasketsQuery(customerId, options) -}) - -const updateBasketFromResponse = ( - customerId: string | null, - options: BasketOptions, - response: Basket -): CacheUpdate => ({ - ...updateBasketQuery(customerId, response.basketId, response), - ...invalidateCustomerBasketsQuery(customerId, options) -}) - -export const cacheUpdateMatrix: CacheUpdateMatrix = { - addCouponToBasket: updateBasketFromRequest, - addItemToBasket: updateBasketFromRequest, - removeItemFromBasket: updateBasketFromRequest, - addPaymentInstrumentToBasket: updateBasketFromRequest, - createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, options): CacheUpdate => ({ - ...invalidateCustomerBasketsQuery(customerId, options), - ...removeBasketQuery(options) - }), - mergeBasket: updateBasketFromResponse, // Response! - removeCouponFromBasket: updateBasketFromRequest, - removePaymentInstrumentFromBasket: updateBasketFromRequest, - updateBasket: updateBasketFromRequest, - updateBillingAddressForBasket: updateBasketFromRequest, - updateCustomerForBasket: updateBasketFromRequest, - updateItemInBasket: updateBasketFromRequest, - updatePaymentInstrumentInBasket: updateBasketFromRequest, - updateShippingAddressForShipment: updateBasketFromRequest, - updateShippingMethodForShipment: updateBasketFromRequest -} diff --git a/packages/commerce-sdk-react/src/hooks/Test/index.ts b/packages/commerce-sdk-react/src/hooks/Test/index.ts deleted file mode 100644 index 1a13c64feb..0000000000 --- a/packages/commerce-sdk-react/src/hooks/Test/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright (c) 2023, 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 - */ -export * from './mutation' -export * from './query' diff --git a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts b/packages/commerce-sdk-react/src/hooks/Test/mutation.ts deleted file mode 100644 index 2dd86ede84..0000000000 --- a/packages/commerce-sdk-react/src/hooks/Test/mutation.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2023, 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' -import {useMutation} from '../useMutation' -import {UseMutationResult} from '@tanstack/react-query' -import {NotImplementedError} from '../utils' -import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' - -type Client = ApiClients['shopperBaskets'] - -export const ShopperBasketsMutations = { - CreateBasket: 'createBasket', - TransferBasket: 'transferBasket', - MergeBasket: 'mergeBasket', - DeleteBasket: 'deleteBasket', - UpdateBasket: 'updateBasket', - UpdateBillingAddressForBasket: 'updateBillingAddressForBasket', - AddCouponToBasket: 'addCouponToBasket', - RemoveCouponFromBasket: 'removeCouponFromBasket', - UpdateCustomerForBasket: 'updateCustomerForBasket', - AddGiftCertificateItemToBasket: 'addGiftCertificateItemToBasket', - RemoveGiftCertificateItemFromBasket: 'removeGiftCertificateItemFromBasket', - UpdateGiftCertificateItemInBasket: 'updateGiftCertificateItemInBasket', - AddItemToBasket: 'addItemToBasket', - RemoveItemFromBasket: 'removeItemFromBasket', - UpdateItemInBasket: 'updateItemInBasket', - AddTaxesForBasketItem: 'addTaxesForBasketItem', - AddPaymentInstrumentToBasket: 'addPaymentInstrumentToBasket', - RemovePaymentInstrumentFromBasket: 'removePaymentInstrumentFromBasket', - UpdatePaymentInstrumentInBasket: 'updatePaymentInstrumentInBasket', - AddPriceBooksToBasket: 'addPriceBooksToBasket', - CreateShipmentForBasket: 'createShipmentForBasket', - RemoveShipmentFromBasket: 'removeShipmentFromBasket', - UpdateShipmentForBasket: 'updateShipmentForBasket', - UpdateShippingAddressForShipment: 'updateShippingAddressForShipment', - UpdateShippingMethodForShipment: 'updateShippingMethodForShipment', - AddTaxesForBasket: 'addTaxesForBasket' -} as const - -export type ShopperBasketsMutation = - (typeof ShopperBasketsMutations)[keyof typeof ShopperBasketsMutations] - -export function useShopperBasketsMutation( - mutation: Mutation -): UseMutationResult, unknown, Argument> { - const getCacheUpdates = cacheUpdateMatrix[mutation] - // TODO: Remove this check when all mutations are implemented. - if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) - - // The `Options` and `Data` types for each mutation are similar, but distinct, and the union - // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. - // I'm not sure if there's a way to avoid the type assertions in here for the methods that - // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply - // re-asserting what we already have. - const {shopperBaskets: client} = useCommerceApi() - type Options = Argument - type Data = DataType - return useMutation({ - client, - method: (opts: Options) => (client[mutation] as ApiMethod)(opts), - getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> - }) -} diff --git a/packages/commerce-sdk-react/src/hooks/Test/query.ts b/packages/commerce-sdk-react/src/hooks/Test/query.ts deleted file mode 100644 index 37981d8f03..0000000000 --- a/packages/commerce-sdk-react/src/hooks/Test/query.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (c) 2023, 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' -import useCommerceApi from '../useCommerceApi' -import {useQuery} from '../useQuery' - -type Client = ApiClients['shopperBaskets'] - -/** - * A hook for `ShopperBaskets#getBasket`. - * Gets a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getbasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const useBasket = ( - apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} -): UseQueryResult> => { - const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => client.getBasket(arg) - const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - // Full parameters last for easy lookup - parameters - ] as const - - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) -} -/** - * A hook for `ShopperBaskets#getPaymentMethodsForBasket`. - * Gets applicable payment methods for an existing basket considering the open payment amount only. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPaymentMethodsForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpaymentmethodsforbasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const usePaymentMethodsForBasket = ( - apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} -): UseQueryResult> => { - const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPaymentMethodsForBasket(arg) - const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ - parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/payment-methods', - // Full parameters last for easy lookup - parameters - ] as const - - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) -} -/** - * A hook for `ShopperBaskets#getPriceBooksForBasket`. - * Gets applicable price books for an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPriceBooksForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpricebooksforbasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const usePriceBooksForBasket = ( - apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} -): UseQueryResult> => { - const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPriceBooksForBasket(arg) - const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ - parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/price-books', - // Full parameters last for easy lookup - parameters - ] as const - - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) -} -/** - * A hook for `ShopperBaskets#getShippingMethodsForShipment`. - * Gets the applicable shipping methods for a certain shipment of a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getShippingMethodsForShipment} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getshippingmethodsforshipment} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const useShippingMethodsForShipment = ( - apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} -): UseQueryResult> => { - const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => - client.getShippingMethodsForShipment(arg) - const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ - parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/shipments/', - parameters.shipmentId, - '/shipping-methods', - // Full parameters last for easy lookup - parameters - ] as const - - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) -} -/** - * A hook for `ShopperBaskets#getTaxesFromBasket`. - * This method gives you the external taxation data set by the PUT taxes API. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getTaxesFromBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#gettaxesfrombasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const useTaxesFromBasket = ( - apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} -): UseQueryResult> => { - const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => client.getTaxesFromBasket(arg) - const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ - parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/taxes', - // Full parameters last for easy lookup - parameters - ] as const - - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) -} diff --git a/packages/commerce-sdk-react/src/hooks/Test/temp.ts b/packages/commerce-sdk-react/src/hooks/Test/temp.ts deleted file mode 100644 index 2d6596fe92..0000000000 --- a/packages/commerce-sdk-react/src/hooks/Test/temp.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2023, 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 - */ - -// Temp test file to validate that types are working as expected - -import {cacheUpdateMatrix} from './config' -import {useShopperBasketsMutation} from './mutation' -import {useBasket} from './query' - -const query = useBasket({ - parameters: { - basketId: 'basketId' - } -}) -const queryData = query.data - -const updates = cacheUpdateMatrix.removeItemFromBasket?.( - 'customerId', - { - headers: {}, - parameters: { - shortCode: 'shortCode', - clientId: 'clientId', - siteId: 'siteId', - organizationId: 'organizationId', - basketId: 'basketId', - itemId: 'itemId' - } - }, - {basketId: 'basketId'} -) -if (updates) { - const {invalidate} = updates -} - -const mutation = useShopperBasketsMutation('addCouponToBasket') -const {data: mutationData, mutate} = mutation -mutate({ - body: {code: 'code'}, - parameters: {basketId: 'basketId'} -}) From 5c5fff09b349de3c63b5f2cc7bf7c7d59f33dd2b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 16:49:12 -0500 Subject: [PATCH 021/122] Generate hooks for all APIs --- .../src/hooks/ShopperBaskets/mutation.ts | 24 +- .../src/hooks/ShopperBaskets/query.ts | 61 +- .../src/hooks/ShopperContexts/config.ts | 43 ++ .../src/hooks/ShopperContexts/mutation.ts | 60 +- .../src/hooks/ShopperContexts/query.ts | 42 +- .../src/hooks/ShopperCustomers/config.ts | 510 +++++++++++++++ .../src/hooks/ShopperCustomers/mutation.ts | 506 +-------------- .../src/hooks/ShopperCustomers/query.ts | 590 +++++++++++------- .../hooks/ShopperDiscoverySearch/config.ts | 27 + .../hooks/ShopperDiscoverySearch/mutation.ts | 46 +- .../src/hooks/ShopperDiscoverySearch/query.ts | 39 +- .../hooks/ShopperGiftCertificates/config.ts | 27 + .../hooks/ShopperGiftCertificates/mutation.ts | 51 +- .../src/hooks/ShopperOrders/config.ts | 139 +++++ .../src/hooks/ShopperOrders/mutation.ts | 151 +---- .../src/hooks/ShopperOrders/query.ts | 138 ++-- .../src/hooks/ShopperProducts/query.ts | 225 ++++--- .../src/hooks/ShopperPromotions/query.ts | 98 +-- .../src/hooks/ShopperSearch/query.ts | 119 ++-- 19 files changed, 1677 insertions(+), 1219 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts index fc03892a7e..2dd86ede84 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts @@ -4,7 +4,7 @@ * 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 {ApiClients, Argument, DataType, ApiMethod} from '../types' +import {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' @@ -52,15 +52,17 @@ export function useShopperBasketsMutation) => - (client[mutation] as ApiMethod, DataType>)( - options - ) - - return useMutation({method, getCacheUpdates}) + type Options = Argument + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts index 978b81aa9d..37981d8f03 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType} from '../types' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' @@ -27,21 +27,12 @@ export const useBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({parameters}: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, - '?', - 'siteId', - parameters.siteId, - 'locale', - parameters.locale, // Full parameters last for easy lookup parameters ] as const @@ -73,22 +64,15 @@ export const usePaymentMethodsForBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/payment-methods', - '?', - 'siteId', - parameters.siteId, - 'locale', - parameters.locale, // Full parameters last for easy lookup parameters ] as const @@ -117,20 +101,15 @@ export const usePriceBooksForBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/price-books', - '?', - 'siteId', - parameters.siteId, // Full parameters last for easy lookup parameters ] as const @@ -162,12 +141,10 @@ export const useShippingMethodsForShipment = ( const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', @@ -175,11 +152,6 @@ export const useShippingMethodsForShipment = ( '/shipments/', parameters.shipmentId, '/shipping-methods', - '?', - 'siteId', - parameters.siteId, - 'locale', - parameters.locale, // Full parameters last for easy lookup parameters ] as const @@ -207,20 +179,15 @@ export const useTaxesFromBasket = ( const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper // hook, so we use a callback here that receives that merged object. - const getQueryKey = >(parameters: T) => + const getQueryKey = ({ + parameters + }: MergedOptions>) => [ - 'https://', - parameters.shortCode, - '.api.commercecloud.salesforce.com/checkout/shopper-baskets/', - parameters.version, '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/taxes', - '?', - 'siteId', - parameters.siteId, // Full parameters last for easy lookup parameters ] as const diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts new file mode 100644 index 0000000000..603c81b90d --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022, 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 {NotImplementedError} from '../utils' + +export const ShopperContextsMutations = { + /** + * WARNING: This method is not implemented. + * + * Creates the shopper's context based on shopperJWT. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=createShopperContext} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#createshoppercontext} for more information on the parameters and returned data type. + */ + CreateShopperContext: 'createShopperContext', + /** + * WARNING: This method is not implemented. + * + * Gets the shopper's context based on the shopperJWT. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=deleteShopperContext} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#deleteshoppercontext} for more information on the parameters and returned data type. + */ + DeleteShopperContext: 'deleteShopperContext', + /** + * WARNING: This method is not implemented. + * + * Updates the shopper's context based on the Shopper JWT. If the shopper context exists, it's updated with the patch body. If a customer qualifier or an `effectiveDateTime` is already present in the existing shopper context, its value is replaced by the corresponding value from the patch body. If a customer qualifers' value is set to `null` it's deleted from existing shopper context. If `effectiveDateTime` value is set to set to an empty string (\"\"), it's deleted from existing shopper context. If `effectiveDateTime` value is set to `null` it's ignored. If an `effectiveDateTime` or customer qualifiiers' value is new, it's added to the existing Shopper context. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=updateShopperContext} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#updateshoppercontext} for more information on the parameters and returned data type. + */ + UpdateShopperContext: 'updateShopperContext' +} as const + +/** + * WARNING: This method is not implemented. + * + * A hook for performing mutations with the Shopper Contexts API. + */ +export function useShopperContextsMutation() { + NotImplementedError() +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts index 603c81b90d..4ff24fd270 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts @@ -1,43 +1,45 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' +import {useMutation} from '../useMutation' +import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' + +type Client = ApiClients['shopperContexts'] export const ShopperContextsMutations = { - /** - * WARNING: This method is not implemented. - * - * Creates the shopper's context based on shopperJWT. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=createShopperContext} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#createshoppercontext} for more information on the parameters and returned data type. - */ CreateShopperContext: 'createShopperContext', - /** - * WARNING: This method is not implemented. - * - * Gets the shopper's context based on the shopperJWT. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=deleteShopperContext} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#deleteshoppercontext} for more information on the parameters and returned data type. - */ DeleteShopperContext: 'deleteShopperContext', - /** - * WARNING: This method is not implemented. - * - * Updates the shopper's context based on the Shopper JWT. If the shopper context exists, it's updated with the patch body. If a customer qualifier or an `effectiveDateTime` is already present in the existing shopper context, its value is replaced by the corresponding value from the patch body. If a customer qualifers' value is set to `null` it's deleted from existing shopper context. If `effectiveDateTime` value is set to set to an empty string (\"\"), it's deleted from existing shopper context. If `effectiveDateTime` value is set to `null` it's ignored. If an `effectiveDateTime` or customer qualifiiers' value is new, it's added to the existing Shopper context. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=updateShopperContext} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#updateshoppercontext} for more information on the parameters and returned data type. - */ UpdateShopperContext: 'updateShopperContext' } as const -/** - * WARNING: This method is not implemented. - * - * A hook for performing mutations with the Shopper Contexts API. - */ -export function useShopperContextsMutation() { - NotImplementedError() +export type ShopperContextsMutation = + (typeof ShopperContextsMutations)[keyof typeof ShopperContextsMutations] + +export function useShopperContextsMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + // The `Options` and `Data` types for each mutation are similar, but distinct, and the union + // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. + // I'm not sure if there's a way to avoid the type assertions in here for the methods that + // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply + // re-asserting what we already have. + const {shopperContexts: client} = useCommerceApi() + type Options = Argument + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts index e44adb23e2..4522ba1f15 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts @@ -1,20 +1,48 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {NotImplementedError} from './../utils' +import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' + +type Client = ApiClients['shopperContexts'] /** - * WARNING: This method is not implemented. - * * A hook for `ShopperContexts#getShopperContext`. - * Gets the shopper's context based on the shopperJWT. + * Gets the shopper's context based on the shopperJWT. ******** This API is currently a work in progress, and not available to use yet. ******** * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=getShopperContext} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#getshoppercontext} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -export const useShopperContext = (): void => { - NotImplementedError() +export const useShopperContext = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperContexts: client} = useCommerceApi() + const method = (arg: Argument) => client.getShopperContext(arg) + const requiredParameters = ['organizationId', 'usid'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/shopper-context/', + parameters.usid, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts new file mode 100644 index 0000000000..49f161a5be --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2022, 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 {ApiClients, Argument, DataType} from '../types' +import {useMutation} from '../useMutation' +import {MutationFunction, useQueryClient, UseMutationResult} from '@tanstack/react-query' +import {updateCache, CacheUpdateMatrixElement, Client, NotImplementedError} from '../utils' + +export const ShopperCustomersMutations = { + /** + * Registers a new customer. The mandatory data are the credentials, profile last name, and email. This requires a JSON Web Token (JWT) which needs to be obtained using the POST /customers/auth API with type \"guest\". + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerCustomer} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registercustomer} for more information on the parameters and returned data type. + */ + RegisterCustomer: 'registerCustomer', + /** + * WARNING: This method is not implemented. + * + * **DEPRECATION NOTICE** + * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. + * --- + * Log the user out. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=invalidateCustomerAuth} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#invalidatecustomerauth} for more information on the parameters and returned data type. + */ + InvalidateCustomerAuth: 'invalidateCustomerAuth', + /** + * WARNING: This method is not implemented. + * + * **DEPRECATION NOTICE** + * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. + * --- + * Obtains a new JSON Web Token (JWT)for a guest or registered customer. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeCustomer} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizecustomer} for more information on the parameters and returned data type. + */ + AuthorizeCustomer: 'authorizeCustomer', + /** + * WARNING: This method is not implemented. + * + * **DEPRECATION NOTICE** + * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. + * --- + * Obtain the JSON Web Token (JWT) for registered customers whose credentials are stored using a third party system. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeTrustedSystem} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizetrustedsystem} for more information on the parameters and returned data type. + */ + AuthorizeTrustedSystem: 'authorizeTrustedSystem', + /** + * WARNING: This method is not implemented. + * + * Reset customer password, after obtaining a reset token. This is the second step in the reset customer password flow, where a customer password is reset by providing the new credentials along with a reset token. This call should be preceded by a call to the /create-reset-token endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=resetPassword} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#resetpassword} for more information on the parameters and returned data type. + */ + ResetPassword: 'resetPassword', + /** + * WARNING: This method is not implemented. + * + * Get reset password token. This is the first step in the reset customer password flow, where a password reset token is requested for future use to reset a customer password. This call should be followed by a call to the /reset endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getResetPasswordToken} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getresetpasswordtoken} for more information on the parameters and returned data type. + */ + GetResetPasswordToken: 'getResetPasswordToken', + /** + * WARNING: This method is not implemented. + * + * Registers a new external profile for a customer. This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerExternalProfile} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registerexternalprofile} for more information on the parameters and returned data type. + */ + RegisterExternalProfile: 'registerExternalProfile', + /** + * Updates a customer. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomer} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomer} for more information on the parameters and returned data type. + */ + UpdateCustomer: 'updateCustomer', + /** + * Creates a new address with the given name for the given customer. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerAddress} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomeraddress} for more information on the parameters and returned data type. + */ + CreateCustomerAddress: 'createCustomerAddress', + /** + * Deletes a customer's address by address name. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=removeCustomerAddress} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#removecustomeraddress} for more information on the parameters and returned data type. + */ + RemoveCustomerAddress: 'removeCustomerAddress', + /** + * Updates a customer's address by address name. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerAddress} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomeraddress} for more information on the parameters and returned data type. + */ + UpdateCustomerAddress: 'updateCustomerAddress', + /** + * WARNING: This method is not implemented. + * + * Updates the customer's password. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerPassword} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerpassword} for more information on the parameters and returned data type. + */ + UpdateCustomerPassword: 'updateCustomerPassword', + /** + * Adds a payment instrument to the customer information. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerPaymentInstrument} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerpaymentinstrument} for more information on the parameters and returned data type. + */ + CreateCustomerPaymentInstrument: 'createCustomerPaymentInstrument', + /** + * Deletes a customer's payment instrument. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerPaymentInstrument} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerpaymentinstrument} for more information on the parameters and returned data type. + */ + DeleteCustomerPaymentInstrument: 'deleteCustomerPaymentInstrument', + /** + * Creates a customer product list. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductList} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlist} for more information on the parameters and returned data type. + */ + CreateCustomerProductList: 'createCustomerProductList', + /** + * WARNING: This method is not implemented. + * + * Deletes a customer product list. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductList} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlist} for more information on the parameters and returned data type. + */ + DeleteCustomerProductList: 'deleteCustomerProductList', + /** + * WARNING: This method is not implemented. + * + * Changes a product list. Changeable properties are the name, description, and if the list is public. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductList} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlist} for more information on the parameters and returned data type. + */ + UpdateCustomerProductList: 'updateCustomerProductList', + /** + * Adds an item to the customer's product list. Considered values from the request body are: + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductListItem} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlistitem} for more information on the parameters and returned data type. + */ + CreateCustomerProductListItem: 'createCustomerProductListItem', + /** + * Removes an item from a customer product list. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductListItem} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlistitem} for more information on the parameters and returned data type. + */ + DeleteCustomerProductListItem: 'deleteCustomerProductListItem', + /** + * Updates an item of a customer's product list. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductListItem} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlistitem} for more information on the parameters and returned data type. + */ + UpdateCustomerProductListItem: 'updateCustomerProductListItem' +} as const + +export const shopperCustomersCacheUpdateMatrix = { + authorizeCustomer: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + authorizeTrustedSystem: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + deleteCustomerProductList: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + getResetPasswordToken: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + invalidateCustomerAuth: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + registerCustomer: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + registerExternalProfile: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + resetPassword: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + updateCustomerPassword: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + updateCustomerProductList: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + updateCustomer: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const {customerId} = params.parameters + return { + update: [ + { + name: 'customer', + key: ['/customers', customerId, {customerId}], + updater: () => response + } + ], + invalidate: [ + { + name: 'customerPaymentInstrument', + key: ['/customers', customerId, '/payment-instruments'] + }, + {name: 'customerAddress', key: ['/customers', customerId, '/addresses']}, + {name: 'externalProfile', key: ['/customers', '/external-profile']} + ] + } + }, + + updateCustomerAddress: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const {customerId, addressName} = params.parameters + return { + update: [ + { + name: 'customerAddress', + key: ['/customers', customerId, '/addresses', {addressName, customerId}], + updater: () => response + } + ], + invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] + } + }, + + createCustomerAddress: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const {customerId} = params.parameters + const {addressId} = params.body + return { + update: [ + { + name: 'customerAddress', + key: [ + '/customers', + customerId, + '/addresses', + {addressName: addressId, customerId} + ], + updater: () => response + } + ], + invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] + } + }, + + removeCustomerAddress: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion + // The required parameters become optional accidentally + // @ts-ignore + const {customerId, addressName} = params.parameters + return { + invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}], + remove: [ + { + name: 'customerAddress', + key: ['/customers', customerId, '/addresses', {addressName, customerId}] + } + ] + } + }, + + createCustomerPaymentInstrument: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const {customerId} = params.parameters + return { + update: [ + { + name: 'customerPaymentInstrument', + key: [ + '/customers', + customerId, + '/payment-instruments', + {customerId, paymentInstrumentId: response?.paymentInstrumentId} + ], + updater: () => response + } + ], + invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] + } + }, + + deleteCustomerPaymentInstrument: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion + // The required parameters become optional accidentally + // @ts-ignore + const {customerId, paymentInstrumentId} = params.parameters + return { + invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}], + remove: [ + { + name: 'customerPaymentInstrument', + key: [ + '/customers', + customerId, + '/payment-instruments', + {customerId, paymentInstrumentId} + ] + } + ] + } + }, + + createCustomerProductList: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const {customerId} = params.parameters + return { + update: [ + { + name: 'customerProductList', + key: [ + '/customers', + customerId, + '/product-list', + {customerId, listId: response?.id} + ], + updater: () => response + } + ] + } + }, + + createCustomerProductListItem: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const {customerId, listId} = params.parameters + return { + update: [ + { + name: 'customerProductListItem', + key: [ + '/customers', + customerId, + '/product-list', + listId, + {itemId: response?.id} + ], + updater: () => response + } + ], + invalidate: [ + { + name: 'customerProductList', + key: ['/customers', customerId, '/product-list', {customerId, listId}] + } + ] + } + }, + + updateCustomerProductListItem: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const {customerId, listId, itemId} = params.parameters + return { + update: [ + { + name: 'customerProductListItem', + key: ['/customers', customerId, '/product-list', listId, {itemId}], + updater: () => response + } + ], + invalidate: [ + { + name: 'customerProductList', + key: ['/customers', customerId, '/product-list', {customerId, listId}] + } + ] + } + }, + + deleteCustomerProductListItem: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion + // The required parameters become optional accidentally + // @ts-ignore + const {customerId, listId, itemId} = params.parameters + return { + invalidate: [ + { + name: 'customerProductList', + key: ['/customers', customerId, '/product-list', {customerId, listId}] + } + ], + remove: [ + { + name: 'customerProductListItem', + key: ['/customers', customerId, '/product-list', listId, {itemId}] + } + ] + } + } +} + +export const SHOPPER_CUSTOMERS_NOT_IMPLEMENTED = [ + 'authorizeCustomer', + 'authorizeTrustedSystem', + 'deleteCustomerProductList', + 'invalidateCustomerAuth', + 'registerExternalProfile', + 'resetPassword', + 'updateCustomerPassword', + 'updateCustomerProductList' +] + +export type ShopperCustomersMutationType = + (typeof ShopperCustomersMutations)[keyof typeof ShopperCustomersMutations] + +type UseShopperCustomersMutationHeaders = NonNullable< + Argument +>['headers'] +type UseShopperCustomersMutationArg = { + headers?: UseShopperCustomersMutationHeaders + rawResponse?: boolean + action: ShopperCustomersMutationType +} + +type ShopperCustomersClient = ApiClients['shopperCustomers'] + +/** + * A hook for performing mutations with the Shopper Customers API. + */ +function useShopperCustomersMutation( + arg: UseShopperCustomersMutationArg +): UseMutationResult< + DataType | Response, + Error, + Argument +> { + const {headers, rawResponse, action} = arg + + if (SHOPPER_CUSTOMERS_NOT_IMPLEMENTED.includes(action)) { + NotImplementedError() + } + type Params = Argument + type Data = DataType + const queryClient = useQueryClient() + return useMutation( + (params, apiClients) => { + const method = apiClients['shopperCustomers'][action] as MutationFunction + return ( + method.call as ( + apiClient: ShopperCustomersClient, + params: Params, + rawResponse: boolean | undefined + ) => any + )(apiClients['shopperCustomers'], {...params, headers}, rawResponse) + }, + { + onSuccess: (data, params) => { + updateCache(queryClient, action, shopperCustomersCacheUpdateMatrix, data, params) + } + } + ) +} + +export {useShopperCustomersMutation} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts index 49f161a5be..7522231dd0 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts @@ -1,510 +1,62 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType} from '../types' +import {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' import {useMutation} from '../useMutation' -import {MutationFunction, useQueryClient, UseMutationResult} from '@tanstack/react-query' -import {updateCache, CacheUpdateMatrixElement, Client, NotImplementedError} from '../utils' +import {UseMutationResult} from '@tanstack/react-query' +import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' + +type Client = ApiClients['shopperCustomers'] export const ShopperCustomersMutations = { - /** - * Registers a new customer. The mandatory data are the credentials, profile last name, and email. This requires a JSON Web Token (JWT) which needs to be obtained using the POST /customers/auth API with type \"guest\". - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registercustomer} for more information on the parameters and returned data type. - */ RegisterCustomer: 'registerCustomer', - /** - * WARNING: This method is not implemented. - * - * **DEPRECATION NOTICE** - * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - * --- - * Log the user out. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=invalidateCustomerAuth} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#invalidatecustomerauth} for more information on the parameters and returned data type. - */ InvalidateCustomerAuth: 'invalidateCustomerAuth', - /** - * WARNING: This method is not implemented. - * - * **DEPRECATION NOTICE** - * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - * --- - * Obtains a new JSON Web Token (JWT)for a guest or registered customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizecustomer} for more information on the parameters and returned data type. - */ AuthorizeCustomer: 'authorizeCustomer', - /** - * WARNING: This method is not implemented. - * - * **DEPRECATION NOTICE** - * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - * --- - * Obtain the JSON Web Token (JWT) for registered customers whose credentials are stored using a third party system. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeTrustedSystem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizetrustedsystem} for more information on the parameters and returned data type. - */ AuthorizeTrustedSystem: 'authorizeTrustedSystem', - /** - * WARNING: This method is not implemented. - * - * Reset customer password, after obtaining a reset token. This is the second step in the reset customer password flow, where a customer password is reset by providing the new credentials along with a reset token. This call should be preceded by a call to the /create-reset-token endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=resetPassword} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#resetpassword} for more information on the parameters and returned data type. - */ ResetPassword: 'resetPassword', - /** - * WARNING: This method is not implemented. - * - * Get reset password token. This is the first step in the reset customer password flow, where a password reset token is requested for future use to reset a customer password. This call should be followed by a call to the /reset endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getResetPasswordToken} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getresetpasswordtoken} for more information on the parameters and returned data type. - */ GetResetPasswordToken: 'getResetPasswordToken', - /** - * WARNING: This method is not implemented. - * - * Registers a new external profile for a customer. This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerExternalProfile} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registerexternalprofile} for more information on the parameters and returned data type. - */ RegisterExternalProfile: 'registerExternalProfile', - /** - * Updates a customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomer} for more information on the parameters and returned data type. - */ UpdateCustomer: 'updateCustomer', - /** - * Creates a new address with the given name for the given customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerAddress} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomeraddress} for more information on the parameters and returned data type. - */ CreateCustomerAddress: 'createCustomerAddress', - /** - * Deletes a customer's address by address name. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=removeCustomerAddress} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#removecustomeraddress} for more information on the parameters and returned data type. - */ RemoveCustomerAddress: 'removeCustomerAddress', - /** - * Updates a customer's address by address name. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerAddress} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomeraddress} for more information on the parameters and returned data type. - */ UpdateCustomerAddress: 'updateCustomerAddress', - /** - * WARNING: This method is not implemented. - * - * Updates the customer's password. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerPassword} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerpassword} for more information on the parameters and returned data type. - */ UpdateCustomerPassword: 'updateCustomerPassword', - /** - * Adds a payment instrument to the customer information. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerPaymentInstrument} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerpaymentinstrument} for more information on the parameters and returned data type. - */ CreateCustomerPaymentInstrument: 'createCustomerPaymentInstrument', - /** - * Deletes a customer's payment instrument. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerPaymentInstrument} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerpaymentinstrument} for more information on the parameters and returned data type. - */ DeleteCustomerPaymentInstrument: 'deleteCustomerPaymentInstrument', - /** - * Creates a customer product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlist} for more information on the parameters and returned data type. - */ CreateCustomerProductList: 'createCustomerProductList', - /** - * WARNING: This method is not implemented. - * - * Deletes a customer product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlist} for more information on the parameters and returned data type. - */ DeleteCustomerProductList: 'deleteCustomerProductList', - /** - * WARNING: This method is not implemented. - * - * Changes a product list. Changeable properties are the name, description, and if the list is public. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlist} for more information on the parameters and returned data type. - */ UpdateCustomerProductList: 'updateCustomerProductList', - /** - * Adds an item to the customer's product list. Considered values from the request body are: - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlistitem} for more information on the parameters and returned data type. - */ CreateCustomerProductListItem: 'createCustomerProductListItem', - /** - * Removes an item from a customer product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlistitem} for more information on the parameters and returned data type. - */ DeleteCustomerProductListItem: 'deleteCustomerProductListItem', - /** - * Updates an item of a customer's product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlistitem} for more information on the parameters and returned data type. - */ UpdateCustomerProductListItem: 'updateCustomerProductListItem' } as const -export const shopperCustomersCacheUpdateMatrix = { - authorizeCustomer: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - authorizeTrustedSystem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - deleteCustomerProductList: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - getResetPasswordToken: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - invalidateCustomerAuth: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - registerCustomer: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - registerExternalProfile: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - resetPassword: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - updateCustomerPassword: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - updateCustomerProductList: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - updateCustomer: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters - return { - update: [ - { - name: 'customer', - key: ['/customers', customerId, {customerId}], - updater: () => response - } - ], - invalidate: [ - { - name: 'customerPaymentInstrument', - key: ['/customers', customerId, '/payment-instruments'] - }, - {name: 'customerAddress', key: ['/customers', customerId, '/addresses']}, - {name: 'externalProfile', key: ['/customers', '/external-profile']} - ] - } - }, - - updateCustomerAddress: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId, addressName} = params.parameters - return { - update: [ - { - name: 'customerAddress', - key: ['/customers', customerId, '/addresses', {addressName, customerId}], - updater: () => response - } - ], - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] - } - }, - - createCustomerAddress: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters - const {addressId} = params.body - return { - update: [ - { - name: 'customerAddress', - key: [ - '/customers', - customerId, - '/addresses', - {addressName: addressId, customerId} - ], - updater: () => response - } - ], - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] - } - }, - - removeCustomerAddress: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion - // The required parameters become optional accidentally - // @ts-ignore - const {customerId, addressName} = params.parameters - return { - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}], - remove: [ - { - name: 'customerAddress', - key: ['/customers', customerId, '/addresses', {addressName, customerId}] - } - ] - } - }, - - createCustomerPaymentInstrument: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters - return { - update: [ - { - name: 'customerPaymentInstrument', - key: [ - '/customers', - customerId, - '/payment-instruments', - {customerId, paymentInstrumentId: response?.paymentInstrumentId} - ], - updater: () => response - } - ], - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] - } - }, - - deleteCustomerPaymentInstrument: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion - // The required parameters become optional accidentally - // @ts-ignore - const {customerId, paymentInstrumentId} = params.parameters - return { - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}], - remove: [ - { - name: 'customerPaymentInstrument', - key: [ - '/customers', - customerId, - '/payment-instruments', - {customerId, paymentInstrumentId} - ] - } - ] - } - }, - - createCustomerProductList: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters - return { - update: [ - { - name: 'customerProductList', - key: [ - '/customers', - customerId, - '/product-list', - {customerId, listId: response?.id} - ], - updater: () => response - } - ] - } - }, - - createCustomerProductListItem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId, listId} = params.parameters - return { - update: [ - { - name: 'customerProductListItem', - key: [ - '/customers', - customerId, - '/product-list', - listId, - {itemId: response?.id} - ], - updater: () => response - } - ], - invalidate: [ - { - name: 'customerProductList', - key: ['/customers', customerId, '/product-list', {customerId, listId}] - } - ] - } - }, - - updateCustomerProductListItem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId, listId, itemId} = params.parameters - return { - update: [ - { - name: 'customerProductListItem', - key: ['/customers', customerId, '/product-list', listId, {itemId}], - updater: () => response - } - ], - invalidate: [ - { - name: 'customerProductList', - key: ['/customers', customerId, '/product-list', {customerId, listId}] - } - ] - } - }, - - deleteCustomerProductListItem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion - // The required parameters become optional accidentally - // @ts-ignore - const {customerId, listId, itemId} = params.parameters - return { - invalidate: [ - { - name: 'customerProductList', - key: ['/customers', customerId, '/product-list', {customerId, listId}] - } - ], - remove: [ - { - name: 'customerProductListItem', - key: ['/customers', customerId, '/product-list', listId, {itemId}] - } - ] - } - } -} - -export const SHOPPER_CUSTOMERS_NOT_IMPLEMENTED = [ - 'authorizeCustomer', - 'authorizeTrustedSystem', - 'deleteCustomerProductList', - 'invalidateCustomerAuth', - 'registerExternalProfile', - 'resetPassword', - 'updateCustomerPassword', - 'updateCustomerProductList' -] - -export type ShopperCustomersMutationType = +export type ShopperCustomersMutation = (typeof ShopperCustomersMutations)[keyof typeof ShopperCustomersMutations] -type UseShopperCustomersMutationHeaders = NonNullable< - Argument ->['headers'] -type UseShopperCustomersMutationArg = { - headers?: UseShopperCustomersMutationHeaders - rawResponse?: boolean - action: ShopperCustomersMutationType +export function useShopperCustomersMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + // The `Options` and `Data` types for each mutation are similar, but distinct, and the union + // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. + // I'm not sure if there's a way to avoid the type assertions in here for the methods that + // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply + // re-asserting what we already have. + const {shopperCustomers: client} = useCommerceApi() + type Options = Argument + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> + }) } - -type ShopperCustomersClient = ApiClients['shopperCustomers'] - -/** - * A hook for performing mutations with the Shopper Customers API. - */ -function useShopperCustomersMutation( - arg: UseShopperCustomersMutationArg -): UseMutationResult< - DataType | Response, - Error, - Argument -> { - const {headers, rawResponse, action} = arg - - if (SHOPPER_CUSTOMERS_NOT_IMPLEMENTED.includes(action)) { - NotImplementedError() - } - type Params = Argument - type Data = DataType - const queryClient = useQueryClient() - return useMutation( - (params, apiClients) => { - const method = apiClients['shopperCustomers'][action] as MutationFunction - return ( - method.call as ( - apiClient: ShopperCustomersClient, - params: Params, - rawResponse: boolean | undefined - ) => any - )(apiClients['shopperCustomers'], {...params, headers}, rawResponse) - }, - { - onSuccess: (data, params) => { - updateCache(queryClient, action, shopperCustomersCacheUpdateMatrix, data, params) - } - } - ) -} - -export {useShopperCustomersMutation} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts index 863dbc4184..b3e6bf6ed1 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts @@ -1,35 +1,55 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType} from '../types' -import {useQuery} from '../useQuery' -import {NotImplementedError} from './../utils' - -// TODO: Remove once phase2 is completed and all hooks are implemented +import {ApiClients, Argument, DataType, MergedOptions} from '../types' import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' type Client = ApiClients['shopperCustomers'] /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperCustomers#getExternalProfile`. * Gets the new external profile for a customer.This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getExternalProfile} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getexternalprofile} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useExternalProfile(): void { - NotImplementedError() -} +export const useExternalProfile = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => client.getExternalProfile(arg) + const requiredParameters = [ + 'organizationId', + 'externalId', + 'authenticationProviderId', + 'siteId' + ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/external-profile', + // Full parameters last for easy lookup + parameters + ] as const -type UseCustomerParameters = NonNullable>['parameters'] -type UseCustomerHeaders = NonNullable>['headers'] -type UseCustomerArg = {headers?: UseCustomerHeaders; rawResponse?: boolean} & UseCustomerParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperCustomers#getCustomer`. * Gets a customer with all existing addresses and payment instruments associated with the requested customer. @@ -37,36 +57,32 @@ type UseCustomerArg = {headers?: UseCustomerHeaders; rawResponse?: boolean} & Us * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomer} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomer( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomer( - arg: Omit & {rawResponse?: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomer( - arg: UseCustomerArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - ['/customers', parameters.customerId, arg], - (_, {shopperCustomers}) => { - return shopperCustomers.getCustomer({parameters, headers}, rawResponse) - }, - options - ) -} +export const useCustomer = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => client.getCustomer(arg) + const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + // Full parameters last for easy lookup + parameters + ] as const -type UseCustomerAddressParameters = NonNullable< - Argument ->['parameters'] -type UseCustomerAddressHeaders = NonNullable>['headers'] -type UseCustomerAddressArg = { - headers?: UseCustomerAddressHeaders - rawResponse?: boolean -} & UseCustomerAddressParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperCustomers#getCustomerAddress`. * Retrieves a customer's address by address name. @@ -74,37 +90,36 @@ type UseCustomerAddressArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomeraddress} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomerAddress( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerAddress( - arg: Omit & {rawResponse?: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerAddress( - arg: UseCustomerAddressArg, - options?: UseQueryOptions | Response, Error> -) { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - // TODO: `parameters.addressName` is also needed here - ['/customers', parameters.customerId, '/addresses', arg], - (_, {shopperCustomers}) => { - return shopperCustomers.getCustomerAddress({parameters, headers}, rawResponse) - }, - options - ) -} +export const useCustomerAddress = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => client.getCustomerAddress(arg) + const requiredParameters = ['organizationId', 'customerId', 'addressName', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/addresses/', + parameters.addressName, + // Full parameters last for easy lookup + parameters + ] as const -type UseCustomerBasketsParameters = NonNullable< - Argument ->['parameters'] -type UseCustomerBasketsHeaders = NonNullable>['headers'] -type UseCustomerBasketsArg = { - headers?: UseCustomerBasketsHeaders - rawResponse?: boolean -} & UseCustomerBasketsParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperCustomers#getCustomerBaskets`. * Gets the baskets of a customer. @@ -112,37 +127,35 @@ type UseCustomerBasketsArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerbaskets} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomerBaskets( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerBaskets( - arg: Omit & {rawResponse?: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerBaskets( - arg: UseCustomerBasketsArg, - options?: UseQueryOptions | Response, Error> -) { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - ['/customers', parameters.customerId, '/baskets', arg], - (_, {shopperCustomers}) => { - return shopperCustomers.getCustomerBaskets({parameters, headers}, rawResponse) - }, - { - enabled: !!parameters.customerId, - ...options - } - ) -} +export const useCustomerBaskets = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => client.getCustomerBaskets(arg) + const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/baskets', + // Full parameters last for easy lookup + parameters + ] as const -type UseCustomerOrdersParameters = NonNullable>['parameters'] -type UseCustomerOrdersHeaders = NonNullable>['headers'] -type UseCustomerOrdersArg = { - headers?: UseCustomerOrdersHeaders - rawResponse?: boolean -} & UseCustomerOrdersParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperCustomers#getCustomerOrders`. * Returns a pageable list of all customer's orders. The default page size is 10. @@ -150,52 +163,81 @@ type UseCustomerOrdersArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerorders} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomerOrders( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerOrders( - arg: Omit & {rawResponse?: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerOrders( - arg: UseCustomerOrdersArg, - options?: UseQueryOptions | Response, Error> -) { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - ['/customers', parameters.customerId, '/orders', arg], - (_, {shopperCustomers}) => { - return shopperCustomers.getCustomerOrders({parameters, headers}, rawResponse) - }, - options - ) -} +export const useCustomerOrders = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => client.getCustomerOrders(arg) + const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/orders', + // Full parameters last for easy lookup + parameters + ] as const + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperCustomers#getCustomerPaymentInstrument`. * Retrieves a customer's payment instrument by its ID. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerPaymentInstrument} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerpaymentinstrument} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomerPaymentInstrument(): void { - NotImplementedError() -} - -type UseCustomerProductListsParameters = NonNullable< - Argument ->['parameters'] -type UseCustomerProductListsHeaders = NonNullable< - Argument ->['headers'] -type UseCustomerProductListsArg = { - headers?: UseCustomerProductListsHeaders - rawResponse?: boolean -} & UseCustomerProductListsParameters +export const useCustomerPaymentInstrument = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => + client.getCustomerPaymentInstrument(arg) + const requiredParameters = [ + 'organizationId', + 'customerId', + 'paymentInstrumentId', + 'siteId' + ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/payment-instruments/', + parameters.paymentInstrumentId, + // Full parameters last for easy lookup + parameters + ] as const + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperCustomers#getCustomerProductLists`. * Returns all customer product lists. @@ -203,38 +245,36 @@ type UseCustomerProductListsArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlists} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomerProductLists( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerProductLists( - arg: Omit & {rawResponse?: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerProductLists( - arg: UseCustomerProductListsArg, - options?: UseQueryOptions | Response, Error> -) { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - ['/customers', parameters.customerId, '/product-lists', arg], - (_, {shopperCustomers}) => { - return shopperCustomers.getCustomerProductLists({parameters, headers}, rawResponse) - }, - options - ) -} +export const useCustomerProductLists = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => + client.getCustomerProductLists(arg) + const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/product-lists', + // Full parameters last for easy lookup + parameters + ] as const -type UseCustomerProductListParameters = NonNullable< - Argument ->['parameters'] -type UseCustomerProductListHeaders = NonNullable< - Argument ->['headers'] -type UseCustomerProductListArg = { - headers?: UseCustomerProductListHeaders - rawResponse?: boolean -} & UseCustomerProductListParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperCustomers#getCustomerProductList`. * Returns a customer product list of the given customer and the items in the list. @@ -242,88 +282,194 @@ type UseCustomerProductListArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlist} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomerProductList( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerProductList( - arg: Omit & {rawResponse?: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCustomerProductList( - arg: UseCustomerProductListArg, - options?: UseQueryOptions | Response, Error> -) { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - ['/customers', parameters.customerId, '/product-list', parameters.listId, arg], - (_, {shopperCustomers}) => { - return shopperCustomers.getCustomerProductList({parameters, headers}, rawResponse) - }, - options - ) -} +export const useCustomerProductList = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => + client.getCustomerProductList(arg) + const requiredParameters = ['organizationId', 'customerId', 'listId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/product-lists/', + parameters.listId, + // Full parameters last for easy lookup + parameters + ] as const + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperCustomers#getCustomerProductListItem`. * Returns an item of a customer product list and the actual product details like image, availability and price. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerProductListItem} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlistitem} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCustomerProductListItem(): void { - NotImplementedError() +export const useCustomerProductListItem = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => + client.getCustomerProductListItem(arg) + const requiredParameters = [ + 'organizationId', + 'customerId', + 'listId', + 'itemId', + 'siteId' + ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/product-lists/', + parameters.listId, + '/items/', + parameters.itemId, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperCustomers#getPublicProductListsBySearchTerm`. * Retrieves all public product lists as defined by the given search term (for example, email OR first name and last name). * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getPublicProductListsBySearchTerm} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getpublicproductlistsbysearchterm} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePublicProductListsBySearchTerm(): void { - NotImplementedError() +export const usePublicProductListsBySearchTerm = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPublicProductListsBySearchTerm(arg) + const requiredParameters = ['organizationId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/product-lists', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperCustomers#getPublicProductList`. * Retrieves a public product list by ID and the items under that product list. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getPublicProductList} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getpublicproductlist} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePublicProductList(): void { - NotImplementedError() +export const usePublicProductList = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPublicProductList(arg) + const requiredParameters = ['organizationId', 'listId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/product-lists/', + parameters.listId, + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperCustomers#getProductListItem`. * Retrieves an item from a public product list and the actual product details like product, image, availability and price. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getProductListItem} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getproductlistitem} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useProductListItem(): void { - NotImplementedError() -} +export const useProductListItem = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperCustomers: client} = useCommerceApi() + const method = (arg: Argument) => client.getProductListItem(arg) + const requiredParameters = ['organizationId', 'listId', 'itemId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/product-lists/', + parameters.listId, + '/items/', + parameters.itemId, + // Full parameters last for easy lookup + parameters + ] as const -export { - useExternalProfile, - useCustomer, - useCustomerAddress, - useCustomerBaskets, - useCustomerOrders, - useCustomerPaymentInstrument, - useCustomerProductLists, - useCustomerProductList, - useCustomerProductListItem, - usePublicProductListsBySearchTerm, - usePublicProductList, - useProductListItem + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts new file mode 100644 index 0000000000..d7313c0998 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022, 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 {NotImplementedError} from '../utils' + +export const ShopperDiscoverySearchMutations = { + /** + * WARNING: This method is not implemented. + * + * This method retrieves search results for a Channel. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-discovery-search?meta=retrieveResults} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperdiscoverysearch.shopperdiscoverysearch-1.html#retrieveresults} for more information on the parameters and returned data type. + */ + RetrieveResults: 'retrieveResults' +} as const + +/** + * WARNING: This method is not implemented. + * + * A hook for performing mutations with the Shopper Discovery Search API. + */ +export function useShopperDiscoverySearchMutation() { + NotImplementedError() +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts index d7313c0998..09d80410ef 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts @@ -1,27 +1,43 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' +import {useMutation} from '../useMutation' +import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' + +type Client = ApiClients['shopperDiscoverySearch'] export const ShopperDiscoverySearchMutations = { - /** - * WARNING: This method is not implemented. - * - * This method retrieves search results for a Channel. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-discovery-search?meta=retrieveResults} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperdiscoverysearch.shopperdiscoverysearch-1.html#retrieveresults} for more information on the parameters and returned data type. - */ RetrieveResults: 'retrieveResults' } as const -/** - * WARNING: This method is not implemented. - * - * A hook for performing mutations with the Shopper Discovery Search API. - */ -export function useShopperDiscoverySearchMutation() { - NotImplementedError() +export type ShopperDiscoverySearchMutation = + (typeof ShopperDiscoverySearchMutations)[keyof typeof ShopperDiscoverySearchMutations] + +export function useShopperDiscoverySearchMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + // The `Options` and `Data` types for each mutation are similar, but distinct, and the union + // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. + // I'm not sure if there's a way to avoid the type assertions in here for the methods that + // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply + // re-asserting what we already have. + const {shopperDiscoverySearch: client} = useCommerceApi() + type Options = Argument + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts index 9244c57bc2..41fda8f8ec 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts @@ -1,20 +1,47 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {NotImplementedError} from '../utils' +import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' + +type Client = ApiClients['shopperDiscoverySearch'] /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperDiscoverySearch#getSuggestions`. * This method gets suggestions for the user's search activity for a channel. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-discovery-search?meta=getSuggestions} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperdiscoverysearch.shopperdiscoverysearch-1.html#getsuggestions} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -export const useSuggestions = (): void => { - NotImplementedError() +export const useSuggestions = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperDiscoverySearch: client} = useCommerceApi() + const method = (arg: Argument) => client.getSuggestions(arg) + const requiredParameters = ['organizationId', 'channelId', 'suggestionTypes', 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/channels/', + parameters.channelId, + '/suggestions', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts new file mode 100644 index 0000000000..759c66c25c --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022, 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 {NotImplementedError} from '../utils' + +export const ShopperGiftCertificatesMutations = { + /** + * WARNING: This method is not implemented. + * + * Action to retrieve an existing gift certificate. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-gift-certificates?meta=getGiftCertificate} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppergiftcertificates.shoppergiftcertificates-1.html#getgiftcertificate} for more information on the parameters and returned data type. + */ + GetGiftCertificate: 'getGiftCertificate' +} as const + +/** + * WARNING: This method is not implemented. + * + * A hook for performing mutations with the Shopper Gift Certificates API. + */ +export function useShopperGiftCertificatesMutation() { + NotImplementedError() +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts index 759c66c25c..8184ee2c3e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts @@ -1,27 +1,48 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' +import {useMutation} from '../useMutation' +import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' +type Client = ApiClients['shopperGiftCertificates'] + +// TODO: Our general convention is GET = query, POST/etc. = mutation. However, this is conceptually +// a query (and is called "get"!); I assume it is a POST because gift certificates are secret. +// Should this be a query hook or a mutation hook? export const ShopperGiftCertificatesMutations = { - /** - * WARNING: This method is not implemented. - * - * Action to retrieve an existing gift certificate. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-gift-certificates?meta=getGiftCertificate} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppergiftcertificates.shoppergiftcertificates-1.html#getgiftcertificate} for more information on the parameters and returned data type. - */ GetGiftCertificate: 'getGiftCertificate' } as const -/** - * WARNING: This method is not implemented. - * - * A hook for performing mutations with the Shopper Gift Certificates API. - */ -export function useShopperGiftCertificatesMutation() { - NotImplementedError() +export type ShopperGiftCertificatesMutation = + (typeof ShopperGiftCertificatesMutations)[keyof typeof ShopperGiftCertificatesMutations] + +export function useShopperGiftCertificatesMutation< + Mutation extends ShopperGiftCertificatesMutation +>( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + // The `Options` and `Data` types for each mutation are similar, but distinct, and the union + // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. + // I'm not sure if there's a way to avoid the type assertions in here for the methods that + // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply + // re-asserting what we already have. + const {shopperGiftCertificates: client} = useCommerceApi() + type Options = Argument + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts new file mode 100644 index 0000000000..ea52a81bd9 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2022, 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 {DataType, Argument, ApiClients} from '../types' +import {useMutation} from '../useMutation' +import {MutationFunction, UseMutationResult, useQueryClient} from '@tanstack/react-query' +import {updateCache, CacheUpdateMatrixElement, Client, NotImplementedError} from '../utils' + +export const ShopperOrdersMutations = { + /** + * Submits an order based on a prepared basket. The only considered value from the request body is basketId. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createOrder} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createorder} for more information on the parameters and returned data type. + */ + CreateOrder: 'createOrder', + /** + * WARNING: This method is not implemented. + * + * Adds a payment instrument to an order. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createPaymentInstrumentForOrder} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createpaymentinstrumentfororder} for more information on the parameters and returned data type. + */ + CreatePaymentInstrumentForOrder: 'createPaymentInstrumentForOrder', + /** + * WARNING: This method is not implemented. + * + * Removes a payment instrument of an order. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=removePaymentInstrumentFromOrder} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#removepaymentinstrumentfromorder} for more information on the parameters and returned data type. + */ + RemovePaymentInstrumentFromOrder: 'removePaymentInstrumentFromOrder', + /** + * WARNING: This method is not implemented. + * + * Updates a payment instrument of an order. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=updatePaymentInstrumentForOrder} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#updatepaymentinstrumentfororder} for more information on the parameters and returned data type. + */ + UpdatePaymentInstrumentForOrder: 'updatePaymentInstrumentForOrder' +} as const + +export const shopperOrdersCacheUpdateMatrix = { + createOrder: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + const customerId = response?.customerInfo?.customerId + return { + update: [ + { + name: 'order', + key: ['/orders', {orderNo: response.orderNo}], + updater: () => response + } + ], + invalidate: [{name: 'customerBaskets', key: ['/customers', customerId, '/baskets']}] + } + }, + createPaymentInstrumentForOrder: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + removePaymentInstrumentFromOrder: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + }, + updatePaymentInstrumentForOrder: ( + params: Argument, + response: DataType + ): CacheUpdateMatrixElement => { + return {} + } +} + +export const SHOPPER_ORDERS_NOT_IMPLEMENTED = [ + 'CreatePaymentInstrumentForOrder', + 'RemovePaymentInstrumentFromOrder', + 'UpdatePaymentInstrumentForOrder' +] + +export type ShopperOrdersMutationType = + (typeof ShopperOrdersMutations)[keyof typeof ShopperOrdersMutations] + +type UseShopperOrdersMutationHeaders = NonNullable>['headers'] +type UseShopperOrdersMutationArg = { + headers?: UseShopperOrdersMutationHeaders + rawResponse?: boolean + action: ShopperOrdersMutationType +} + +type ShopperOrdersClient = ApiClients['shopperOrders'] + +/** + * A hook for performing mutations with the Shopper Orders API. + */ + +function useShopperOrdersMutation( + arg: UseShopperOrdersMutationArg +): UseMutationResult< + DataType | Response, + Error, + Argument +> { + const {headers, rawResponse, action} = arg + + if (SHOPPER_ORDERS_NOT_IMPLEMENTED.includes(action)) { + NotImplementedError() + } + type Params = Argument + type Data = DataType + const queryClient = useQueryClient() + + return useMutation( + (params, apiClients) => { + const method = apiClients['shopperOrders'][action] as MutationFunction + return ( + method.call as ( + apiClient: ShopperOrdersClient, + params: Params, + rawResponse: boolean | undefined + ) => any + )(apiClients['shopperOrders'], {...params, headers}, rawResponse) + }, + { + onSuccess: (data, params) => { + updateCache(queryClient, action, shopperOrdersCacheUpdateMatrix, data, params) + } + } + ) +} + +export {useShopperOrdersMutation} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts index ea52a81bd9..7fa3cc8a1f 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts @@ -1,139 +1,46 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {DataType, Argument, ApiClients} from '../types' +import {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' import {useMutation} from '../useMutation' -import {MutationFunction, UseMutationResult, useQueryClient} from '@tanstack/react-query' -import {updateCache, CacheUpdateMatrixElement, Client, NotImplementedError} from '../utils' +import {UseMutationResult} from '@tanstack/react-query' +import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' + +type Client = ApiClients['shopperOrders'] export const ShopperOrdersMutations = { - /** - * Submits an order based on a prepared basket. The only considered value from the request body is basketId. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createorder} for more information on the parameters and returned data type. - */ CreateOrder: 'createOrder', - /** - * WARNING: This method is not implemented. - * - * Adds a payment instrument to an order. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createPaymentInstrumentForOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createpaymentinstrumentfororder} for more information on the parameters and returned data type. - */ CreatePaymentInstrumentForOrder: 'createPaymentInstrumentForOrder', - /** - * WARNING: This method is not implemented. - * - * Removes a payment instrument of an order. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=removePaymentInstrumentFromOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#removepaymentinstrumentfromorder} for more information on the parameters and returned data type. - */ RemovePaymentInstrumentFromOrder: 'removePaymentInstrumentFromOrder', - /** - * WARNING: This method is not implemented. - * - * Updates a payment instrument of an order. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=updatePaymentInstrumentForOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#updatepaymentinstrumentfororder} for more information on the parameters and returned data type. - */ UpdatePaymentInstrumentForOrder: 'updatePaymentInstrumentForOrder' } as const -export const shopperOrdersCacheUpdateMatrix = { - createOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const customerId = response?.customerInfo?.customerId - return { - update: [ - { - name: 'order', - key: ['/orders', {orderNo: response.orderNo}], - updater: () => response - } - ], - invalidate: [{name: 'customerBaskets', key: ['/customers', customerId, '/baskets']}] - } - }, - createPaymentInstrumentForOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - removePaymentInstrumentFromOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - updatePaymentInstrumentForOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - } -} - -export const SHOPPER_ORDERS_NOT_IMPLEMENTED = [ - 'CreatePaymentInstrumentForOrder', - 'RemovePaymentInstrumentFromOrder', - 'UpdatePaymentInstrumentForOrder' -] - -export type ShopperOrdersMutationType = +export type ShopperOrdersMutation = (typeof ShopperOrdersMutations)[keyof typeof ShopperOrdersMutations] -type UseShopperOrdersMutationHeaders = NonNullable>['headers'] -type UseShopperOrdersMutationArg = { - headers?: UseShopperOrdersMutationHeaders - rawResponse?: boolean - action: ShopperOrdersMutationType -} - -type ShopperOrdersClient = ApiClients['shopperOrders'] - -/** - * A hook for performing mutations with the Shopper Orders API. - */ - -function useShopperOrdersMutation( - arg: UseShopperOrdersMutationArg -): UseMutationResult< - DataType | Response, - Error, - Argument -> { - const {headers, rawResponse, action} = arg - - if (SHOPPER_ORDERS_NOT_IMPLEMENTED.includes(action)) { - NotImplementedError() - } - type Params = Argument - type Data = DataType - const queryClient = useQueryClient() - - return useMutation( - (params, apiClients) => { - const method = apiClients['shopperOrders'][action] as MutationFunction - return ( - method.call as ( - apiClient: ShopperOrdersClient, - params: Params, - rawResponse: boolean | undefined - ) => any - )(apiClients['shopperOrders'], {...params, headers}, rawResponse) - }, - { - onSuccess: (data, params) => { - updateCache(queryClient, action, shopperOrdersCacheUpdateMatrix, data, params) - } - } - ) +export function useShopperOrdersMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + // The `Options` and `Data` types for each mutation are similar, but distinct, and the union + // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. + // I'm not sure if there's a way to avoid the type assertions in here for the methods that + // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply + // re-asserting what we already have. + const {shopperOrders: client} = useCommerceApi() + type Options = Argument + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> + }) } - -export {useShopperOrdersMutation} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts index 48d7b5a836..265ffe8561 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts @@ -1,23 +1,16 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType} from '../types' -import {useQuery} from '../useQuery' import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {NotImplementedError} from '../utils' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' type Client = ApiClients['shopperOrders'] -type UseOrderParameters = NonNullable>['parameters'] -type UseOrderHeaders = NonNullable>['headers'] -type UseOrderArg = { - headers?: UseOrderHeaders - rawResponse?: boolean -} & UseOrderParameters - /** * A hook for `ShopperOrders#getOrder`. * Gets information for an order. @@ -25,54 +18,107 @@ type UseOrderArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#getorder} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useOrder( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useOrder( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useOrder( - arg: UseOrderArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - ['/orders', arg], - (_, {shopperOrders}) => { - return shopperOrders.getOrder({parameters, headers}, rawResponse) - }, - options - ) -} +export const useOrder = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperOrders: client} = useCommerceApi() + const method = (arg: Argument) => client.getOrder(arg) + const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/orders/', + parameters.orderNo, + // Full parameters last for easy lookup + parameters + ] as const + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperOrders#getPaymentMethodsForOrder`. * Gets the applicable payment methods for an existing order considering the open payment amount only. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getPaymentMethodsForOrder} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#getpaymentmethodsfororder} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePaymentMethodsForOrder(): void { - NotImplementedError() -} +export const usePaymentMethodsForOrder = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperOrders: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPaymentMethodsForOrder(arg) + const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/orders/', + parameters.orderNo, + '/payment-methods', + // Full parameters last for easy lookup + parameters + ] as const + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperOrders#getTaxesFromOrder`. - * This method gives you the external taxation data of the order transferred from the basket during -order creation. This endpoint can be called only if external taxation was used. See POST /baskets -for more information. + * This method gives you the external taxation data of the order transferred from the basket during +order creation. This endpoint can be called only if external taxation was used. See POST /baskets +for more information. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getTaxesFromOrder} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#gettaxesfromorder} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useTaxesFromOrder(): void { - NotImplementedError() -} +export const useTaxesFromOrder = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperOrders: client} = useCommerceApi() + const method = (arg: Argument) => client.getTaxesFromOrder(arg) + const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/orders/', + parameters.orderNo, + '/taxes', + // Full parameters last for easy lookup + parameters + ] as const -export {useOrder, usePaymentMethodsForOrder, useTaxesFromOrder} + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts index c1ad88c8e0..3a5f65c2bb 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts @@ -1,54 +1,48 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType} from '../types' -import {useQuery} from '../useQuery' -import useConfig from '../useConfig' import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' type Client = ApiClients['shopperProducts'] -type UseProductsParameters = NonNullable>['parameters'] -type UseProductsHeaders = NonNullable>['headers'] -type UseProductsArg = {headers?: UseProductsHeaders; rawResponse?: boolean} & UseProductsParameters /** * A hook for `ShopperProducts#getProducts`. - * Allows access to multiple products by a single request. Only products that are online and assigned to a site catalog are returned. The maximum number of productIDs that can be requested are 24. Along with product details, the availability, images, price, promotions, and variations for the valid products will be included, as appropriate. + * Allows access to multiple products by a single request. Only products that are online and assigned to a site catalog are returned. The maximum number of productIDs that can be requested are 24. Along with product details, the availability, product options, images, price, promotions, and variations for the valid products will be included, as appropriate. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getProducts} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getproducts} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useProducts( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useProducts( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useProducts( - arg: UseProductsArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale, currency} = useConfig() - parameters.locale = parameters.locale || locale - parameters.currency = parameters.currency || currency - return useQuery( - ['/products', arg], - (_, {shopperProducts}) => { - return shopperProducts.getProducts({parameters, headers}, rawResponse) - }, - options - ) -} +export const useProducts = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperProducts: client} = useCommerceApi() + const method = (arg: Argument) => client.getProducts(arg) + const requiredParameters = ['organizationId', 'ids', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/products', + // Full parameters last for easy lookup + parameters + ] as const -type UseProductParameters = NonNullable>['parameters'] -type UseProductHeaders = NonNullable>['headers'] -type UseProductArg = {headers?: UseProductHeaders; rawResponse?: boolean} & UseProductParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperProducts#getProduct`. * Allows access to product details for a single product ID. Only products that are online and assigned to a site catalog are returned. Along with product details, the availability, images, price, bundled_products, set_products, recommedations, product options, variations, and promotions for the products will be included, as appropriate. @@ -56,37 +50,32 @@ type UseProductArg = {headers?: UseProductHeaders; rawResponse?: boolean} & UseP * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getproduct} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useProduct( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useProduct( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useProduct( - arg: UseProductArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale, currency} = useConfig() - parameters.locale = parameters.locale || locale - parameters.currency = parameters.currency || currency - return useQuery( - ['/products', arg], - (_, {shopperProducts}) => { - return shopperProducts.getProduct({parameters, headers}, rawResponse) - }, - options - ) -} +export const useProduct = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperProducts: client} = useCommerceApi() + const method = (arg: Argument) => client.getProduct(arg) + const requiredParameters = ['organizationId', 'id', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/products/', + parameters.id, + // Full parameters last for easy lookup + parameters + ] as const -type UseCategoriesParameters = NonNullable>['parameters'] -type UseCategoriesHeaders = NonNullable>['headers'] -type UseCategoriesArg = { - headers?: UseCategoriesHeaders - rawResponse?: boolean -} & UseCategoriesParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperProducts#getCategories`. * When you use the URL template, the server returns multiple categories (a result object of category documents). You can use this template as a convenient way of obtaining multiple categories in a single request, instead of issuing separate requests for each category. You can specify up to 50 multiple IDs. You must enclose the list of IDs in parentheses. If a category identifier contains parenthesis or the separator sign, you must URL encode the character. The server only returns online categories. @@ -94,67 +83,63 @@ type UseCategoriesArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getcategories} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCategories( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCategories( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useCategories( - arg: UseCategoriesArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale} = useConfig() - parameters.locale = parameters.locale || locale - return useQuery( - ['/categories', arg], - (_, {shopperProducts}) => { - return shopperProducts.getCategories({parameters, headers}, rawResponse) - }, - options - ) -} +export const useCategories = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperProducts: client} = useCommerceApi() + const method = (arg: Argument) => client.getCategories(arg) + const requiredParameters = ['organizationId', 'ids', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/categories', + // Full parameters last for easy lookup + parameters + ] as const -type UseCategoryParameters = NonNullable>['parameters'] -type UseCategoryHeaders = NonNullable>['headers'] -type UseCategoryArg = { - headers?: UseCategoryHeaders - rawResponse?: boolean -} & UseCategoryParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperProducts#getCategory`. * When you use the URL template below, the server returns a category identified by its ID; by default, the server - also returns the first level of subcategories, but you can specify another level by setting the levels - parameter. The server only returns online categories. +also returns the first level of subcategories, but you can specify another level by setting the levels +parameter. The server only returns online categories. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getCategory} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getcategory} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useCategory( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useCategory( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useCategory( - arg: UseCategoryArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale} = useConfig() - parameters.locale = parameters.locale || locale - return useQuery( - ['/categories', arg], - (_, {shopperProducts}) => { - return shopperProducts.getCategory({parameters, headers}, rawResponse) - }, - options - ) -} +export const useCategory = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperProducts: client} = useCommerceApi() + const method = (arg: Argument) => client.getCategory(arg) + const requiredParameters = ['organizationId', 'id', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/categories/', + parameters.id, + // Full parameters last for easy lookup + parameters + ] as const -export {useProducts, useProduct, useCategories, useCategory} + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts index 1d99862bcf..0cbaf132ea 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts @@ -1,22 +1,16 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType} from '../types' -import {useQuery} from '../useQuery' import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {NotImplementedError} from '../utils' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' type Client = ApiClients['shopperPromotions'] -type UsePromotionsParameters = NonNullable>['parameters'] -type UsePromotionsHeaders = NonNullable>['headers'] -type UsePromotionsArg = { - headers?: UsePromotionsHeaders - rawResponse?: boolean -} & UsePromotionsParameters /** * A hook for `ShopperPromotions#getPromotions`. * Returns an array of enabled promotions for a list of specified IDs. In the request URL, you can specify up to 50 IDs. If you specify an ID that contains either parentheses or the separator characters, you must URL encode these characters. Each request returns only enabled promotions as the server does not consider promotion qualifiers or schedules. @@ -24,43 +18,71 @@ type UsePromotionsArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperpromotions.shopperpromotions-1.html#getpromotions} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePromotions( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function usePromotions( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function usePromotions( - arg: UsePromotionsArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - return useQuery( - ['/promotions', arg], - (_, {shopperPromotions}) => { - return shopperPromotions.getPromotions({parameters, headers}, rawResponse) - }, - options - ) -} +export const usePromotions = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperPromotions: client} = useCommerceApi() + const method = (arg: Argument) => client.getPromotions(arg) + const requiredParameters = ['organizationId', 'siteId', 'ids'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/promotions', + // Full parameters last for easy lookup + parameters + ] as const + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** - * WARNING: This method is not implemented yet. - * * A hook for `ShopperPromotions#getPromotionsForCampaign`. * Handles get promotion by filter criteria. Returns an array of enabled promotions matching the specified filter criteria. In the request URL, you must provide a campaign_id parameter, and you can optionally specify a date -range by providing start_date and end_date parameters. Both parameters are required to specify a date range, as +range by providing start_date and end_date parameters. Both parameters are required to specify a date range, as omitting one causes the server to return a MissingParameterException fault. Each request returns only enabled promotions, since the server does not consider promotion qualifiers or schedules. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-promotions?meta=getPromotionsForCampaign} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperpromotions.shopperpromotions-1.html#getpromotionsforcampaign} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePromotionsForCampaign(): void { - NotImplementedError() -} +export const usePromotionsForCampaign = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperPromotions: client} = useCommerceApi() + const method = (arg: Argument) => + client.getPromotionsForCampaign(arg) + const requiredParameters = ['organizationId', 'campaignId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/promotions/campaigns/', + parameters.campaignId, + // Full parameters last for easy lookup + parameters + ] as const -export {usePromotions, usePromotionsForCampaign} + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts index d743fcad0f..55e2c513cf 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts @@ -1,22 +1,16 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType} from '../types' -import {useQuery} from '../useQuery' import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import useConfig from '../useConfig' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' type Client = ApiClients['shopperSearch'] -type UseProductSearchParameters = NonNullable>['parameters'] -type UseProductSearchHeaders = NonNullable>['headers'] -type UseProductSearchArg = { - headers?: UseProductSearchHeaders - rawResponse?: boolean -} & UseProductSearchParameters /** * A hook for `ShopperSearch#productSearch`. * Provides keyword and refinement search functionality for products. Only returns the product ID, link, and name in @@ -25,37 +19,31 @@ the product search hit. The search result contains only products that are online * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppersearch.shoppersearch-1.html#productsearch} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useProductSearch( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useProductSearch( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useProductSearch( - arg: UseProductSearchArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale, currency} = useConfig() - parameters.locale = parameters.locale || locale - parameters.currency = parameters.currency || currency - return useQuery( - ['/product-search', arg], - (_, {shopperSearch}) => shopperSearch.productSearch({parameters, headers}, rawResponse), - options - ) -} +export const useProductSearch = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperSearch: client} = useCommerceApi() + const method = (arg: Argument) => client.productSearch(arg) + const requiredParameters = ['organizationId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/product-search', + // Full parameters last for easy lookup + parameters + ] as const -type UseSearchSuggestionsParameters = NonNullable< - Argument ->['parameters'] -type UseSearchSuggestionsHeaders = NonNullable>['headers'] -type UseSearchSuggestionsArg = { - headers?: UseSearchSuggestionsHeaders - rawResponse?: boolean -} & UseSearchSuggestionsParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperSearch#getSearchSuggestions`. * Provides keyword search functionality for products, categories, and brands suggestions. Returns suggested products, suggested categories, and suggested brands for the given search phrase. @@ -63,28 +51,31 @@ type UseSearchSuggestionsArg = { * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppersearch.shoppersearch-1.html#getsearchsuggestions} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function useSearchSuggestions( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function useSearchSuggestions( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function useSearchSuggestions( - arg: UseSearchSuggestionsArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale, currency} = useConfig() - parameters.locale = parameters.locale || locale - parameters.currency = parameters.currency || currency - return useQuery( - ['/search-suggestions', arg], - (_, {shopperSearch}) => - shopperSearch.getSearchSuggestions({parameters, headers}, rawResponse), - options - ) -} +export const useSearchSuggestions = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperSearch: client} = useCommerceApi() + const method = (arg: Argument) => + client.getSearchSuggestions(arg) + const requiredParameters = ['organizationId', 'siteId', 'q'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/search-suggestions', + // Full parameters last for easy lookup + parameters + ] as const -export {useProductSearch, useSearchSuggestions} + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} From 8b9e7f217d2d60860b7638d32948d96fc54c8844 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 16:52:20 -0500 Subject: [PATCH 022/122] Update Shopper Baskets config from Test directory. --- .../src/hooks/ShopperBaskets/config.ts | 120 ++++++++++++------ 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts index 187633861e..9fb461d6f6 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts @@ -4,67 +4,109 @@ * 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 {ShopperBasketsTypes} from 'commerce-sdk-isomorphic' -import {ApiClients, CacheUpdateMatrix} from '../types' -import {CacheUpdate} from '../types' +import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' +import { + ApiClients, + ApiOptions, + CacheUpdate, + CacheUpdateMatrix, + CacheUpdateUpdate, + MergedOptions +} from '../types' +import {and, matchesApiConfig, pathStartsWith} from '../utils' -const updateBasketQuery = (basketId?: string): CacheUpdate => { - // TODO: we're missing headers, rawResponse -> not only {basketId} - const arg = {basketId} - return basketId - ? { - update: [['/baskets', basketId, arg]] - } - : {} +type Client = ApiClients['shopperBaskets'] +type Basket = ShopperBasketsTypes.Basket +type CustomerBasketsResult = ShopperCustomersTypes.BasketsResult +type BasketOptions = MergedOptions> + +const updateBasketQuery = ( + customerId: string | null, + basketId: string | undefined, + newBasket: Basket +): Pick => { + if (!basketId) return {} + const update: Array | CacheUpdateUpdate> = [ + { + queryKey: ['/baskets', basketId, {basketId}], + updater: newBasket + } + ] + if (customerId) { + const updateCustomerBaskets: CacheUpdateUpdate = { + // Since we use baskets from customer basket query, we need to update it for any basket mutation + queryKey: ['/customers', customerId, '/baskets', {customerId}], + updater: (oldData) => { + // do not update if response basket is not part of existing customer baskets + if (!oldData?.baskets?.some((basket) => basket.basketId === basketId)) { + return undefined + } + const updatedBaskets = oldData.baskets.map((basket) => { + return basket.basketId === basketId ? newBasket : basket + }) + return { + ...oldData, + // TODO: Remove type assertion when RAML specs match + baskets: updatedBaskets as CustomerBasketsResult['baskets'] + } + } + } + update.push(updateCustomerBaskets) + } + // TODO: This type assertion is so that we "forget" what type the updater uses. + // Is there a way to avoid the assertion? + return {update} as CacheUpdate } -const removeBasketQuery = (basketId?: string): CacheUpdate => { - const arg = {basketId} - return basketId - ? { - remove: [['/baskets', basketId, arg]] - } - : {} +const removeBasketQuery = ( + options: BasketOptions, + basketId?: string +): Pick => { + if (!basketId) return {} + return { + remove: [and(matchesApiConfig(options), pathStartsWith(['/baskets', basketId]))] + } } -const invalidateCustomerBasketsQuery = (customerId: string | null): CacheUpdate => { - // TODO: should we use arg here or not? The invalidate method does not need exact query key. - const arg = {customerId} - return customerId - ? { - invalidate: [['/customers', customerId, '/baskets', arg]] - } - : {} +const invalidateCustomerBasketsQuery = ( + customerId: string | null, + options: BasketOptions +): Pick => { + if (!customerId) return {} + return { + invalidate: [ + and(matchesApiConfig(options), pathStartsWith(['/customers', customerId, '/baskets'])) + ] + } } const updateBasketFromRequest = ( customerId: string | null, - params: { - parameters: ShopperBasketsTypes.Basket - } + options: BasketOptions, + response: Basket ): CacheUpdate => ({ - ...updateBasketQuery(params.parameters.basketId), - ...invalidateCustomerBasketsQuery(customerId) + ...updateBasketQuery(customerId, options.parameters?.basketId, response), + ...invalidateCustomerBasketsQuery(customerId, options) }) const updateBasketFromResponse = ( customerId: string | null, - params: unknown, // not used, - response: ShopperBasketsTypes.Basket + options: BasketOptions, + response: Basket ): CacheUpdate => ({ - ...updateBasketQuery(response.basketId), - ...invalidateCustomerBasketsQuery(customerId) + ...updateBasketQuery(customerId, response.basketId, response), + ...invalidateCustomerBasketsQuery(customerId, options) }) -export const cacheUpdateMatrix: CacheUpdateMatrix = { +export const cacheUpdateMatrix: CacheUpdateMatrix = { addCouponToBasket: updateBasketFromRequest, addItemToBasket: updateBasketFromRequest, removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, params): CacheUpdate => ({ - ...invalidateCustomerBasketsQuery(customerId), - ...removeBasketQuery(params.parameters.basketId) + deleteBasket: (customerId, options): CacheUpdate => ({ + ...invalidateCustomerBasketsQuery(customerId, options), + ...removeBasketQuery(options) }), mergeBasket: updateBasketFromResponse, // Response! removeCouponFromBasket: updateBasketFromRequest, From 6b30735ae44e5557f255b6e896a747e64ff3cfb6 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 10 Feb 2023 16:57:45 -0500 Subject: [PATCH 023/122] Update Shopper Experience query hooks. --- .../src/hooks/ShopperExperience/query.ts | 116 +++++++++--------- .../src/hooks/ShopperLogin/index.ts | 5 +- 2 files changed, 62 insertions(+), 59 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts index 5a09fe68af..3b2ec5b1ca 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts @@ -4,79 +4,81 @@ * 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 {ApiClients, Argument, DataType} from '../types' -import {useQuery} from '../useQuery' import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import useConfig from '../useConfig' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' type Client = ApiClients['shopperExperience'] -type UsePagesParameters = NonNullable>['parameters'] -type UsePagesHeaders = NonNullable>['headers'] -type UsePagesArg = {headers?: UsePagesHeaders; rawResponse?: boolean} & UsePagesParameters /** * A hook for `ShopperExperience#getPages`. * Get Page Designer pages. The results will apply the visibility rules for each page's components, such as personalization or scheduled visibility. + +Either `categoryId` or `productId` must be given in addition to `aspectTypeId`. Because only a single page-to-product and page-to-category assignment per aspect type can be authored today, the returned results contains one element at most. + +**Important**: Because this resource uses the GET method, you must not pass sensitive data (payment card information, for example) and must not perform any transactional processes within the server-side scripts that are run for the page and components. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-experience?meta=getPages} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperexperience.shopperexperience-1.html#getpages} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -function usePages( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function usePages( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function usePages( - arg: UsePagesArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale} = useConfig() - parameters.locale = parameters.locale || locale - return useQuery( - ['/pages', arg], - (_, {shopperExperience}) => { - return shopperExperience.getPages({parameters, headers}, rawResponse) - }, - options - ) -} +export const usePages = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperExperience: client} = useCommerceApi() + const method = (arg: Argument) => client.getPages(arg) + const requiredParameters = ['organizationId', 'aspectTypeId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/pages', + // Full parameters last for easy lookup + parameters + ] as const -type UsePageParameters = NonNullable>['parameters'] -type UsePageHeaders = NonNullable>['headers'] -type UsePageArg = {headers?: UsePageHeaders; rawResponse?: boolean} & UsePageParameters + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} /** * A hook for `ShopperExperience#getPage`. * Get a Page Designer page based on a single page ID. The results will apply the visibility rules for the page's components, such as personalization or scheduled visibility. + +**Important**: Because this resource uses the GET method, you must not pass sensitive data (payment card information, for example) and must not perform any transactional processes within the server-side scripts that are run for the page and components. * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-experience?meta=getPage} for more information about the API endpoint. * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperexperience.shopperexperience-1.html#getpage} for more information on the parameters and returned data type. - * @returns A promise of type Page. + * @returns An object describing the state of the request. */ -function usePage( - arg: Omit & {rawResponse?: false}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult, Error> -function usePage( - arg: Omit & {rawResponse: true}, - options?: UseQueryOptions | Response, Error> -): UseQueryResult -function usePage( - arg: UsePageArg, - options?: UseQueryOptions | Response, Error> -): UseQueryResult | Response, Error> { - const {headers, rawResponse, ...parameters} = arg - const {locale} = useConfig() - parameters.locale = parameters.locale || locale - return useQuery( - ['/pages', arg], - (_, {shopperExperience}) => { - return shopperExperience.getPage({parameters, headers}, rawResponse) - }, - options - ) -} +export const usePage = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperExperience: client} = useCommerceApi() + const method = (arg: Argument) => client.getPage(arg) + const requiredParameters = ['organizationId', 'pageId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/pages/', + parameters.pageId, + // Full parameters last for easy lookup + parameters + ] as const -export {usePages, usePage} + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts index ee0321f5ac..1a13c64feb 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts @@ -1,7 +1,8 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 */ -export * from './helper' +export * from './mutation' +export * from './query' From b3b36b2eed2f397dc258bd83821a034b463182e1 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 13 Feb 2023 13:25:09 -0500 Subject: [PATCH 024/122] Fix missed merged options type. --- packages/commerce-sdk-react/src/hooks/useQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index 0ffebc8972..a2cae46f8b 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -29,7 +29,7 @@ export const useQuery = < client: Client method: ApiMethod getQueryKey: (options: MergedOptions) => QK - requiredParameters: ReadonlyArray + requiredParameters: ReadonlyArray['parameters']> enabled?: boolean } ) => { From 71e8831fab5dd1ae75fcdec8d8cdeceb02c44f08 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 13 Feb 2023 13:29:09 -0500 Subject: [PATCH 025/122] Add Shopper Login hooks. --- .../src/hooks/ShopperLogin/config.ts | 22 ++ .../src/hooks/ShopperLogin/index.ts | 1 + .../src/hooks/ShopperLogin/mutation.ts | 53 ++++ .../src/hooks/ShopperLogin/query.ts | 278 ++++++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperLogin/config.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/config.ts new file mode 100644 index 0000000000..0a98ca9302 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/config.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023, 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 {ApiClients, CacheUpdateMatrix} from '../types' + +const noop = () => ({}) +export const cacheUpdateMatrix: CacheUpdateMatrix = { + authenticateCustomer: noop, + authorizePasswordlessCustomer: noop, + getAccessToken: noop, + getSessionBridgeAccessToken: noop, + getTrustedSystemAccessToken: noop, + getTrustedAgentAccessToken: noop, + getPasswordResetToken: noop, + resetPassword: noop, + getPasswordLessAccessToken: noop, + revokeToken: noop, + introspectToken: noop +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts index 1a13c64feb..6a11ab19a2 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts @@ -6,3 +6,4 @@ */ export * from './mutation' export * from './query' +export * from './helper' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts new file mode 100644 index 0000000000..e8823f9f4e --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023, 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' +import {useMutation} from '../useMutation' +import {UseMutationResult} from '@tanstack/react-query' +import {NotImplementedError} from '../utils' +import useCommerceApi from '../useCommerceApi' +import {cacheUpdateMatrix} from './config' + +type Client = ApiClients['shopperLogin'] + +export const ShopperLoginMutations = { + AuthenticateCustomer: 'authenticateCustomer', + AuthorizePasswordlessCustomer: 'authorizePasswordlessCustomer', + GetAccessToken: 'getAccessToken', + GetSessionBridgeAccessToken: 'getSessionBridgeAccessToken', + GetTrustedSystemAccessToken: 'getTrustedSystemAccessToken', + GetTrustedAgentAccessToken: 'getTrustedAgentAccessToken', + GetPasswordResetToken: 'getPasswordResetToken', + ResetPassword: 'resetPassword', + GetPasswordLessAccessToken: 'getPasswordLessAccessToken', + RevokeToken: 'revokeToken', + IntrospectToken: 'introspectToken' +} as const + +export type ShopperLoginMutation = + (typeof ShopperLoginMutations)[keyof typeof ShopperLoginMutations] + +export function useShopperLoginMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + const getCacheUpdates = cacheUpdateMatrix[mutation] + // TODO: Remove this check when all mutations are implemented. + if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) + + // The `Options` and `Data` types for each mutation are similar, but distinct, and the union + // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. + // I'm not sure if there's a way to avoid the type assertions in here for the methods that + // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply + // re-asserting what we already have. + const {shopperLogin: client} = useCommerceApi() + type Options = Argument + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts new file mode 100644 index 0000000000..9b592799ff --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2023, 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' + +type Client = ApiClients['shopperLogin'] + +/** + * A hook for `ShopperLogin#retrieveCredQualityUserInfo`. + * Get credential quality statistics for a user. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=retrieveCredQualityUserInfo} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#retrievecredqualityuserinfo} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useRetrieveCredQualityUserInfo = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperLogin: client} = useCommerceApi() + const method = (arg: Argument) => + client.retrieveCredQualityUserInfo(arg) + const requiredParameters = ['organizationId', 'username'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/cred-qual/user', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperLogin#logoutCustomer`. + * Log out a shopper. The shopper's access token and refresh token are revoked. If the shopper authenticated with a B2C Commerce (ECOM) instance, the OCAPI JWT is also revoked. This should be called for Registered users that have logged in using SLAS. his should be called for registered users that have logged in using SLAS. This endpoint is not for use with guest users. + +Required header: Authorization header bearer token of the Shopper access token to logout. + +Required parameters: `refresh token`, `channel_id`, and `client`. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=logoutCustomer} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#logoutcustomer} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useLogoutCustomer = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperLogin: client} = useCommerceApi() + const method = (arg: Argument) => client.logoutCustomer(arg) + const requiredParameters = ['organizationId', 'client_id', 'refresh_token'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/oauth2/logout', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperLogin#authorizeCustomer`. + * Get an authorization code after authenticating a user against an identity provider (IDP). This is the first step of the OAuth 2.1 authorization code flow, where a user can log in via federation to the IDP configured for the client. After successfully logging in, the user gets an authorization code via a redirect URI. + +This endpoint can be called from the front channel (the browser). + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=authorizeCustomer} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#authorizecustomer} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useAuthorizeCustomer = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperLogin: client} = useCommerceApi() + const method = (arg: Argument) => client.authorizeCustomer(arg) + const requiredParameters = [ + 'organizationId', + 'redirect_uri', + 'response_type', + 'client_id', + 'code_challenge' + ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/oauth2/authorize', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperLogin#getTrustedAgentAuthorizationToken`. + * Obtains a new agent on behalf authorization token for a registered customer. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getTrustedAgentAuthorizationToken} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#gettrustedagentauthorizationtoken} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useTrustedAgentAuthorizationToken = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperLogin: client} = useCommerceApi() + const method = (arg: Argument) => + client.getTrustedAgentAuthorizationToken(arg) + const requiredParameters = [ + 'organizationId', + 'client_id', + 'channel_id', + 'code_challenge', + 'login_id', + 'idp_origin', + 'redirect_uri', + 'response_type' + ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/oauth2/trusted-agent/authorize', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperLogin#getUserInfo`. + * Returns a JSON listing of claims about the currently authenticated user. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getUserInfo} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getuserinfo} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useUserInfo = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperLogin: client} = useCommerceApi() + const method = (arg: Argument) => client.getUserInfo(arg) + const requiredParameters = ['organizationId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/oauth2/userinfo', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperLogin#getWellknownOpenidConfiguration`. + * Returns a JSON listing of the OpenID/OAuth endpoints, supported scopes and claims, public keys used to sign the tokens, and other details. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getWellknownOpenidConfiguration} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getwellknownopenidconfiguration} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useWellknownOpenidConfiguration = ( + apiOptions: Argument, + queryOptions: Omit< + UseQueryOptions>, + 'queryFn' + > = {} +): UseQueryResult> => { + const {shopperLogin: client} = useCommerceApi() + const method = (arg: Argument) => + client.getWellknownOpenidConfiguration(arg) + const requiredParameters = ['organizationId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/oauth2/.well-known/openid-configuration', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} +/** + * A hook for `ShopperLogin#getJwksUri`. + * Returns a JSON Web Key Set (JWKS) containing the current, past, and future public keys. The key set enables clients to validate the Shopper JSON Web Token (JWT) issued by SLAS. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getJwksUri} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getjwksuri} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useJwksUri = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperLogin: client} = useCommerceApi() + const method = (arg: Argument) => client.getJwksUri(arg) + const requiredParameters = ['organizationId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({parameters}: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/oauth2/jwks', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} From 0a9ef34810ccbd5a4d191a9febc6e20928af2971 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 13 Feb 2023 14:04:50 -0500 Subject: [PATCH 026/122] Convert getGiftCertificate from mutation hook to query hook. --- .../hooks/ShopperGiftCertificates/config.ts | 27 ----------- .../hooks/ShopperGiftCertificates/index.ts | 4 +- .../hooks/ShopperGiftCertificates/mutation.ts | 48 ------------------- .../hooks/ShopperGiftCertificates/query.ts | 47 ++++++++++++++++++ 4 files changed, 49 insertions(+), 77 deletions(-) delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts deleted file mode 100644 index 759c66c25c..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/config.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2022, 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 {NotImplementedError} from '../utils' - -export const ShopperGiftCertificatesMutations = { - /** - * WARNING: This method is not implemented. - * - * Action to retrieve an existing gift certificate. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-gift-certificates?meta=getGiftCertificate} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppergiftcertificates.shoppergiftcertificates-1.html#getgiftcertificate} for more information on the parameters and returned data type. - */ - GetGiftCertificate: 'getGiftCertificate' -} as const - -/** - * WARNING: This method is not implemented. - * - * A hook for performing mutations with the Shopper Gift Certificates API. - */ -export function useShopperGiftCertificatesMutation() { - NotImplementedError() -} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.ts index 3b351e1176..df1d7e713c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.ts @@ -1,7 +1,7 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 */ -export * from './mutation' +export * from './query' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts deleted file mode 100644 index 8184ee2c3e..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/mutation.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2023, 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' -import {useMutation} from '../useMutation' -import {UseMutationResult} from '@tanstack/react-query' -import {NotImplementedError} from '../utils' -import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' - -type Client = ApiClients['shopperGiftCertificates'] - -// TODO: Our general convention is GET = query, POST/etc. = mutation. However, this is conceptually -// a query (and is called "get"!); I assume it is a POST because gift certificates are secret. -// Should this be a query hook or a mutation hook? -export const ShopperGiftCertificatesMutations = { - GetGiftCertificate: 'getGiftCertificate' -} as const - -export type ShopperGiftCertificatesMutation = - (typeof ShopperGiftCertificatesMutations)[keyof typeof ShopperGiftCertificatesMutations] - -export function useShopperGiftCertificatesMutation< - Mutation extends ShopperGiftCertificatesMutation ->( - mutation: Mutation -): UseMutationResult, unknown, Argument> { - const getCacheUpdates = cacheUpdateMatrix[mutation] - // TODO: Remove this check when all mutations are implemented. - if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) - - // The `Options` and `Data` types for each mutation are similar, but distinct, and the union - // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. - // I'm not sure if there's a way to avoid the type assertions in here for the methods that - // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply - // re-asserting what we already have. - const {shopperGiftCertificates: client} = useCommerceApi() - type Options = Argument - type Data = DataType - return useMutation({ - client, - method: (opts: Options) => (client[mutation] as ApiMethod)(opts), - getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> - }) -} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts new file mode 100644 index 0000000000..6b0b60627a --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023, 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' +import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import useCommerceApi from '../useCommerceApi' +import {useQuery} from '../useQuery' + +type Client = ApiClients['shopperGiftCertificates'] + +/** + * A hook for `ShopperGiftCertificates#getGiftCertificate`. + * Action to retrieve an existing gift certificate. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-gift-certificates?meta=getGiftCertificate} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppergiftcertificates.shoppergiftcertificates-1.html#getgiftcertificate} for more information on the parameters and returned data type. + * @returns An object describing the state of the request. + */ +export const useGiftCertificate = ( + apiOptions: Argument, + queryOptions: Omit>, 'queryFn'> = {} +): UseQueryResult> => { + const {shopperGiftCertificates: client} = useCommerceApi() + const method = (arg: Argument) => client.getGiftCertificate(arg) + const requiredParameters = ['organizationId', 'siteId'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper + // hook, so we use a callback here that receives that merged object. + const getQueryKey = ({ + parameters + }: MergedOptions>) => + [ + '/organizations/', + parameters.organizationId, + '/gift-certificate', + // Full parameters last for easy lookup + parameters + ] as const + + return useQuery(apiOptions, queryOptions, { + client, + method, + requiredParameters, + getQueryKey + }) +} From d4cdf446f1f55357cd13dd0d6da0a83978a837fe Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 13 Feb 2023 14:05:50 -0500 Subject: [PATCH 027/122] Update Shopper Login hooks. Change logoutCustomer from query to mutation. Change useRetrieveCredQualityUserInfo to getCredQualityUserInfo. --- .../src/hooks/ShopperLogin/mutation.ts | 1 + .../src/hooks/ShopperLogin/query.ts | 38 +------------------ 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts index e8823f9f4e..5a41b8f3b1 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts @@ -16,6 +16,7 @@ type Client = ApiClients['shopperLogin'] export const ShopperLoginMutations = { AuthenticateCustomer: 'authenticateCustomer', AuthorizePasswordlessCustomer: 'authorizePasswordlessCustomer', + LogoutCustomer: 'logoutCustomer', GetAccessToken: 'getAccessToken', GetSessionBridgeAccessToken: 'getSessionBridgeAccessToken', GetTrustedSystemAccessToken: 'getTrustedSystemAccessToken', diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts index 9b592799ff..940df75de0 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts @@ -18,7 +18,7 @@ type Client = ApiClients['shopperLogin'] * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#retrievecredqualityuserinfo} for more information on the parameters and returned data type. * @returns An object describing the state of the request. */ -export const useRetrieveCredQualityUserInfo = ( +export const useCredQualityUserInfo = ( apiOptions: Argument, queryOptions: Omit< UseQueryOptions>, @@ -49,42 +49,6 @@ export const useRetrieveCredQualityUserInfo = ( getQueryKey }) } -/** - * A hook for `ShopperLogin#logoutCustomer`. - * Log out a shopper. The shopper's access token and refresh token are revoked. If the shopper authenticated with a B2C Commerce (ECOM) instance, the OCAPI JWT is also revoked. This should be called for Registered users that have logged in using SLAS. his should be called for registered users that have logged in using SLAS. This endpoint is not for use with guest users. - -Required header: Authorization header bearer token of the Shopper access token to logout. - -Required parameters: `refresh token`, `channel_id`, and `client`. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=logoutCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#logoutcustomer} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const useLogoutCustomer = ( - apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} -): UseQueryResult> => { - const {shopperLogin: client} = useCommerceApi() - const method = (arg: Argument) => client.logoutCustomer(arg) - const requiredParameters = ['organizationId', 'client_id', 'refresh_token'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/oauth2/logout', - // Full parameters last for easy lookup - parameters - ] as const - - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) -} /** * A hook for `ShopperLogin#authorizeCustomer`. * Get an authorization code after authenticating a user against an identity provider (IDP). This is the first step of the OAuth 2.1 authorization code flow, where a user can log in via federation to the IDP configured for the client. After successfully logging in, the user gets an authorization code via a redirect URI. From 79930b9e4b7f3a35224b0d6648bb9f6815dab435 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 13 Feb 2023 15:08:01 -0500 Subject: [PATCH 028/122] First pass at converting to new cache update matrix. --- .../src/hooks/ShopperContexts/config.ts | 44 +-- .../src/hooks/ShopperCustomers/config.ts | 286 ++---------------- .../hooks/ShopperDiscoverySearch/config.ts | 26 +- .../src/hooks/ShopperOrders/config.ts | 126 +------- 4 files changed, 51 insertions(+), 431 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts index 603c81b90d..0172fe1a7d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts @@ -1,43 +1,19 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, CacheUpdateMatrix} from '../types' import {NotImplementedError} from '../utils' -export const ShopperContextsMutations = { - /** - * WARNING: This method is not implemented. - * - * Creates the shopper's context based on shopperJWT. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=createShopperContext} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#createshoppercontext} for more information on the parameters and returned data type. - */ - CreateShopperContext: 'createShopperContext', - /** - * WARNING: This method is not implemented. - * - * Gets the shopper's context based on the shopperJWT. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=deleteShopperContext} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#deleteshoppercontext} for more information on the parameters and returned data type. - */ - DeleteShopperContext: 'deleteShopperContext', - /** - * WARNING: This method is not implemented. - * - * Updates the shopper's context based on the Shopper JWT. If the shopper context exists, it's updated with the patch body. If a customer qualifier or an `effectiveDateTime` is already present in the existing shopper context, its value is replaced by the corresponding value from the patch body. If a customer qualifers' value is set to `null` it's deleted from existing shopper context. If `effectiveDateTime` value is set to set to an empty string (\"\"), it's deleted from existing shopper context. If `effectiveDateTime` value is set to `null` it's ignored. If an `effectiveDateTime` or customer qualifiiers' value is new, it's added to the existing Shopper context. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=updateShopperContext} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#updateshoppercontext} for more information on the parameters and returned data type. - */ - UpdateShopperContext: 'updateShopperContext' -} as const +type Client = ApiClients['shopperContexts'] -/** - * WARNING: This method is not implemented. - * - * A hook for performing mutations with the Shopper Contexts API. - */ -export function useShopperContextsMutation() { - NotImplementedError() +const TODO = (method: string) => { + throw new NotImplementedError(method) +} +export const cacheUpdateMatrix: CacheUpdateMatrix = { + updateShopperContext: TODO('updateShopperContext'), + createShopperContext: TODO('createShopperContext'), + deleteShopperContext: TODO('deleteShopperContext') } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts index 49f161a5be..ebb97f4b9c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts @@ -1,225 +1,30 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, Argument, DataType} from '../types' -import {useMutation} from '../useMutation' -import {MutationFunction, useQueryClient, UseMutationResult} from '@tanstack/react-query' -import {updateCache, CacheUpdateMatrixElement, Client, NotImplementedError} from '../utils' +import {ApiClients, Argument, CacheUpdateMatrix, DataType} from '../types' -export const ShopperCustomersMutations = { - /** - * Registers a new customer. The mandatory data are the credentials, profile last name, and email. This requires a JSON Web Token (JWT) which needs to be obtained using the POST /customers/auth API with type \"guest\". - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registercustomer} for more information on the parameters and returned data type. - */ - RegisterCustomer: 'registerCustomer', - /** - * WARNING: This method is not implemented. - * - * **DEPRECATION NOTICE** - * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - * --- - * Log the user out. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=invalidateCustomerAuth} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#invalidatecustomerauth} for more information on the parameters and returned data type. - */ - InvalidateCustomerAuth: 'invalidateCustomerAuth', - /** - * WARNING: This method is not implemented. - * - * **DEPRECATION NOTICE** - * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - * --- - * Obtains a new JSON Web Token (JWT)for a guest or registered customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizecustomer} for more information on the parameters and returned data type. - */ - AuthorizeCustomer: 'authorizeCustomer', - /** - * WARNING: This method is not implemented. - * - * **DEPRECATION NOTICE** - * To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - * --- - * Obtain the JSON Web Token (JWT) for registered customers whose credentials are stored using a third party system. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeTrustedSystem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizetrustedsystem} for more information on the parameters and returned data type. - */ - AuthorizeTrustedSystem: 'authorizeTrustedSystem', - /** - * WARNING: This method is not implemented. - * - * Reset customer password, after obtaining a reset token. This is the second step in the reset customer password flow, where a customer password is reset by providing the new credentials along with a reset token. This call should be preceded by a call to the /create-reset-token endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=resetPassword} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#resetpassword} for more information on the parameters and returned data type. - */ - ResetPassword: 'resetPassword', - /** - * WARNING: This method is not implemented. - * - * Get reset password token. This is the first step in the reset customer password flow, where a password reset token is requested for future use to reset a customer password. This call should be followed by a call to the /reset endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getResetPasswordToken} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getresetpasswordtoken} for more information on the parameters and returned data type. - */ - GetResetPasswordToken: 'getResetPasswordToken', - /** - * WARNING: This method is not implemented. - * - * Registers a new external profile for a customer. This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerExternalProfile} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registerexternalprofile} for more information on the parameters and returned data type. - */ - RegisterExternalProfile: 'registerExternalProfile', - /** - * Updates a customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomer} for more information on the parameters and returned data type. - */ - UpdateCustomer: 'updateCustomer', - /** - * Creates a new address with the given name for the given customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerAddress} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomeraddress} for more information on the parameters and returned data type. - */ - CreateCustomerAddress: 'createCustomerAddress', - /** - * Deletes a customer's address by address name. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=removeCustomerAddress} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#removecustomeraddress} for more information on the parameters and returned data type. - */ - RemoveCustomerAddress: 'removeCustomerAddress', - /** - * Updates a customer's address by address name. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerAddress} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomeraddress} for more information on the parameters and returned data type. - */ - UpdateCustomerAddress: 'updateCustomerAddress', - /** - * WARNING: This method is not implemented. - * - * Updates the customer's password. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerPassword} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerpassword} for more information on the parameters and returned data type. - */ - UpdateCustomerPassword: 'updateCustomerPassword', - /** - * Adds a payment instrument to the customer information. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerPaymentInstrument} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerpaymentinstrument} for more information on the parameters and returned data type. - */ - CreateCustomerPaymentInstrument: 'createCustomerPaymentInstrument', - /** - * Deletes a customer's payment instrument. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerPaymentInstrument} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerpaymentinstrument} for more information on the parameters and returned data type. - */ - DeleteCustomerPaymentInstrument: 'deleteCustomerPaymentInstrument', - /** - * Creates a customer product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlist} for more information on the parameters and returned data type. - */ - CreateCustomerProductList: 'createCustomerProductList', - /** - * WARNING: This method is not implemented. - * - * Deletes a customer product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlist} for more information on the parameters and returned data type. - */ - DeleteCustomerProductList: 'deleteCustomerProductList', - /** - * WARNING: This method is not implemented. - * - * Changes a product list. Changeable properties are the name, description, and if the list is public. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlist} for more information on the parameters and returned data type. - */ - UpdateCustomerProductList: 'updateCustomerProductList', - /** - * Adds an item to the customer's product list. Considered values from the request body are: - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlistitem} for more information on the parameters and returned data type. - */ - CreateCustomerProductListItem: 'createCustomerProductListItem', - /** - * Removes an item from a customer product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlistitem} for more information on the parameters and returned data type. - */ - DeleteCustomerProductListItem: 'deleteCustomerProductListItem', - /** - * Updates an item of a customer's product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlistitem} for more information on the parameters and returned data type. - */ - UpdateCustomerProductListItem: 'updateCustomerProductListItem' -} as const +type Client = ApiClients['shopperCustomers'] +export const cacheUpdateMatrix: CacheUpdateMatrix = {} + +type CacheUpdateMatrixElement = any // Tempoary to quiet errors + +const noop = () => ({}) +// TODO: Convert old matrix to new format export const shopperCustomersCacheUpdateMatrix = { - authorizeCustomer: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - authorizeTrustedSystem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - deleteCustomerProductList: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - getResetPasswordToken: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - invalidateCustomerAuth: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - registerCustomer: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - registerExternalProfile: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - resetPassword: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - updateCustomerPassword: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - updateCustomerProductList: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, + authorizeCustomer: noop, + authorizeTrustedSystem: noop, + deleteCustomerProductList: noop, + getResetPasswordToken: noop, + invalidateCustomerAuth: noop, + registerCustomer: noop, + registerExternalProfile: noop, + resetPassword: noop, + updateCustomerPassword: noop, + updateCustomerProductList: noop, updateCustomer: ( params: Argument, response: DataType @@ -455,56 +260,3 @@ export const SHOPPER_CUSTOMERS_NOT_IMPLEMENTED = [ 'updateCustomerPassword', 'updateCustomerProductList' ] - -export type ShopperCustomersMutationType = - (typeof ShopperCustomersMutations)[keyof typeof ShopperCustomersMutations] - -type UseShopperCustomersMutationHeaders = NonNullable< - Argument ->['headers'] -type UseShopperCustomersMutationArg = { - headers?: UseShopperCustomersMutationHeaders - rawResponse?: boolean - action: ShopperCustomersMutationType -} - -type ShopperCustomersClient = ApiClients['shopperCustomers'] - -/** - * A hook for performing mutations with the Shopper Customers API. - */ -function useShopperCustomersMutation( - arg: UseShopperCustomersMutationArg -): UseMutationResult< - DataType | Response, - Error, - Argument -> { - const {headers, rawResponse, action} = arg - - if (SHOPPER_CUSTOMERS_NOT_IMPLEMENTED.includes(action)) { - NotImplementedError() - } - type Params = Argument - type Data = DataType - const queryClient = useQueryClient() - return useMutation( - (params, apiClients) => { - const method = apiClients['shopperCustomers'][action] as MutationFunction - return ( - method.call as ( - apiClient: ShopperCustomersClient, - params: Params, - rawResponse: boolean | undefined - ) => any - )(apiClients['shopperCustomers'], {...params, headers}, rawResponse) - }, - { - onSuccess: (data, params) => { - updateCache(queryClient, action, shopperCustomersCacheUpdateMatrix, data, params) - } - } - ) -} - -export {useShopperCustomersMutation} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts index d7313c0998..75c35a160c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts @@ -1,27 +1,17 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {ApiClients, CacheUpdateMatrix} from '../types' import {NotImplementedError} from '../utils' -export const ShopperDiscoverySearchMutations = { - /** - * WARNING: This method is not implemented. - * - * This method retrieves search results for a Channel. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-discovery-search?meta=retrieveResults} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperdiscoverysearch.shopperdiscoverysearch-1.html#retrieveresults} for more information on the parameters and returned data type. - */ - RetrieveResults: 'retrieveResults' -} as const +type Client = ApiClients['shopperDiscoverySearch'] -/** - * WARNING: This method is not implemented. - * - * A hook for performing mutations with the Shopper Discovery Search API. - */ -export function useShopperDiscoverySearchMutation() { - NotImplementedError() +const TODO = (method: string) => { + throw new NotImplementedError(method) +} +export const cacheUpdateMatrix: CacheUpdateMatrix = { + retrieveResults: TODO('retrieveResults') // This can probably be a no-op? } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts index ea52a81bd9..d2250e04a8 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts @@ -1,52 +1,22 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {DataType, Argument, ApiClients} from '../types' -import {useMutation} from '../useMutation' -import {MutationFunction, UseMutationResult, useQueryClient} from '@tanstack/react-query' -import {updateCache, CacheUpdateMatrixElement, Client, NotImplementedError} from '../utils' +import {DataType, ApiClients, CacheUpdateMatrix} from '../types' -export const ShopperOrdersMutations = { - /** - * Submits an order based on a prepared basket. The only considered value from the request body is basketId. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createorder} for more information on the parameters and returned data type. - */ - CreateOrder: 'createOrder', - /** - * WARNING: This method is not implemented. - * - * Adds a payment instrument to an order. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createPaymentInstrumentForOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createpaymentinstrumentfororder} for more information on the parameters and returned data type. - */ - CreatePaymentInstrumentForOrder: 'createPaymentInstrumentForOrder', - /** - * WARNING: This method is not implemented. - * - * Removes a payment instrument of an order. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=removePaymentInstrumentFromOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#removepaymentinstrumentfromorder} for more information on the parameters and returned data type. - */ - RemovePaymentInstrumentFromOrder: 'removePaymentInstrumentFromOrder', - /** - * WARNING: This method is not implemented. - * - * Updates a payment instrument of an order. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=updatePaymentInstrumentForOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#updatepaymentinstrumentfororder} for more information on the parameters and returned data type. - */ - UpdatePaymentInstrumentForOrder: 'updatePaymentInstrumentForOrder' -} as const +type Client = ApiClients['shopperOrders'] +export const cacheUpdateMatrix: CacheUpdateMatrix = {} + +type CacheUpdateMatrixElement = any // Temporary to quiet errors + +const noop = () => ({}) + +// TODO: Convert old matrix to new format export const shopperOrdersCacheUpdateMatrix = { - createOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { + createOrder: (response: DataType): CacheUpdateMatrixElement => { const customerId = response?.customerInfo?.customerId return { update: [ @@ -59,24 +29,9 @@ export const shopperOrdersCacheUpdateMatrix = { invalidate: [{name: 'customerBaskets', key: ['/customers', customerId, '/baskets']}] } }, - createPaymentInstrumentForOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - removePaymentInstrumentFromOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - }, - updatePaymentInstrumentForOrder: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - return {} - } + createPaymentInstrumentForOrder: noop, + removePaymentInstrumentFromOrder: noop, + updatePaymentInstrumentForOrder: noop } export const SHOPPER_ORDERS_NOT_IMPLEMENTED = [ @@ -84,56 +39,3 @@ export const SHOPPER_ORDERS_NOT_IMPLEMENTED = [ 'RemovePaymentInstrumentFromOrder', 'UpdatePaymentInstrumentForOrder' ] - -export type ShopperOrdersMutationType = - (typeof ShopperOrdersMutations)[keyof typeof ShopperOrdersMutations] - -type UseShopperOrdersMutationHeaders = NonNullable>['headers'] -type UseShopperOrdersMutationArg = { - headers?: UseShopperOrdersMutationHeaders - rawResponse?: boolean - action: ShopperOrdersMutationType -} - -type ShopperOrdersClient = ApiClients['shopperOrders'] - -/** - * A hook for performing mutations with the Shopper Orders API. - */ - -function useShopperOrdersMutation( - arg: UseShopperOrdersMutationArg -): UseMutationResult< - DataType | Response, - Error, - Argument -> { - const {headers, rawResponse, action} = arg - - if (SHOPPER_ORDERS_NOT_IMPLEMENTED.includes(action)) { - NotImplementedError() - } - type Params = Argument - type Data = DataType - const queryClient = useQueryClient() - - return useMutation( - (params, apiClients) => { - const method = apiClients['shopperOrders'][action] as MutationFunction - return ( - method.call as ( - apiClient: ShopperOrdersClient, - params: Params, - rawResponse: boolean | undefined - ) => any - )(apiClients['shopperOrders'], {...params, headers}, rawResponse) - }, - { - onSuccess: (data, params) => { - updateCache(queryClient, action, shopperOrdersCacheUpdateMatrix, data, params) - } - } - ) -} - -export {useShopperOrdersMutation} From 6a35ad1ddc7d2293a300a87fe75399d9948b3bd9 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 13 Feb 2023 15:26:43 -0500 Subject: [PATCH 029/122] Remove canceled Shopper Discovery Search API. --- .../hooks/ShopperDiscoverySearch/config.ts | 17 ------- .../src/hooks/ShopperDiscoverySearch/index.ts | 8 ---- .../hooks/ShopperDiscoverySearch/mutation.ts | 43 ----------------- .../src/hooks/ShopperDiscoverySearch/query.ts | 47 ------------------- .../commerce-sdk-react/src/hooks/index.ts | 1 - .../src/hooks/query.test.tsx | 8 ---- .../commerce-sdk-react/src/hooks/types.ts | 2 - .../src/hooks/useCommerceApi.test.tsx | 1 - packages/commerce-sdk-react/src/provider.tsx | 2 - 9 files changed, 129 deletions(-) delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/index.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts deleted file mode 100644 index 75c35a160c..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2023, 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 {ApiClients, CacheUpdateMatrix} from '../types' -import {NotImplementedError} from '../utils' - -type Client = ApiClients['shopperDiscoverySearch'] - -const TODO = (method: string) => { - throw new NotImplementedError(method) -} -export const cacheUpdateMatrix: CacheUpdateMatrix = { - retrieveResults: TODO('retrieveResults') // This can probably be a no-op? -} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/index.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/index.ts deleted file mode 100644 index 388e07d6ee..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright (c) 2022, 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 - */ -export * from './mutation' -export * from './query' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts deleted file mode 100644 index 09d80410ef..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/mutation.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023, 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types' -import {useMutation} from '../useMutation' -import {UseMutationResult} from '@tanstack/react-query' -import {NotImplementedError} from '../utils' -import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' - -type Client = ApiClients['shopperDiscoverySearch'] - -export const ShopperDiscoverySearchMutations = { - RetrieveResults: 'retrieveResults' -} as const - -export type ShopperDiscoverySearchMutation = - (typeof ShopperDiscoverySearchMutations)[keyof typeof ShopperDiscoverySearchMutations] - -export function useShopperDiscoverySearchMutation( - mutation: Mutation -): UseMutationResult, unknown, Argument> { - const getCacheUpdates = cacheUpdateMatrix[mutation] - // TODO: Remove this check when all mutations are implemented. - if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`) - - // The `Options` and `Data` types for each mutation are similar, but distinct, and the union - // type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle. - // I'm not sure if there's a way to avoid the type assertions in here for the methods that - // use them. However, I'm fairly confident that they are safe to do, as they seem to be simply - // re-asserting what we already have. - const {shopperDiscoverySearch: client} = useCommerceApi() - type Options = Argument - type Data = DataType - return useMutation({ - client, - method: (opts: Options) => (client[mutation] as ApiMethod)(opts), - getCacheUpdates: getCacheUpdates as CacheUpdateGetter, Data> - }) -} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts deleted file mode 100644 index 41fda8f8ec..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperDiscoverySearch/query.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023, 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' -import useCommerceApi from '../useCommerceApi' -import {useQuery} from '../useQuery' - -type Client = ApiClients['shopperDiscoverySearch'] - -/** - * A hook for `ShopperDiscoverySearch#getSuggestions`. - * This method gets suggestions for the user's search activity for a channel. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-discovery-search?meta=getSuggestions} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperdiscoverysearch.shopperdiscoverysearch-1.html#getsuggestions} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const useSuggestions = ( - apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} -): UseQueryResult> => { - const {shopperDiscoverySearch: client} = useCommerceApi() - const method = (arg: Argument) => client.getSuggestions(arg) - const requiredParameters = ['organizationId', 'channelId', 'suggestionTypes', 'locale'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/channels/', - parameters.channelId, - '/suggestions', - // Full parameters last for easy lookup - parameters - ] as const - - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) -} diff --git a/packages/commerce-sdk-react/src/hooks/index.ts b/packages/commerce-sdk-react/src/hooks/index.ts index 5b68b9b93b..c101788528 100644 --- a/packages/commerce-sdk-react/src/hooks/index.ts +++ b/packages/commerce-sdk-react/src/hooks/index.ts @@ -8,7 +8,6 @@ export * from './ShopperBaskets' export * from './ShopperContexts' export * from './ShopperCustomers' export * from './ShopperExperience' -export * from './ShopperDiscoverySearch' export * from './ShopperGiftCertificates' export * from './ShopperLogin' export * from './ShopperOrders' diff --git a/packages/commerce-sdk-react/src/hooks/query.test.tsx b/packages/commerce-sdk-react/src/hooks/query.test.tsx index c8a1518350..48d4fc121e 100644 --- a/packages/commerce-sdk-react/src/hooks/query.test.tsx +++ b/packages/commerce-sdk-react/src/hooks/query.test.tsx @@ -15,7 +15,6 @@ import { } from './ShopperBaskets/query' import {useProductSearch, useSearchSuggestions} from './ShopperSearch/query' import {useShopperContext} from './ShopperContexts/query' -import {useSuggestions} from './ShopperDiscoverySearch/query' import { useExternalProfile, useCustomer, @@ -204,13 +203,6 @@ const QUERY_TESTS = [ name: 'useSearchSuggestions', hook: () => useSearchSuggestions({q: 'test'}), endpoint: /\/search-suggestions$/ - }, - // ShopperDiscoverySearch - { - name: 'useSuggestions', - hook: () => useSuggestions(), - endpoint: new RegExp(''), - notImplemented: true } ] diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index c082c2b992..6cc428a971 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -9,7 +9,6 @@ import { ShopperBaskets, ShopperContexts, ShopperCustomers, - ShopperDiscoverySearch, ShopperExperience, ShopperGiftCertificates, ShopperLogin, @@ -40,7 +39,6 @@ export interface ApiClients { shopperBaskets: ShopperBaskets shopperContexts: ShopperContexts shopperCustomers: ShopperCustomers - shopperDiscoverySearch: ShopperDiscoverySearch shopperExperience: ShopperExperience shopperGiftCertificates: ShopperGiftCertificates shopperLogin: ShopperLogin diff --git a/packages/commerce-sdk-react/src/hooks/useCommerceApi.test.tsx b/packages/commerce-sdk-react/src/hooks/useCommerceApi.test.tsx index 9aee74f305..06a8f91a15 100644 --- a/packages/commerce-sdk-react/src/hooks/useCommerceApi.test.tsx +++ b/packages/commerce-sdk-react/src/hooks/useCommerceApi.test.tsx @@ -17,7 +17,6 @@ describe('useCommerceApi', () => { 'shopperBaskets', 'shopperContexts', 'shopperCustomers', - 'shopperDiscoverySearch', 'shopperGiftCertificates', 'shopperLogin', 'shopperOrders', diff --git a/packages/commerce-sdk-react/src/provider.tsx b/packages/commerce-sdk-react/src/provider.tsx index 495c04982e..427933949e 100644 --- a/packages/commerce-sdk-react/src/provider.tsx +++ b/packages/commerce-sdk-react/src/provider.tsx @@ -14,7 +14,6 @@ import { ShopperOrders, ShopperProducts, ShopperPromotions, - ShopperDiscoverySearch, ShopperGiftCertificates, ShopperSearch, ShopperBasketsTypes @@ -86,7 +85,6 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => { shopperBaskets: new ShopperBaskets(config), shopperContexts: new ShopperContexts(config), shopperCustomers: new ShopperCustomers(config), - shopperDiscoverySearch: new ShopperDiscoverySearch(config), shopperExperience: new ShopperExperience(config), shopperGiftCertificates: new ShopperGiftCertificates(config), shopperLogin: new ShopperLogin(config), From d27786dc7089cde80d4a347dbb1560147eb5458b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 14 Feb 2023 16:51:24 -0500 Subject: [PATCH 030/122] Update login helpers. --- packages/commerce-sdk-react/src/auth/index.ts | 14 ++++++ .../src/hooks/ShopperLogin/helper.ts | 44 +++++++++---------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/commerce-sdk-react/src/auth/index.ts b/packages/commerce-sdk-react/src/auth/index.ts index 42e0413010..58db8c2365 100644 --- a/packages/commerce-sdk-react/src/auth/index.ts +++ b/packages/commerce-sdk-react/src/auth/index.ts @@ -326,6 +326,20 @@ class Auth { ) } + /** + * Creates a function that only executes after a session is initialized. + * @param fn Function that needs to wait until the session is initialized. + * @returns Wrapped function + */ + whenReady( + fn: (...args: Args) => Promise + ): (...args: Args) => Promise { + return async (...args) => { + await this.ready() + return await fn(...args) + } + } + /** * A wrapper method for commerce-sdk-isomorphic helper: loginGuestUser. * diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts index bacbae89bd..3be40e2c47 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts @@ -1,12 +1,12 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {useMutation} from '../useMutation' +import {MutationFunction, useMutation, UseMutationResult} from '@tanstack/react-query' import useAuth from '../useAuth' -import {UseMutationResult} from '@tanstack/react-query' +import Auth from '../../auth' export const ShopperLoginHelpers = { LoginGuestUser: 'loginGuestUser', @@ -15,7 +15,7 @@ export const ShopperLoginHelpers = { Logout: 'logout' } as const -type ShopperLoginHelpersType = (typeof ShopperLoginHelpers)[keyof typeof ShopperLoginHelpers] +export type ShopperLoginHelper = (typeof ShopperLoginHelpers)[keyof typeof ShopperLoginHelpers] /** * A hook for Public Client Shopper Login OAuth helpers. @@ -28,27 +28,25 @@ type ShopperLoginHelpersType = (typeof ShopperLoginHelpers)[keyof typeof Shopper * - register * - logout */ -export function useShopperLoginHelper( - action: Action +export function useShopperLoginHelper( + mutation: Mutation ): UseMutationResult< - // TODO: what's the better way for declaring the types? - any, - Error, - any + // Extract the data from the returned promise (all mutations should be async) + ReturnType extends Promise ? Data : never, + unknown, + // Variables = void if no arguments, otherwise the first argument + [] extends Parameters ? void : Parameters[0], + unknown > { const auth = useAuth() - if (action === ShopperLoginHelpers.LoginGuestUser) { - return useMutation(() => auth.loginGuestUser()) - } - if (action === ShopperLoginHelpers.Logout) { - return useMutation(() => auth.logout()) - } - if (action === ShopperLoginHelpers.Register) { - return useMutation((body) => auth.register(body)) - } - if (action === ShopperLoginHelpers.LoginRegisteredUserB2C) { - return useMutation((credentials) => auth.loginRegisteredUserB2C(credentials)) - } + if (!auth[mutation]) throw new Error(`Unknown login helper mutation: ${mutation}`) - throw new Error('Unknown ShopperLogin helper.') + // I'm not sure if there's a way to avoid this type assertion, but, I'm fairly confident that + // it is safe to do, as it seems to be simply re-asserting what we already know. + type Method = Auth[Mutation] + type PromisedData = ReturnType + type Data = PromisedData extends Promise ? D : never + type Variables = [] extends Parameters ? void : Parameters[0] + const method = auth[mutation].bind(auth) as MutationFunction + return useMutation(auth.whenReady(method)) } From 601b9f3c0032a086dd32a51e9d5786769222a7a1 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 14 Feb 2023 17:03:16 -0500 Subject: [PATCH 031/122] Fix auth types. --- packages/commerce-sdk-react/src/auth/index.ts | 60 +++++++++---------- .../commerce-sdk-react/src/hooks/types.ts | 23 ++++++- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/packages/commerce-sdk-react/src/auth/index.ts b/packages/commerce-sdk-react/src/auth/index.ts index 58db8c2365..e19e5b6f58 100644 --- a/packages/commerce-sdk-react/src/auth/index.ts +++ b/packages/commerce-sdk-react/src/auth/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 @@ -12,11 +12,12 @@ import { ShopperCustomersTypes } from 'commerce-sdk-isomorphic' import jwtDecode from 'jwt-decode' -import {ApiClientConfigParams, Argument} from '../hooks/types' +import {ApiClientConfigParams, Prettify, RemoveStringIndex} from '../hooks/types' import {BaseStorage, LocalStorage, CookieStorage, MemoryStorage, StorageType} from './storage' import {CustomerType} from '../hooks/useCustomerType' import {onClient} from '../utils' +type TokenResponse = ShopperLoginTypes.TokenResponse type Helpers = typeof helpers interface AuthConfig extends ApiClientConfigParams { redirectURI: string @@ -29,24 +30,26 @@ interface JWTHeaders { iat: number } -// this type is slightly different from ShopperLoginTypes.TokenResponse, reasons: -// 1. TokenResponse is too generic (with & {[key:string]: any}), we need a more -// restrictive type to make sure type safe -// 2. The refresh tokens are stored separately for guest and registered user. Instead -// of refresh_token, we have refresh_token_guest and refresh_token_registered +/** + * The extended field is not from api response, we manually store the auth type, + * so we don't need to make another API call when we already have the data. + * Plus, the getCustomer endpoint only works for registered user, it returns a 404 for a guest user, + * and it's not easy to grab this info in user land, so we add it into the Auth object, and expose it via a hook + */ +type AuthData = Prettify< + RemoveStringIndex & { + customer_type: CustomerType + idp_access_token: string + site_id?: string // TODO - make required or remove + } +> + +/** A shopper could be guest or registered, so we store the refresh tokens individually. */ type AuthDataKeys = - | 'access_token' - | 'customer_id' - | 'enc_user_id' - | 'expires_in' - | 'id_token' - | 'idp_access_token' + | Exclude | 'refresh_token_guest' | 'refresh_token_registered' - | 'token_type' - | 'usid' - | 'site_id' - | 'customer_type' + type AuthDataMap = Record< AuthDataKeys, { @@ -56,16 +59,6 @@ type AuthDataMap = Record< } > -/** - * The extended field is not from api response, we manually store the auth type, - * so we don't need to make another API call when we already have the data. - * Plus, the getCustomer endpoint only works for registered user, it returns a 404 for a guest user, - * and it's not easy to grab this info in user land, so we add it into the Auth object, and expose it via a hook - */ -type AuthData = ShopperLoginTypes.TokenResponse & { - customer_type: CustomerType -} - /** * A map of the data that this auth module stores. This maps the name of the property to * the storage type and the key when stored in that storage. You can also pass in a "callback" @@ -140,7 +133,7 @@ class Auth { private client: ShopperLogin private shopperCustomersClient: ShopperCustomers private redirectURI: string - private pendingToken: Promise | undefined + private pendingToken: Promise | undefined private REFRESH_TOKEN_EXPIRATION_DAYS = 90 private stores: Record @@ -204,9 +197,10 @@ class Auth { } private clearStorage() { - Object.keys(DATA_MAP).forEach((keyName) => { - type Key = keyof AuthDataMap - const {key, storageType} = DATA_MAP[keyName as Key] + // Type assertion because Object.keys is silly and limited :( + const keys = Object.keys(DATA_MAP) as AuthDataKeys[] + keys.forEach((keyName) => { + const {key, storageType} = DATA_MAP[keyName] const store = this.stores[storageType] store.delete(key) }) @@ -244,7 +238,7 @@ class Auth { * This method stores the TokenResponse object retrived from SLAS, and * store the data in storage. */ - private handleTokenResponse(res: ShopperLoginTypes.TokenResponse, isGuest: boolean) { + private handleTokenResponse(res: TokenResponse, isGuest: boolean) { this.set('access_token', res.access_token) this.set('customer_id', res.customer_id) this.set('enc_user_id', res.enc_user_id) @@ -268,7 +262,7 @@ class Auth { * * @Internal */ - async queueRequest(fn: () => Promise, isGuest: boolean) { + async queueRequest(fn: () => Promise, isGuest: boolean) { const queue = this.pendingToken ?? Promise.resolve() this.pendingToken = queue.then(async () => { const token = await fn() diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 6cc428a971..0fd2e70f12 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -20,12 +20,29 @@ import { // --- GENERAL UTILITIES --- // -/** - * Marks the given keys as required. - */ +/** Makes a type easier to read. */ +export type Prettify = NonNullable> + +/** Marks the given keys as required. */ // The outer Pick<...> is used to prettify the result type type RequireKeys = Pick>, keyof T> +/** Removes keys whose value is `never`. */ +type RemoveNeverValues = { + [K in keyof T as T[K] extends never ? never : K]: T[K] +} + +/** Change string index type to `never`. */ +type StringIndexNever = { + [K in keyof T]: string extends K ? never : T[K] +} + +/** Removes a string index type */ +export type RemoveStringIndex = RemoveNeverValues> + +/** Get the known (non-index) keys of a type. */ +export type KnownKeys = keyof RemoveStringIndex + // --- API CLIENTS --- // export type ApiClientConfigParams = { From d0e58557bf8b0505d8943070cfc9a3f28837544d Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 14 Feb 2023 17:11:30 -0500 Subject: [PATCH 032/122] Hopefully avoid caching getGiftCertificate. --- .../hooks/ShopperGiftCertificates/query.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts index 6b0b60627a..d0adf62730 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts @@ -38,10 +38,21 @@ export const useGiftCertificate = ( parameters ] as const - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + return useQuery( + apiOptions, + { + // !!! This is a violation of our design goal of minimal logic in the endpoint hook + // implementations. This is because getGiftCertificate is actually a POST method, + // rather than GET, and its body contains a secret (gift certificate code). To avoid + // exposing that secret in the shared cache, we set cacheTime to 0 to avoid caching it. + cacheTime: 0, + ...queryOptions + }, + { + client, + method, + requiredParameters, + getQueryKey + } + ) } From 7dc671b3930c5342406c422bd27dd0e9c3b6afd3 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 10:04:05 -0500 Subject: [PATCH 033/122] Rename shopper login helper to auth helper. --- .../src/hooks/ShopperLogin/helper.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts index 3be40e2c47..041b7758fc 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts @@ -8,27 +8,27 @@ import {MutationFunction, useMutation, UseMutationResult} from '@tanstack/react- import useAuth from '../useAuth' import Auth from '../../auth' -export const ShopperLoginHelpers = { +export const AuthHelpers = { LoginGuestUser: 'loginGuestUser', LoginRegisteredUserB2C: 'loginRegisteredUserB2C', - Register: 'register', - Logout: 'logout' + Logout: 'logout', + Register: 'register' } as const -export type ShopperLoginHelper = (typeof ShopperLoginHelpers)[keyof typeof ShopperLoginHelpers] +export type AuthHelper = (typeof AuthHelpers)[keyof typeof AuthHelpers] /** - * A hook for Public Client Shopper Login OAuth helpers. + * A hook for Public Client OAuth helpers. * The hook calls the SLAS helpers imported from commerce-sdk-isomorphic. * For more, see https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/#public-client-shopper-login-helpers * * Avaliable helpers: * - loginRegisteredUserB2C * - loginGuestUser - * - register * - logout + * - register */ -export function useShopperLoginHelper( +export function useAuthHelper( mutation: Mutation ): UseMutationResult< // Extract the data from the returned promise (all mutations should be async) From b0f573524ff4df71dd2d485a396b270fe7a9f3f6 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 10:13:41 -0500 Subject: [PATCH 034/122] Move auth helper to hooks root. --- packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts | 1 - packages/commerce-sdk-react/src/hooks/index.ts | 1 + .../src/hooks/{ShopperLogin/helper.ts => useAuthHelper.ts} | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/commerce-sdk-react/src/hooks/{ShopperLogin/helper.ts => useAuthHelper.ts} (96%) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts index 6a11ab19a2..1a13c64feb 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.ts @@ -6,4 +6,3 @@ */ export * from './mutation' export * from './query' -export * from './helper' diff --git a/packages/commerce-sdk-react/src/hooks/index.ts b/packages/commerce-sdk-react/src/hooks/index.ts index c101788528..fe01a2162f 100644 --- a/packages/commerce-sdk-react/src/hooks/index.ts +++ b/packages/commerce-sdk-react/src/hooks/index.ts @@ -14,6 +14,7 @@ export * from './ShopperOrders' export * from './ShopperProducts' export * from './ShopperPromotions' export * from './ShopperSearch' +export * from './useAuthHelper' import useCommerceApi from './useCommerceApi' import useCustomerId from './useCustomerId' import useCustomerType from './useCustomerType' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts similarity index 96% rename from packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts rename to packages/commerce-sdk-react/src/hooks/useAuthHelper.ts index 041b7758fc..fe63e7e7f3 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/helper.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts @@ -5,8 +5,8 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {MutationFunction, useMutation, UseMutationResult} from '@tanstack/react-query' -import useAuth from '../useAuth' -import Auth from '../../auth' +import useAuth from './useAuth' +import Auth from '../auth' export const AuthHelpers = { LoginGuestUser: 'loginGuestUser', From e6c2ba7e3bd9ed209334941a56eebb03fa72da3d Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 10:14:09 -0500 Subject: [PATCH 035/122] Mark useAuthorizationHeader as internal. --- packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts index 61c00d2808..ec51077fd2 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts @@ -11,6 +11,7 @@ import useAuth from './useAuth' * Creates a method that waits for authentication to complete and automatically includes an * Authorization header when making requests. * @param method Bound API method + * @internal */ export const useAuthorizationHeader = ( method: ApiMethod From 89bd6aa75f75ae711d23789d7ac4caf4cf2c93e6 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 10:17:44 -0500 Subject: [PATCH 036/122] Rename useAuth to useAuthContext for clarity w/ useAuthHelper. --- .../src/hooks/{useAuth.ts => useAuthContext.ts} | 4 ++-- packages/commerce-sdk-react/src/hooks/useAuthHelper.ts | 4 ++-- .../commerce-sdk-react/src/hooks/useAuthorizationHeader.ts | 4 ++-- packages/commerce-sdk-react/src/hooks/useCustomerId.ts | 4 ++-- packages/commerce-sdk-react/src/hooks/useCustomerType.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename packages/commerce-sdk-react/src/hooks/{useAuth.ts => useAuthContext.ts} (89%) diff --git a/packages/commerce-sdk-react/src/hooks/useAuth.ts b/packages/commerce-sdk-react/src/hooks/useAuthContext.ts similarity index 89% rename from packages/commerce-sdk-react/src/hooks/useAuth.ts rename to packages/commerce-sdk-react/src/hooks/useAuthContext.ts index 21003711e5..b8aeec0ae0 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuth.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthContext.ts @@ -14,7 +14,7 @@ import {AuthContext} from '../provider' * * @internal */ -const useAuth = (): Auth => { +const useAuthContext = (): Auth => { const context = React.useContext(AuthContext) if (!context) { throw new Error('Missing CommerceApiProvider. You probably forget to render the provider.') @@ -22,4 +22,4 @@ const useAuth = (): Auth => { return context } -export default useAuth +export default useAuthContext diff --git a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts index fe63e7e7f3..b065fbacef 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {MutationFunction, useMutation, UseMutationResult} from '@tanstack/react-query' -import useAuth from './useAuth' +import useAuthContext from './useAuthContext' import Auth from '../auth' export const AuthHelpers = { @@ -38,7 +38,7 @@ export function useAuthHelper( [] extends Parameters ? void : Parameters[0], unknown > { - const auth = useAuth() + const auth = useAuthContext() if (!auth[mutation]) throw new Error(`Unknown login helper mutation: ${mutation}`) // I'm not sure if there's a way to avoid this type assertion, but, I'm fairly confident that diff --git a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts index ec51077fd2..1f16dcb0eb 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ApiOptions, ApiMethod} from './types' -import useAuth from './useAuth' +import useAuthContext from './useAuthContext' /** * Creates a method that waits for authentication to complete and automatically includes an @@ -16,7 +16,7 @@ import useAuth from './useAuth' export const useAuthorizationHeader = ( method: ApiMethod ): ApiMethod => { - const auth = useAuth() + const auth = useAuthContext() return async (options) => { const {access_token} = await auth.ready() return await method({ diff --git a/packages/commerce-sdk-react/src/hooks/useCustomerId.ts b/packages/commerce-sdk-react/src/hooks/useCustomerId.ts index f6ae70d156..c74476560e 100644 --- a/packages/commerce-sdk-react/src/hooks/useCustomerId.ts +++ b/packages/commerce-sdk-react/src/hooks/useCustomerId.ts @@ -4,7 +4,7 @@ * 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 useAuth from './useAuth' +import useAuthContext from './useAuthContext' import useLocalStorage from './useLocalStorage' const onClient = typeof window !== 'undefined' @@ -17,7 +17,7 @@ const useCustomerId = (): string | null => { if (onClient) { return useLocalStorage('customer_id') } - const auth = useAuth() + const auth = useAuthContext() return auth.get('customer_id') } diff --git a/packages/commerce-sdk-react/src/hooks/useCustomerType.ts b/packages/commerce-sdk-react/src/hooks/useCustomerType.ts index d65825dd99..5dc01683d0 100644 --- a/packages/commerce-sdk-react/src/hooks/useCustomerType.ts +++ b/packages/commerce-sdk-react/src/hooks/useCustomerType.ts @@ -4,7 +4,7 @@ * 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 useAuth from './useAuth' +import useAuthContext from './useAuthContext' import useLocalStorage from './useLocalStorage' const onClient = typeof window !== 'undefined' @@ -32,7 +32,7 @@ const useCustomerType = (): useCustomerType => { if (onClient) { customerType = useLocalStorage('customer_type') } else { - const auth = useAuth() + const auth = useAuthContext() customerType = auth.get('customer_type') } From 4cdf3c8fa4173217f447e410ca77935e8fd0e8df Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 10:20:41 -0500 Subject: [PATCH 037/122] Convert useAuthHelper to default export for consistency. --- packages/commerce-sdk-react/src/hooks/index.ts | 11 +++++------ .../commerce-sdk-react/src/hooks/useAuthHelper.ts | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/index.ts b/packages/commerce-sdk-react/src/hooks/index.ts index fe01a2162f..014aebf0df 100644 --- a/packages/commerce-sdk-react/src/hooks/index.ts +++ b/packages/commerce-sdk-react/src/hooks/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 @@ -14,8 +14,7 @@ export * from './ShopperOrders' export * from './ShopperProducts' export * from './ShopperPromotions' export * from './ShopperSearch' -export * from './useAuthHelper' -import useCommerceApi from './useCommerceApi' -import useCustomerId from './useCustomerId' -import useCustomerType from './useCustomerType' -export {useCommerceApi, useCustomerId, useCustomerType} +export {default as useAuthHelper} from './useAuthHelper' +export {default as useCommerceApi} from './useCommerceApi' +export {default as useCustomerId} from './useCustomerId' +export {default as useCustomerType} from './useCustomerType' diff --git a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts index b065fbacef..27b1d6a5d9 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts @@ -28,7 +28,7 @@ export type AuthHelper = (typeof AuthHelpers)[keyof typeof AuthHelpers] * - logout * - register */ -export function useAuthHelper( +export default function useAuthHelper( mutation: Mutation ): UseMutationResult< // Extract the data from the returned promise (all mutations should be async) From da93e18fcd21cbf7e21ada8e877fd665364f5b90 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 10:31:42 -0500 Subject: [PATCH 038/122] Remove siteId from AuthData, as it is passed via config. --- packages/commerce-sdk-react/src/auth/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/commerce-sdk-react/src/auth/index.ts b/packages/commerce-sdk-react/src/auth/index.ts index e19e5b6f58..90ed54c298 100644 --- a/packages/commerce-sdk-react/src/auth/index.ts +++ b/packages/commerce-sdk-react/src/auth/index.ts @@ -40,7 +40,6 @@ type AuthData = Prettify< RemoveStringIndex & { customer_type: CustomerType idp_access_token: string - site_id?: string // TODO - make required or remove } > @@ -111,10 +110,6 @@ const DATA_MAP: AuthDataMap = { store.delete('cc-nx-g') } }, - site_id: { - storageType: 'cookie', - key: 'cc-site-id' - }, customer_type: { storageType: 'local', key: 'customer_type' From 4e727a5c78ba508eb5dc90292fd74c72cc6f69d5 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 10:55:02 -0500 Subject: [PATCH 039/122] Update comment with generated code. --- .../src/hooks/ShopperGiftCertificates/query.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts index d0adf62730..68c614b451 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts @@ -41,10 +41,10 @@ export const useGiftCertificate = ( return useQuery( apiOptions, { - // !!! This is a violation of our design goal of minimal logic in the endpoint hook - // implementations. This is because getGiftCertificate is actually a POST method, - // rather than GET, and its body contains a secret (gift certificate code). To avoid - // exposing that secret in the shared cache, we set cacheTime to 0 to avoid caching it. + // !!! This is a violation of our design goal of minimal logic in the indivudal endpoint + // endpoint hooks. This is because this method is a POST method, rather than GET, + // and its body contains secrets. Setting cacheTime to 0 avoids exposing the secrets in + // the shared cache. cacheTime: 0, ...queryOptions }, From ed97973a32d80d4cb4be73d7b3a8bc14fdf024b8 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 13:58:24 -0500 Subject: [PATCH 040/122] Move headers to last parameter because we rarely use it. --- packages/commerce-sdk-react/src/hooks/types.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 0fd2e70f12..d9497bd936 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -23,7 +23,10 @@ import { /** Makes a type easier to read. */ export type Prettify = NonNullable> -/** Marks the given keys as required. */ +/** + * Marks the given keys as required. + * WARNING: Does not work if T has an index signature. + */ // The outer Pick<...> is used to prettify the result type type RequireKeys = Pick>, keyof T> @@ -74,8 +77,8 @@ export type ApiClient = ApiClients[keyof ApiClients] */ export type ApiOptions< Parameters extends object = Record, - Headers extends Record = Record, - Body extends object | unknown[] | undefined = Record | unknown[] | undefined + Body extends object | unknown[] | undefined = Record | unknown[] | undefined, + Headers extends Record = Record > = { parameters?: Parameters headers?: Headers @@ -109,11 +112,11 @@ export type DataType = T extends ApiMethod ? R : nev export type MergedOptions = RequireKeys< ApiOptions< NonNullable, - NonNullable, // `body` may not exist on `Options`, in which case it is `unknown` here. Due to the type // constraint in `ApiOptions`, that is not a valid value. We must replace it with `never` // to indicate that the result type does not have a `body`. - unknown extends Options['body'] ? never : Options['body'] + unknown extends Options['body'] ? never : Options['body'], + NonNullable >, 'parameters' | 'headers' > From dd6adce7a82d821ed2737370951c369b6ce8205d Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 15 Feb 2023 16:16:24 -0500 Subject: [PATCH 041/122] Update cache update matrices to new interface. --- .../src/hooks/ShopperBaskets/config.ts | 3 +- .../src/hooks/ShopperContexts/config.ts | 4 +- .../src/hooks/ShopperCustomers/config.ts | 413 +++++++++--------- .../src/hooks/ShopperOrders/config.ts | 72 +-- .../commerce-sdk-react/src/hooks/types.ts | 2 +- .../src/hooks/useMutation.ts | 2 +- .../commerce-sdk-react/src/hooks/utils.ts | 9 +- 7 files changed, 268 insertions(+), 237 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts index 9fb461d6f6..6e7c4493b8 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts @@ -28,8 +28,7 @@ const updateBasketQuery = ( if (!basketId) return {} const update: Array | CacheUpdateUpdate> = [ { - queryKey: ['/baskets', basketId, {basketId}], - updater: newBasket + queryKey: ['/baskets', basketId, {basketId}] } ] if (customerId) { diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts index 0172fe1a7d..fbbaaa25b1 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts @@ -9,8 +9,8 @@ import {NotImplementedError} from '../utils' type Client = ApiClients['shopperContexts'] -const TODO = (method: string) => { - throw new NotImplementedError(method) +const TODO = (method: keyof Client) => { + throw new NotImplementedError(`Cache logic for '${method}'`) } export const cacheUpdateMatrix: CacheUpdateMatrix = { updateShopperContext: TODO('updateShopperContext'), diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts index ebb97f4b9c..506fab438c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts @@ -4,259 +4,272 @@ * 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 {ApiClients, Argument, CacheUpdateMatrix, DataType} from '../types' +import {ShopperCustomersTypes} from 'commerce-sdk-isomorphic' +import { + ApiClients, + ApiOptions, + CacheUpdate, + CacheUpdateMatrix, + CacheUpdateUpdate, + MergedOptions +} from '../types' +import {and, matchesApiConfig, NotImplementedError, pathStartsWith} from '../utils' type Client = ApiClients['shopperCustomers'] +type Customer = ShopperCustomersTypes.Customer +type CustomerOptions = MergedOptions> -export const cacheUpdateMatrix: CacheUpdateMatrix = {} +const noop = () => ({}) +const TODO = (method: keyof Client) => { + throw new NotImplementedError(`Cache logic for '${method}'`) +} -type CacheUpdateMatrixElement = any // Tempoary to quiet errors +const baseQueryKey = (customerId: string, parameters: CustomerOptions['parameters']) => [ + '/organizations/', + parameters.organizationId, + '/customers/', + customerId +] -const noop = () => ({}) -// TODO: Convert old matrix to new format -export const shopperCustomersCacheUpdateMatrix = { - authorizeCustomer: noop, - authorizeTrustedSystem: noop, - deleteCustomerProductList: noop, - getResetPasswordToken: noop, - invalidateCustomerAuth: noop, - registerCustomer: noop, - registerExternalProfile: noop, - resetPassword: noop, - updateCustomerPassword: noop, - updateCustomerProductList: noop, - updateCustomer: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters +/** Invalidates the customer and all derivative endpoints */ +const invalidateCustomer = ( + customerId: string, + parameters: CustomerOptions['parameters'] +): CacheUpdate => { + return { + invalidate: [ + and(matchesApiConfig(parameters), pathStartsWith(baseQueryKey(customerId, parameters))) + ] + } +} + +// TODO: Rather than every /customers/{customerId} endpoint whenever we update +// a derivative property (e.g. address), can we instead update the corresponding +// cached query and insert the data into the cached customer query, but leave +// everything else alone? +export const cacheUpdateMatrix: CacheUpdateMatrix = { + authorizeCustomer: TODO('authorizeCustomer'), + authorizeTrustedSystem: TODO('authorizeTrustedSystem'), + createCustomerAddress(customerId, {parameters}, response) { + if (!customerId) return {} return { + ...invalidateCustomer(customerId, parameters), update: [ { - name: 'customer', - key: ['/customers', customerId, {customerId}], - updater: () => response + queryKey: [ + ...baseQueryKey(customerId, parameters), + '/addresses', + response.addressId, + // getCustomerAddress uses `addressName` rather than `addressId` + {...parameters, addressName: response.addressId} + ] } - ], - invalidate: [ - { - name: 'customerPaymentInstrument', - key: ['/customers', customerId, '/payment-instruments'] - }, - {name: 'customerAddress', key: ['/customers', customerId, '/addresses']}, - {name: 'externalProfile', key: ['/customers', '/external-profile']} ] } }, + createCustomerPaymentInstrument(customerId, {parameters}, response) { + if (!customerId) return {} - updateCustomerAddress: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId, addressName} = params.parameters return { + ...invalidateCustomer(customerId, parameters), update: [ { - name: 'customerAddress', - key: ['/customers', customerId, '/addresses', {addressName, customerId}], - updater: () => response + queryKey: [ + ...baseQueryKey(customerId, parameters), + '/payment-instruments/', + response.paymentInstrumentId, + {parameters, paymentInstrumentId: response.paymentInstrumentId} + ] } - ], - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] + ] } }, - - createCustomerAddress: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters - const {addressId} = params.body + createCustomerProductList(customerId, {parameters}, response) { + if (!customerId) return {} + const base = baseQueryKey(customerId, parameters) return { - update: [ - { - name: 'customerAddress', - key: [ - '/customers', - customerId, - '/addresses', - {addressName: addressId, customerId} - ], - updater: () => response - } - ], - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] + // We can only update cache if the response comes with an ID + update: !response.id + ? [] + : [ + { + queryKey: [ + ...base, + '/product-lists/', + response.id, + { + ...parameters, + listId: response.id + } + ] + } + ], + // We always invalidate, because even without an ID we assume that something has changed + invalidate: [ + // TODO: Convert to exact path match + and(matchesApiConfig(parameters), pathStartsWith([...base, '/product-lists'])) + ] } }, - - removeCustomerAddress: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion - // The required parameters become optional accidentally - // @ts-ignore - const {customerId, addressName} = params.parameters + createCustomerProductListItem(customerId, {parameters}, response) { + if (!customerId) return {} + const base = baseQueryKey(customerId, parameters) return { - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}], - remove: [ - { - name: 'customerAddress', - key: ['/customers', customerId, '/addresses', {addressName, customerId}] - } + // We can only update cache if the response comes with an ID + update: !response.id + ? [] + : [ + { + queryKey: [ + ...base, + '/product-lists/', + parameters.listId, + '/items/', + response.id, + { + ...parameters, + itemId: response.id + } + ] + } + ], + // We always invalidate, because even without an ID we assume that something has changed + invalidate: [ + and( + matchesApiConfig(parameters), + // TODO: Convert to exact path match + pathStartsWith([...base, '/product-lists/', parameters.listId]) + ) ] } }, - - createCustomerPaymentInstrument: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters + deleteCustomerPaymentInstrument(customerId, {parameters}) { + if (!customerId) return {} return { - update: [ - { - name: 'customerPaymentInstrument', - key: [ - '/customers', - customerId, + ...invalidateCustomer(customerId, parameters), + remove: [ + and( + matchesApiConfig(parameters), + pathStartsWith([ + ...baseQueryKey(customerId, parameters), '/payment-instruments', - {customerId, paymentInstrumentId: response?.paymentInstrumentId} - ], - updater: () => response - } - ], - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}] + parameters.paymentInstrumentId + ]) + ) + ] } }, - - deleteCustomerPaymentInstrument: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion - // The required parameters become optional accidentally - // @ts-ignore - const {customerId, paymentInstrumentId} = params.parameters + deleteCustomerProductList: TODO('deleteCustomerProductList'), + deleteCustomerProductListItem(customerId, {parameters}) { + if (!customerId) return {} + const base = baseQueryKey(customerId, parameters) return { - invalidate: [{name: 'customer', key: ['/customers', customerId, {customerId}]}], + invalidate: [ + and( + matchesApiConfig(parameters), + pathStartsWith([...base, '/product-lists/', parameters.listId]) + ) + ], remove: [ - { - name: 'customerPaymentInstrument', - key: [ - '/customers', - customerId, - '/payment-instruments', - {customerId, paymentInstrumentId} - ] - } + and( + matchesApiConfig(parameters), + pathStartsWith([ + ...base, + '/product-lists/', + parameters.listId, + '/items/', + parameters.itemId + ]) + ) ] } }, - - createCustomerProductList: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId} = params.parameters + getResetPasswordToken: noop, + invalidateCustomerAuth: TODO('invalidateCustomerAuth'), + registerCustomer: noop, + registerExternalProfile: TODO('registerExternalProfile'), + removeCustomerAddress(customerId, {parameters}) { + if (!customerId) return {} return { - update: [ - { - name: 'customerProductList', - key: [ - '/customers', - customerId, - '/product-list', - {customerId, listId: response?.id} - ], - updater: () => response - } + ...invalidateCustomer(customerId, parameters), + remove: [ + and( + matchesApiConfig(parameters), + pathStartsWith([ + ...baseQueryKey(customerId, parameters), + '/addresses', + parameters.addressName + ]) + ) ] } }, - - createCustomerProductListItem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId, listId} = params.parameters + resetPassword: TODO('resetPassword'), + updateCustomer(customerId, {parameters}) { + if (!customerId) return {} + const base = baseQueryKey(customerId, parameters) + const update: CacheUpdateUpdate[] = [ + { + queryKey: [...base, parameters] + } + ] + // TODO: Can we just use invalidateCustomer() here, since it invalidates all child paths? + const invalidate = [ + and(matchesApiConfig(parameters), pathStartsWith([...base, '/payment-instruments'])), + and(matchesApiConfig(parameters), pathStartsWith([...base, '/addresses'])), + and( + matchesApiConfig(parameters), + pathStartsWith([ + '/organizations/', + parameters.organizationId, + '/customers/external-profile' + ]) + ) + ] + return {update, invalidate} + }, + updateCustomerAddress(customerId, {parameters}) { + if (!customerId) return {} + // TODO: Can this `invalidate` instead be an update that targets the appropriate property? return { + ...invalidateCustomer(customerId, parameters), update: [ { - name: 'customerProductListItem', - key: [ - '/customers', - customerId, - '/product-list', - listId, - {itemId: response?.id} - ], - updater: () => response - } - ], - invalidate: [ - { - name: 'customerProductList', - key: ['/customers', customerId, '/product-list', {customerId, listId}] + queryKey: [ + ...baseQueryKey(customerId, parameters), + '/addresses', + parameters.addressName, + parameters + ] } ] } }, - - updateCustomerProductListItem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - const {customerId, listId, itemId} = params.parameters + updateCustomerPassword: TODO('updateCustomerPassword'), + updateCustomerProductList: TODO('updateCustomerProductList'), + updateCustomerProductListItem(customerId, {parameters}) { + if (!customerId) return {} + const base = baseQueryKey(customerId, parameters) return { update: [ { - name: 'customerProductListItem', - key: ['/customers', customerId, '/product-list', listId, {itemId}], - updater: () => response + queryKey: [ + ...base, + '/product-lists/', + parameters.listId, + '/items/', + parameters.itemId, + parameters + ] } ], invalidate: [ - { - name: 'customerProductList', - key: ['/customers', customerId, '/product-list', {customerId, listId}] - } - ] - } - }, - - deleteCustomerProductListItem: ( - params: Argument, - response: DataType - ): CacheUpdateMatrixElement => { - // TODO: Fix the RequireParametersUnlessAllAreOptional commerce-sdk-isomorphic type assertion - // The required parameters become optional accidentally - // @ts-ignore - const {customerId, listId, itemId} = params.parameters - return { - invalidate: [ - { - name: 'customerProductList', - key: ['/customers', customerId, '/product-list', {customerId, listId}] - } - ], - remove: [ - { - name: 'customerProductListItem', - key: ['/customers', customerId, '/product-list', listId, {itemId}] - } + and( + matchesApiConfig(parameters), + // TODO: Convert to exact path match + pathStartsWith([...base, '/product-lists/', parameters.listId]) + ) ] } } } - -export const SHOPPER_CUSTOMERS_NOT_IMPLEMENTED = [ - 'authorizeCustomer', - 'authorizeTrustedSystem', - 'deleteCustomerProductList', - 'invalidateCustomerAuth', - 'registerExternalProfile', - 'resetPassword', - 'updateCustomerPassword', - 'updateCustomerProductList' -] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts index d2250e04a8..835ea3b39b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts @@ -4,38 +4,56 @@ * 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 {DataType, ApiClients, CacheUpdateMatrix} from '../types' +import {ApiClients, CacheUpdateMatrix, CacheUpdateUpdate, CacheUpdateInvalidate} from '../types' +import {and, matchesApiConfig, NotImplementedError, pathStartsWith} from '../utils' +import type {ShopperOrdersTypes} from 'commerce-sdk-isomorphic' type Client = ApiClients['shopperOrders'] +type Order = ShopperOrdersTypes.Order -export const cacheUpdateMatrix: CacheUpdateMatrix = {} - -type CacheUpdateMatrixElement = any // Temporary to quiet errors +const TODO = (method: keyof Client) => { + throw new NotImplementedError(`Cache logic for '${method}'`) +} -const noop = () => ({}) +export const cacheUpdateMatrix: CacheUpdateMatrix = { + createOrder(customerId, {parameters}, response) { + const update: CacheUpdateUpdate[] = [] + if (response.orderNo) { + const updateOrder: CacheUpdateUpdate = { + queryKey: [ + '/organizations/', + parameters.organizationId, + '/orders/', + response.orderNo, + // TODO: These parameters are not guaranteed to match those sent to getOrder, + // how do we handle that? + {...parameters, orderNo: response.orderNo} + ] + } + // TODO: This type assertion is so that we "forget" what type the updater uses. + // Is there a way to avoid the assertion? + update.push(updateOrder as CacheUpdateUpdate) + } -// TODO: Convert old matrix to new format -export const shopperOrdersCacheUpdateMatrix = { - createOrder: (response: DataType): CacheUpdateMatrixElement => { - const customerId = response?.customerInfo?.customerId - return { - update: [ - { - name: 'order', - key: ['/orders', {orderNo: response.orderNo}], - updater: () => response - } - ], - invalidate: [{name: 'customerBaskets', key: ['/customers', customerId, '/baskets']}] + const invalidate: CacheUpdateInvalidate[] = [] + if (customerId) { + invalidate.push( + and( + matchesApiConfig(parameters), + pathStartsWith([ + '/organization/', + parameters.organizationId, + '/customers/', + customerId, + '/baskets' + ]) + ) + ) } + + return {update, invalidate} }, - createPaymentInstrumentForOrder: noop, - removePaymentInstrumentFromOrder: noop, - updatePaymentInstrumentForOrder: noop + createPaymentInstrumentForOrder: TODO('createPaymentInstrumentForOrder'), + removePaymentInstrumentFromOrder: TODO('removePaymentInstrumentFromOrder'), + updatePaymentInstrumentForOrder: TODO('updatePaymentInstrumentForOrder') } - -export const SHOPPER_ORDERS_NOT_IMPLEMENTED = [ - 'CreatePaymentInstrumentForOrder', - 'RemovePaymentInstrumentFromOrder', - 'UpdatePaymentInstrumentForOrder' -] diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index d9497bd936..c653437cd7 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -137,7 +137,7 @@ export type ApiQueryKey = */ export type CacheUpdateUpdate = { queryKey: ApiQueryKey - updater: Updater + updater?: Updater } /** Query predicate for queries to invalidate */ diff --git a/packages/commerce-sdk-react/src/hooks/useMutation.ts b/packages/commerce-sdk-react/src/hooks/useMutation.ts index 41f006f35e..df1eee6784 100644 --- a/packages/commerce-sdk-react/src/hooks/useMutation.ts +++ b/packages/commerce-sdk-react/src/hooks/useMutation.ts @@ -34,7 +34,7 @@ export const useMutation = < // so we also need to do that to get the "net" options that are actually sent to SCAPI. const netOptions = mergeOptions(hookConfig.client, options) const cacheUpdates = hookConfig.getCacheUpdates(customerId, netOptions, data) - updateCache(queryClient, cacheUpdates) + updateCache(queryClient, cacheUpdates, data) } }) } diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 97f50698b8..1615bd0019 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -8,12 +8,13 @@ import {Query, QueryClient} from '@tanstack/react-query' import {ApiClient, ApiOptions, CacheUpdate, MergedOptions} from './types' /** Applies the set of cache updates to the query client. */ -export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate) => { - cacheUpdates.update?.forEach(({queryKey, updater}) => - queryClient.setQueryData(queryKey, updater) - ) +export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate, data: unknown) => { cacheUpdates.invalidate?.forEach((predicate) => queryClient.invalidateQueries({predicate})) cacheUpdates.remove?.forEach((predicate) => queryClient.removeQueries({predicate})) + cacheUpdates.update?.forEach(({queryKey, updater}) => + // If an updater isn't given, fall back to just setting the data + queryClient.setQueryData(queryKey, updater ?? data) + ) } /** Error thrown when a method is not implemented. */ From 87b43a96b7e4ffa3f57f307c4bfebbf4f1e7f114 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Thu, 16 Feb 2023 09:23:57 -0800 Subject: [PATCH 042/122] Refactore test project after api changes. --- .../commerce-sdk-react/src/hooks/index.ts | 4 ++- .../src/hooks/useAuthHelper.ts | 4 ++- packages/commerce-sdk-react/tsconfig.json | 6 +++- .../app/pages/query-errors.tsx | 4 +-- .../app/pages/use-payment-methods.tsx | 2 +- .../app/pages/use-product-search.tsx | 6 ++-- .../app/pages/use-promotions-for-campaign.tsx | 4 ++- .../app/pages/use-promotions.tsx | 6 +++- .../app/pages/use-search-suggestions.tsx | 2 +- .../app/pages/use-shopper-baskets.tsx | 6 ++-- .../app/pages/use-shopper-categories.tsx | 6 ++-- .../app/pages/use-shopper-category.tsx | 4 ++- .../app/pages/use-shopper-customer.tsx | 33 ++++++++++++------- .../app/pages/use-shopper-experience.tsx | 22 +++++++++---- .../app/pages/use-shopper-get-order.tsx | 2 +- .../app/pages/use-shopper-login-helper.tsx | 8 ++--- .../app/pages/use-shopper-orders.tsx | 17 +++++----- .../app/pages/use-shopper-product.tsx | 2 +- .../app/pages/use-shopper-products.tsx | 4 ++- 19 files changed, 92 insertions(+), 50 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/index.ts b/packages/commerce-sdk-react/src/hooks/index.ts index 014aebf0df..2c6d3af25c 100644 --- a/packages/commerce-sdk-react/src/hooks/index.ts +++ b/packages/commerce-sdk-react/src/hooks/index.ts @@ -14,7 +14,9 @@ export * from './ShopperOrders' export * from './ShopperProducts' export * from './ShopperPromotions' export * from './ShopperSearch' -export {default as useAuthHelper} from './useAuthHelper' +// export {default as useAuthHelper} from './useAuthHelper' +export * from './useAuthHelper' + export {default as useCommerceApi} from './useCommerceApi' export {default as useCustomerId} from './useCustomerId' export {default as useCustomerType} from './useCustomerType' diff --git a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts index 27b1d6a5d9..d5c49fa6a0 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts @@ -28,7 +28,7 @@ export type AuthHelper = (typeof AuthHelpers)[keyof typeof AuthHelpers] * - logout * - register */ -export default function useAuthHelper( +export function useAuthHelper( mutation: Mutation ): UseMutationResult< // Extract the data from the returned promise (all mutations should be async) @@ -50,3 +50,5 @@ export default function useAuthHelper( const method = auth[mutation].bind(auth) as MutationFunction return useMutation(auth.whenReady(method)) } + +export default useAuthHelper \ No newline at end of file diff --git a/packages/commerce-sdk-react/tsconfig.json b/packages/commerce-sdk-react/tsconfig.json index 767fe15366..7ac1dc8571 100644 --- a/packages/commerce-sdk-react/tsconfig.json +++ b/packages/commerce-sdk-react/tsconfig.json @@ -98,5 +98,9 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "exclude": [ + "**/*.test.ts", + "**/*.test.tsx" + ] } diff --git a/packages/test-commerce-sdk-react/app/pages/query-errors.tsx b/packages/test-commerce-sdk-react/app/pages/query-errors.tsx index 2179d882a8..730347a834 100644 --- a/packages/test-commerce-sdk-react/app/pages/query-errors.tsx +++ b/packages/test-commerce-sdk-react/app/pages/query-errors.tsx @@ -11,8 +11,8 @@ import Json from '../components/Json' const QueryErrors = () => { // @ts-ignore - const products = useProducts({FOO: ''}) - const product = useProduct({id: '25502228Mxxx'}) + const products = useProducts({parameters: {FOO: ''}}) + const product = useProduct({parameters: {id: '25502228Mxxx'}}) return ( <> diff --git a/packages/test-commerce-sdk-react/app/pages/use-payment-methods.tsx b/packages/test-commerce-sdk-react/app/pages/use-payment-methods.tsx index 13bd5089b6..a6a811a377 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-payment-methods.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-payment-methods.tsx @@ -11,7 +11,7 @@ import {useParams} from 'react-router-dom' function UsePaymentMethods() { const {orderNo}: {orderNo: string} = useParams() - const {data, isLoading, error} = usePaymentMethodsForOrder({orderNo: orderNo}) + const {data, isLoading, error} = usePaymentMethodsForOrder({parameters: {orderNo: orderNo}}) if (isLoading) { return (
diff --git a/packages/test-commerce-sdk-react/app/pages/use-product-search.tsx b/packages/test-commerce-sdk-react/app/pages/use-product-search.tsx index 6ffd08ef1b..b729b972c6 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-product-search.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-product-search.tsx @@ -18,8 +18,10 @@ function UseProductSearch() { error, data: result } = useProductSearch({ - q: searchQuery, - refine: refinement + parameters: { + q: searchQuery, + refine: refinement + } }) if (isLoading) { return ( diff --git a/packages/test-commerce-sdk-react/app/pages/use-promotions-for-campaign.tsx b/packages/test-commerce-sdk-react/app/pages/use-promotions-for-campaign.tsx index 32b9aa117b..0f2ce5c4e0 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-promotions-for-campaign.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-promotions-for-campaign.tsx @@ -18,7 +18,9 @@ const UsePromotionsForCampaign = () => { isLoading, error } = usePromotionsForCampaign({ - campaignId + parameters: { + campaignId + } }) if (isLoading) { return ( diff --git a/packages/test-commerce-sdk-react/app/pages/use-promotions.tsx b/packages/test-commerce-sdk-react/app/pages/use-promotions.tsx index 0d63ec3de7..351e8bc987 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-promotions.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-promotions.tsx @@ -10,7 +10,11 @@ import {usePromotions} from 'commerce-sdk-react-preview' import Json from '../components/Json' function UsePromotions() { - const {data: result, isLoading, error} = usePromotions({ids: '10offsuits,50%offorder'}) + const { + data: result, + isLoading, + error + } = usePromotions({parameters: {ids: '10offsuits,50%offorder'}}) if (isLoading) { return (
diff --git a/packages/test-commerce-sdk-react/app/pages/use-search-suggestions.tsx b/packages/test-commerce-sdk-react/app/pages/use-search-suggestions.tsx index b54b169ff0..064ed99b9a 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-search-suggestions.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-search-suggestions.tsx @@ -11,7 +11,7 @@ import Json from '../components/Json' const searchQuery = 'shirt' function UseSearchSuggestions() { - const {isLoading, error, data: result} = useSearchSuggestions({q: searchQuery}) + const {isLoading, error, data: result} = useSearchSuggestions({parameters: {q: searchQuery}}) if (isLoading) { return (
diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-baskets.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-baskets.tsx index 559a98ff79..bb711df3ee 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-baskets.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-baskets.tsx @@ -15,9 +15,9 @@ import {UsePaymentMethodsForBasket} from '../components/use-shopper-baskets/use- function UseShopperBaskets() { const customerId = useCustomerId() || '' - const baskets = useCustomerBaskets({customerId}) - const createBasket = useShopperBasketsMutation({action: 'createBasket'}) - const updateBasket = useShopperBasketsMutation({action: 'updateBasket'}) + const baskets = useCustomerBaskets({parameters: {customerId}}) + const createBasket = useShopperBasketsMutation('createBasket') + const updateBasket = useShopperBasketsMutation('updateBasket') const hasBasket = baskets.data?.total !== 0 const basketId = baskets.data?.baskets ? baskets.data?.baskets[0].basketId : '' diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-categories.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-categories.tsx index 1838cfd682..f4ee9762dc 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-categories.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-categories.tsx @@ -18,8 +18,10 @@ function UseShopperCategories() { error, data: result } = useCategories({ - ids: 'root', - levels: 2 + parameters: { + ids: 'root', + levels: 2 + } }) if (isLoading) { return ( diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-category.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-category.tsx index e18a664754..bc86c965e2 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-category.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-category.tsx @@ -14,7 +14,9 @@ import {flatten} from '../utils/utils' function UseShopperCategory() { const {categoryId}: {categoryId: string} = useParams() const {isLoading, error, data} = useCategory({ - id: categoryId + parameters: { + id: categoryId + } }) if (isLoading) { return ( diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx index 83d727f3a5..24562c5c28 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx @@ -6,14 +6,15 @@ */ import React from 'react' import { - ShopperLoginHelpers, + AuthHelpers, useCustomer, useCustomerAddress, useCustomerBaskets, useCustomerOrders, useCustomerProductList, useShopperCustomersMutation, - useShopperLoginHelper + useAuthHelper, + ShopperCustomersMutation } from 'commerce-sdk-react-preview' import Json from '../components/Json' import {useQueryClient} from '@tanstack/react-query' @@ -78,11 +79,11 @@ const renderMutationHook = ({name, hook, body, parameters}: any) => { function UseCustomer() { const queryClient = useQueryClient() - const loginRegisteredUser = useShopperLoginHelper(ShopperLoginHelpers.LoginRegisteredUserB2C) + const loginRegisteredUser = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C) // TODO: Implement the flow - Login as a guest user and then registered that user. // Currently Login as a guest doesn't work in packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx - // const loginGuestUser = useShopperLoginHelper('loginGuestUser') + // const loginGuestUser = useAuthHelper(AuthHelpers.LoginGuestUser) // const guestUserMutationHooks = [ // { // action: 'registerCustomer', @@ -180,7 +181,7 @@ function UseCustomer() { ].map(({action, body, parameters}) => { return { name: action, - hook: useShopperCustomersMutation({action}), + hook: useShopperCustomersMutation(action as ShopperCustomersMutation), body, parameters } @@ -189,26 +190,36 @@ function UseCustomer() { const queryHooks = [ { name: 'useCustomer', - hook: useCustomer({customerId: CUSTOMER_ID}) + hook: useCustomer({ + parameters: {customerId: CUSTOMER_ID} + }) }, { name: 'useCustomerAddress', hook: useCustomerAddress({ - customerId: CUSTOMER_ID, - addressName: ADDRESS_NAME + parameters: { + customerId: CUSTOMER_ID, + addressName: ADDRESS_NAME + } }) }, { name: 'useCustomerOrders', - hook: useCustomerOrders({customerId: CUSTOMER_ID}) + hook: useCustomerOrders({ + parameters: {customerId: CUSTOMER_ID} + }) }, { name: 'useCustomerBaskets', - hook: useCustomerBaskets({customerId: CUSTOMER_ID}) + hook: useCustomerBaskets({ + parameters: {customerId: CUSTOMER_ID} + }) }, { name: 'useCustomerProductList', - hook: useCustomerProductList({customerId: CUSTOMER_ID, listId: LIST_ID}) + hook: useCustomerProductList({ + parameters: {customerId: CUSTOMER_ID, listId: LIST_ID} + }) } ] diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-experience.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-experience.tsx index bcb43f4587..5d95317ea3 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-experience.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-experience.tsx @@ -80,14 +80,20 @@ const UseShopperExperience = () => { const queryHooks = [ { name: 'usePage', - arg: {pageId: PAGE_ID}, - hook: usePage({pageId: PAGE_ID}) + arg: { + parameters: {pageId: PAGE_ID} + }, + get hook() { + return usePage(this.arg) + } }, { name: 'usePages', arg: { - aspectTypeId: ASPECT_TYPE_ID_PDP, - productId: PRODUCT_ID + parameters: { + aspectTypeId: ASPECT_TYPE_ID_PDP, + productId: PRODUCT_ID + } }, get hook() { return usePages(this.arg) @@ -96,9 +102,11 @@ const UseShopperExperience = () => { { name: 'usePages', arg: { - aspectTypeId: ASPECT_TYPE_ID_PLP, - categoryId: CATEGORY_ID, - aspectAttributes: ASPECT_ATTRIBUTES + parameters: { + aspectTypeId: ASPECT_TYPE_ID_PLP, + categoryId: CATEGORY_ID, + aspectAttributes: ASPECT_ATTRIBUTES + } }, get hook() { return usePages(this.arg) diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-get-order.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-get-order.tsx index 372ed6807f..e46c8242d4 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-get-order.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-get-order.tsx @@ -12,7 +12,7 @@ import Json from '../components/Json' function UseShopperGetOrder() { const {orderNo}: {orderNo: string} = useParams() - const {data, isLoading, error} = useOrder({orderNo}) + const {data, isLoading, error} = useOrder({parameters: {orderNo}}) if (isLoading) { return (
diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx index 3d7e3ce08f..e2fa1f429a 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx @@ -5,14 +5,14 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React from 'react' -import {ShopperLoginHelpers, useShopperLoginHelper} from 'commerce-sdk-react-preview' +import {AuthHelpers, useAuthHelper} from 'commerce-sdk-react-preview' import Json from '../components/Json' const UseShopperLoginHelper = () => { // use string or enum - const loginGuestUser = useShopperLoginHelper('loginGuestUser') - const loginRegisteredUser = useShopperLoginHelper(ShopperLoginHelpers.LoginRegisteredUserB2C) - const logout = useShopperLoginHelper(ShopperLoginHelpers.Logout) + const loginGuestUser = useAuthHelper(AuthHelpers.LoginGuestUser) + const loginRegisteredUser = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C) + const logout = useAuthHelper(AuthHelpers.Logout) //logout before logging guest user in const loginGuestUserFlow = async () => { diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-orders.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-orders.tsx index 79df252c12..a83738797e 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-orders.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-orders.tsx @@ -9,9 +9,10 @@ import React from 'react' import {Link} from 'react-router-dom' import Json from '../components/Json' import { + AuthHelpers, useShopperOrdersMutation, - useShopperLoginHelper, - ShopperLoginHelpers + useAuthHelper, + ShopperOrdersMutation } from 'commerce-sdk-react-preview' const orderNos = ['00014202', '00014103'] @@ -41,7 +42,7 @@ const renderMutationHooks = ({name, hook, body, parameters}: any) => { } function UseShopperOrders() { - const loginRegisteredUser = useShopperLoginHelper(ShopperLoginHelpers.LoginRegisteredUserB2C) + const loginRegisteredUser = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C) React.useEffect(() => { loginRegisteredUser.mutate({username: 'alex@test.com', password: 'Test1234#'}) }, []) @@ -54,11 +55,11 @@ function UseShopperOrders() { ].map(({action, body, parameters}) => { return { name: action, - hook: useShopperOrdersMutation({ - action, - headers: {'test-header': 'value'}, - rawResponse: false - }), + // hook: useShopperOrdersMutation({ + // action, + // headers: {'test-header': 'value'} + // }), + hook: useShopperOrdersMutation(action as ShopperOrdersMutation), body, parameters } diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-product.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-product.tsx index 8ea00fe549..6866c0a5c0 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-product.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-product.tsx @@ -11,7 +11,7 @@ import {Link, useParams} from 'react-router-dom' function UseShopperProduct() { const {productId}: {productId: string} = useParams() - const {data, isLoading, error} = useProduct({id: productId}) + const {data, isLoading, error} = useProduct({parameters: {id: productId}}) if (isLoading) { return (
diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-products.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-products.tsx index f39da63251..11f04552e6 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-products.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-products.tsx @@ -16,7 +16,9 @@ const UseShopperProducts = () => { error, data: result } = useProducts({ - ids + parameters: { + ids + } }) if (isLoading) { return ( From a5f56f7d3b5d165440db0f20ec24dad5c0efe6fc Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 13:18:15 -0500 Subject: [PATCH 043/122] Clean up cache update logic. Hopefully makes it much easier to maintain. --- .../src/hooks/ShopperBaskets/config.ts | 116 +++++----- .../src/hooks/ShopperCustomers/config.ts | 213 ++++++------------ .../src/hooks/ShopperOrders/config.ts | 61 +++-- .../commerce-sdk-react/src/hooks/utils.ts | 8 + 4 files changed, 170 insertions(+), 228 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts index 6e7c4493b8..df90e85cfe 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts @@ -13,88 +13,100 @@ import { CacheUpdateUpdate, MergedOptions } from '../types' -import {and, matchesApiConfig, pathStartsWith} from '../utils' +import {and, matchesApiConfig, matchesPath} from '../utils' type Client = ApiClients['shopperBaskets'] type Basket = ShopperBasketsTypes.Basket +type BasketOptions = MergedOptions> +type BasketParameters = BasketOptions['parameters'] type CustomerBasketsResult = ShopperCustomersTypes.BasketsResult -type BasketOptions = MergedOptions> + +// Path helpers to avoid typos! +const getBasePath = (parameters: BasketParameters) => ['/organizations/', parameters.organizationId] +const getBasketPath = (parameters: BasketParameters & {basketId: string}) => [ + ...getBasePath(parameters), + '/baskets/', + parameters.basketId +] + +const getCustomerBasketsPath = (customerId: string, parameters: BasketParameters) => [ + ...getBasePath(parameters), + '/customers/', + customerId, + '/baskets' // No trailing / as it's an aggregate endpoint +] const updateBasketQuery = ( customerId: string | null, - basketId: string | undefined, + parameters: BasketOptions['parameters'], newBasket: Basket -): Pick => { - if (!basketId) return {} - const update: Array | CacheUpdateUpdate> = [ - { - queryKey: ['/baskets', basketId, {basketId}] - } - ] - if (customerId) { - const updateCustomerBaskets: CacheUpdateUpdate = { - // Since we use baskets from customer basket query, we need to update it for any basket mutation - queryKey: ['/customers', customerId, '/baskets', {customerId}], - updater: (oldData) => { - // do not update if response basket is not part of existing customer baskets - if (!oldData?.baskets?.some((basket) => basket.basketId === basketId)) { - return undefined - } - const updatedBaskets = oldData.baskets.map((basket) => { - return basket.basketId === basketId ? newBasket : basket - }) - return { - ...oldData, - // TODO: Remove type assertion when RAML specs match - baskets: updatedBaskets as CustomerBasketsResult['baskets'] - } - } - } - update.push(updateCustomerBaskets) +): CacheUpdate => { + // If we just check `!parameters.basketId`, then TypeScript doesn't infer that the value is + // not `undefined`. We have to use this slightly convoluted predicate to give it that info. + const hasBasketId = (p: {basketId?: string}): p is {basketId: string} => Boolean(p.basketId) + if (!hasBasketId(parameters)) return {} + const updateBasket: CacheUpdateUpdate = { + // TODO: This method is used by multiple endpoints, so `parameters` could have properties + // that don't match getBasket + queryKey: [...getBasketPath(parameters), parameters] } - // TODO: This type assertion is so that we "forget" what type the updater uses. - // Is there a way to avoid the assertion? - return {update} as CacheUpdate -} -const removeBasketQuery = ( - options: BasketOptions, - basketId?: string -): Pick => { - if (!basketId) return {} - return { - remove: [and(matchesApiConfig(options), pathStartsWith(['/baskets', basketId]))] + if (!customerId) return {update: [updateBasket]} + const updateCustomerBaskets: CacheUpdateUpdate = { + // TODO: This method is used by multiple endpoints, so `parameters` could have properties + // that don't match getCustomerBaskets + queryKey: [...getCustomerBasketsPath(customerId, parameters), parameters], + updater: (oldData?: CustomerBasketsResult): CustomerBasketsResult | undefined => { + // do not update if response basket is not part of existing customer baskets + if (!oldData?.baskets?.some((basket) => basket.basketId === parameters.basketId)) { + return undefined + } + const updatedBaskets = oldData.baskets.map((basket) => { + return basket.basketId === parameters.basketId ? newBasket : basket + }) + return { + ...oldData, + // Shopper Customers and Shopper Baskets have different definitions for the `Basket` + // type. (99% similar, but that's not good enough for TypeScript.) + // TODO: Remove this type assertion when the RAML specs match. + baskets: updatedBaskets as CustomerBasketsResult['baskets'] + } + } } + return {update: [updateBasket, updateCustomerBaskets]} } const invalidateCustomerBasketsQuery = ( customerId: string | null, - options: BasketOptions + parameters: BasketOptions['parameters'] ): Pick => { if (!customerId) return {} return { invalidate: [ - and(matchesApiConfig(options), pathStartsWith(['/customers', customerId, '/baskets'])) + and( + matchesApiConfig(parameters), + matchesPath(getCustomerBasketsPath(customerId, parameters)) + ) ] } } const updateBasketFromRequest = ( customerId: string | null, - options: BasketOptions, + {parameters}: BasketOptions, response: Basket ): CacheUpdate => ({ - ...updateBasketQuery(customerId, options.parameters?.basketId, response), - ...invalidateCustomerBasketsQuery(customerId, options) + ...updateBasketQuery(customerId, parameters, response), + ...invalidateCustomerBasketsQuery(customerId, parameters) }) const updateBasketFromResponse = ( customerId: string | null, - options: BasketOptions, + {parameters}: BasketOptions, response: Basket ): CacheUpdate => ({ - ...updateBasketQuery(customerId, response.basketId, response), - ...invalidateCustomerBasketsQuery(customerId, options) + ...updateBasketQuery(customerId, {...parameters, basketId: response.basketId}, response), + ...invalidateCustomerBasketsQuery(customerId, parameters) }) export const cacheUpdateMatrix: CacheUpdateMatrix = { @@ -103,9 +115,9 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { removeItemFromBasket: updateBasketFromRequest, addPaymentInstrumentToBasket: updateBasketFromRequest, createBasket: updateBasketFromResponse, // Response! - deleteBasket: (customerId, options): CacheUpdate => ({ - ...invalidateCustomerBasketsQuery(customerId, options), - ...removeBasketQuery(options) + deleteBasket: (customerId, {parameters}) => ({ + ...invalidateCustomerBasketsQuery(customerId, parameters), + remove: [and(matchesApiConfig(parameters), matchesPath(getBasketPath(parameters)))] }), mergeBasket: updateBasketFromResponse, // Response! removeCouponFromBasket: updateBasketFromRequest, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts index 506fab438c..b2cd0be6c2 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts @@ -4,132 +4,109 @@ * 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 {ShopperCustomersTypes} from 'commerce-sdk-isomorphic' -import { - ApiClients, - ApiOptions, - CacheUpdate, - CacheUpdateMatrix, - CacheUpdateUpdate, - MergedOptions -} from '../types' -import {and, matchesApiConfig, NotImplementedError, pathStartsWith} from '../utils' +import {ApiClientConfigParams, ApiClients, CacheUpdateMatrix} from '../types' +import {and, matchesApiConfig, matchesPath, NotImplementedError, pathStartsWith} from '../utils' type Client = ApiClients['shopperCustomers'] -type Customer = ShopperCustomersTypes.Customer -type CustomerOptions = MergedOptions> const noop = () => ({}) const TODO = (method: keyof Client) => { throw new NotImplementedError(`Cache logic for '${method}'`) } -const baseQueryKey = (customerId: string, parameters: CustomerOptions['parameters']) => [ +// Path helpers (avoid subtle typos!) +const getCustomerPath = (customerId: string, parameters: ApiClientConfigParams) => [ '/organizations/', parameters.organizationId, '/customers/', customerId ] - -/** Invalidates the customer and all derivative endpoints */ -const invalidateCustomer = ( +const getProductListPath = ( + customerId: string, + parameters: ApiClientConfigParams & {listId: string} +) => [...getCustomerPath(customerId, parameters), '/product-lists/', parameters.listId] +const getProductListItemPath = ( customerId: string, - parameters: CustomerOptions['parameters'] -): CacheUpdate => { + parameters: ApiClientConfigParams & {listId: string; itemId: string} +) => [...getProductListPath(customerId, parameters), '/items/', parameters.itemId] +const getPaymentInstrumentPath = ( + customerId: string, + parameters: ApiClientConfigParams & {paymentInstrumentId: string} +) => [ + ...getCustomerPath(customerId, parameters), + '/payment-instruments/', + parameters.paymentInstrumentId +] +const getCustomerAddressPath = ( + customerId: string, + parameters: ApiClientConfigParams, + address: string // Not a parameter because some endpoints use addressId and some use addressName +) => [...getCustomerPath(customerId, parameters), '/addresses/', address] + +/** Invalidates the customer endpoint, but not derivative endpoints. */ +const invalidateCustomer = (customerId: string, parameters: ApiClientConfigParams) => { return { invalidate: [ - and(matchesApiConfig(parameters), pathStartsWith(baseQueryKey(customerId, parameters))) + and(matchesApiConfig(parameters), matchesPath(getCustomerPath(customerId, parameters))) ] } } -// TODO: Rather than every /customers/{customerId} endpoint whenever we update -// a derivative property (e.g. address), can we instead update the corresponding -// cached query and insert the data into the cached customer query, but leave -// everything else alone? export const cacheUpdateMatrix: CacheUpdateMatrix = { authorizeCustomer: TODO('authorizeCustomer'), authorizeTrustedSystem: TODO('authorizeTrustedSystem'), createCustomerAddress(customerId, {parameters}, response) { if (!customerId) return {} + // getCustomerAddress uses `addressName` rather than `addressId` + const address = response.addressId + const newParams = {...parameters, addressName: address} return { ...invalidateCustomer(customerId, parameters), update: [ - { - queryKey: [ - ...baseQueryKey(customerId, parameters), - '/addresses', - response.addressId, - // getCustomerAddress uses `addressName` rather than `addressId` - {...parameters, addressName: response.addressId} - ] - } + {queryKey: [...getCustomerAddressPath(customerId, newParams, address), newParams]} ] } }, createCustomerPaymentInstrument(customerId, {parameters}, response) { if (!customerId) return {} - + const newParams = {...parameters, paymentInstrumentId: response.paymentInstrumentId} return { ...invalidateCustomer(customerId, parameters), - update: [ - { - queryKey: [ - ...baseQueryKey(customerId, parameters), - '/payment-instruments/', - response.paymentInstrumentId, - {parameters, paymentInstrumentId: response.paymentInstrumentId} - ] - } - ] + update: [{queryKey: [...getPaymentInstrumentPath(customerId, newParams), newParams]}] } }, createCustomerProductList(customerId, {parameters}, response) { if (!customerId) return {} - const base = baseQueryKey(customerId, parameters) + const customerPath = getCustomerPath(customerId, parameters) + // We always invalidate, because even without an ID we assume that something has changed + const invalidate = [ + and( + matchesApiConfig(parameters), + // NOTE: This is the aggregate endpoint, so there's no trailing / on /product-lists + matchesPath([...customerPath, '/product-lists']) + ) + ] + const listId = response.id + // We can only update cache for this product list if we get the ID + if (!listId) return {invalidate} + const newParams = {...parameters, listId} return { - // We can only update cache if the response comes with an ID - update: !response.id - ? [] - : [ - { - queryKey: [ - ...base, - '/product-lists/', - response.id, - { - ...parameters, - listId: response.id - } - ] - } - ], - // We always invalidate, because even without an ID we assume that something has changed - invalidate: [ - // TODO: Convert to exact path match - and(matchesApiConfig(parameters), pathStartsWith([...base, '/product-lists'])) - ] + invalidate, + update: [{queryKey: [...getProductListPath(customerId, newParams), newParams]}] } }, createCustomerProductListItem(customerId, {parameters}, response) { if (!customerId) return {} - const base = baseQueryKey(customerId, parameters) + const itemId = response.id return { // We can only update cache if the response comes with an ID - update: !response.id + update: !itemId ? [] : [ { queryKey: [ - ...base, - '/product-lists/', - parameters.listId, - '/items/', - response.id, - { - ...parameters, - itemId: response.id - } + ...getProductListItemPath(customerId, {...parameters, itemId}), + {...parameters, itemId} ] } ], @@ -137,8 +114,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { invalidate: [ and( matchesApiConfig(parameters), - // TODO: Convert to exact path match - pathStartsWith([...base, '/product-lists/', parameters.listId]) + matchesPath(getProductListPath(customerId, parameters)) ) ] } @@ -150,11 +126,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { remove: [ and( matchesApiConfig(parameters), - pathStartsWith([ - ...baseQueryKey(customerId, parameters), - '/payment-instruments', - parameters.paymentInstrumentId - ]) + matchesPath(getPaymentInstrumentPath(customerId, parameters)) ) ] } @@ -162,24 +134,17 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { deleteCustomerProductList: TODO('deleteCustomerProductList'), deleteCustomerProductListItem(customerId, {parameters}) { if (!customerId) return {} - const base = baseQueryKey(customerId, parameters) return { invalidate: [ and( matchesApiConfig(parameters), - pathStartsWith([...base, '/product-lists/', parameters.listId]) + matchesPath(getProductListPath(customerId, parameters)) ) ], remove: [ and( matchesApiConfig(parameters), - pathStartsWith([ - ...base, - '/product-lists/', - parameters.listId, - '/items/', - parameters.itemId - ]) + matchesPath(getProductListItemPath(customerId, parameters)) ) ] } @@ -195,11 +160,9 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { remove: [ and( matchesApiConfig(parameters), - pathStartsWith([ - ...baseQueryKey(customerId, parameters), - '/addresses', - parameters.addressName - ]) + matchesPath( + getCustomerAddressPath(customerId, parameters, parameters.addressName) + ) ) ] } @@ -207,38 +170,23 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { resetPassword: TODO('resetPassword'), updateCustomer(customerId, {parameters}) { if (!customerId) return {} - const base = baseQueryKey(customerId, parameters) - const update: CacheUpdateUpdate[] = [ - { - queryKey: [...base, parameters] - } - ] - // TODO: Can we just use invalidateCustomer() here, since it invalidates all child paths? - const invalidate = [ - and(matchesApiConfig(parameters), pathStartsWith([...base, '/payment-instruments'])), - and(matchesApiConfig(parameters), pathStartsWith([...base, '/addresses'])), - and( - matchesApiConfig(parameters), - pathStartsWith([ - '/organizations/', - parameters.organizationId, - '/customers/external-profile' - ]) - ) - ] - return {update, invalidate} + const customerPath = getCustomerPath(customerId, parameters) + return { + update: [{queryKey: [...customerPath, parameters]}], + // This is NOT invalidateCustomer(), as we want to invalidate *all* customer endpoints, + // not just getCustomer + invalidate: [and(matchesApiConfig(parameters), pathStartsWith(customerPath))] + } }, updateCustomerAddress(customerId, {parameters}) { if (!customerId) return {} - // TODO: Can this `invalidate` instead be an update that targets the appropriate property? return { + // TODO: Rather than invalidating customer, can we selectively update its `addresses`? ...invalidateCustomer(customerId, parameters), update: [ { queryKey: [ - ...baseQueryKey(customerId, parameters), - '/addresses', - parameters.addressName, + ...getCustomerAddressPath(customerId, parameters, parameters.addressName), parameters ] } @@ -249,27 +197,10 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { updateCustomerProductList: TODO('updateCustomerProductList'), updateCustomerProductListItem(customerId, {parameters}) { if (!customerId) return {} - const base = baseQueryKey(customerId, parameters) + const productListPath = getProductListPath(customerId, parameters) return { - update: [ - { - queryKey: [ - ...base, - '/product-lists/', - parameters.listId, - '/items/', - parameters.itemId, - parameters - ] - } - ], - invalidate: [ - and( - matchesApiConfig(parameters), - // TODO: Convert to exact path match - pathStartsWith([...base, '/product-lists/', parameters.listId]) - ) - ] + update: [{queryKey: [...getProductListItemPath(customerId, parameters), parameters]}], + invalidate: [and(matchesApiConfig(parameters), matchesPath(productListPath))] } } } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts index 835ea3b39b..f897a4e1af 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts @@ -5,11 +5,14 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ApiClients, CacheUpdateMatrix, CacheUpdateUpdate, CacheUpdateInvalidate} from '../types' -import {and, matchesApiConfig, NotImplementedError, pathStartsWith} from '../utils' -import type {ShopperOrdersTypes} from 'commerce-sdk-isomorphic' +import {and, matchesApiConfig, matchesPath, NotImplementedError} from '../utils' type Client = ApiClients['shopperOrders'] -type Order = ShopperOrdersTypes.Order + +const basePath = (parameters: Client['clientConfig']['parameters']) => [ + '/organizations/', + parameters.organizationId +] const TODO = (method: keyof Client) => { throw new NotImplementedError(`Cache logic for '${method}'`) @@ -17,39 +20,27 @@ const TODO = (method: keyof Client) => { export const cacheUpdateMatrix: CacheUpdateMatrix = { createOrder(customerId, {parameters}, response) { - const update: CacheUpdateUpdate[] = [] - if (response.orderNo) { - const updateOrder: CacheUpdateUpdate = { - queryKey: [ - '/organizations/', - parameters.organizationId, - '/orders/', - response.orderNo, - // TODO: These parameters are not guaranteed to match those sent to getOrder, - // how do we handle that? - {...parameters, orderNo: response.orderNo} - ] - } - // TODO: This type assertion is so that we "forget" what type the updater uses. - // Is there a way to avoid the assertion? - update.push(updateOrder as CacheUpdateUpdate) - } + const update: CacheUpdateUpdate[] = !response.orderNo + ? [] + : [ + { + queryKey: [ + ...basePath(parameters), + '/orders/', + response.orderNo, + {...parameters, orderNo: response.orderNo} + ] + } + ] - const invalidate: CacheUpdateInvalidate[] = [] - if (customerId) { - invalidate.push( - and( - matchesApiConfig(parameters), - pathStartsWith([ - '/organization/', - parameters.organizationId, - '/customers/', - customerId, - '/baskets' - ]) - ) - ) - } + const invalidate: CacheUpdateInvalidate[] = !customerId + ? [] + : [ + and( + matchesApiConfig(parameters), + matchesPath([...basePath(parameters), '/customers/', customerId, '/baskets']) + ) + ] return {update, invalidate} }, diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 1615bd0019..354a7130a3 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -38,6 +38,14 @@ export const pathStartsWith = ({queryKey}: Query): boolean => queryKey.length >= search.length && search.every((lookup, idx) => queryKey[idx] === lookup) +/** Creates a query predicate that determines whether a query key fully matches the given path segments. */ +export const matchesPath = + (search: readonly string[]) => + ({queryKey}: Query): boolean => + // ApiQueryKey = [...path, parameters] + queryKey.length === 1 + search.length && + search.every((lookup, idx) => queryKey[idx] === lookup) + /** * Creates a query predicate that determines whether the parameters of the query key exactly match * the search object. From e2377ac5588aaa5f7fac219135879a67bda6d4b8 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 13:27:41 -0500 Subject: [PATCH 044/122] Update names for clarity. --- .../src/hooks/ShopperBaskets/config.ts | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts index df90e85cfe..c5eca1b286 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts @@ -28,7 +28,6 @@ const getBasketPath = (parameters: BasketParameters & {basketId: string}) => [ '/baskets/', parameters.basketId ] - const getCustomerBasketsPath = (customerId: string, parameters: BasketParameters) => [ ...getBasePath(parameters), '/customers/', @@ -91,7 +90,7 @@ const invalidateCustomerBasketsQuery = ( } } -const updateBasketFromRequest = ( +const updateBasket = ( customerId: string | null, {parameters}: BasketOptions, response: Basket @@ -100,7 +99,7 @@ const updateBasketFromRequest = ( ...invalidateCustomerBasketsQuery(customerId, parameters) }) -const updateBasketFromResponse = ( +const updateBasketWithResponseBasketId = ( customerId: string | null, {parameters}: BasketOptions, response: Basket @@ -110,23 +109,23 @@ const updateBasketFromResponse = ( }) export const cacheUpdateMatrix: CacheUpdateMatrix = { - addCouponToBasket: updateBasketFromRequest, - addItemToBasket: updateBasketFromRequest, - removeItemFromBasket: updateBasketFromRequest, - addPaymentInstrumentToBasket: updateBasketFromRequest, - createBasket: updateBasketFromResponse, // Response! + addCouponToBasket: updateBasket, + addItemToBasket: updateBasket, + removeItemFromBasket: updateBasket, + addPaymentInstrumentToBasket: updateBasket, + createBasket: updateBasketWithResponseBasketId, deleteBasket: (customerId, {parameters}) => ({ ...invalidateCustomerBasketsQuery(customerId, parameters), remove: [and(matchesApiConfig(parameters), matchesPath(getBasketPath(parameters)))] }), - mergeBasket: updateBasketFromResponse, // Response! - removeCouponFromBasket: updateBasketFromRequest, - removePaymentInstrumentFromBasket: updateBasketFromRequest, - updateBasket: updateBasketFromRequest, - updateBillingAddressForBasket: updateBasketFromRequest, - updateCustomerForBasket: updateBasketFromRequest, - updateItemInBasket: updateBasketFromRequest, - updatePaymentInstrumentInBasket: updateBasketFromRequest, - updateShippingAddressForShipment: updateBasketFromRequest, - updateShippingMethodForShipment: updateBasketFromRequest + mergeBasket: updateBasketWithResponseBasketId, + removeCouponFromBasket: updateBasket, + removePaymentInstrumentFromBasket: updateBasket, + updateBasket: updateBasket, + updateBillingAddressForBasket: updateBasket, + updateCustomerForBasket: updateBasket, + updateItemInBasket: updateBasket, + updatePaymentInstrumentInBasket: updateBasket, + updateShippingAddressForShipment: updateBasket, + updateShippingMethodForShipment: updateBasket } From 080faf1ca828b20f8f3ca54f4baca26feafd1461 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 14:14:55 -0500 Subject: [PATCH 045/122] Revert test file to version from develop. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 8200156e55..28788ff7b9 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Salesforce, Inc. + * Copyright (c) 2022, 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 @@ -13,8 +13,6 @@ import { assertUpdateQuery, DEFAULT_TEST_HOST, mockMutationEndpoints, - NEW_DATA, - OLD_DATA, renderHookWithProviders } from '../../test-utils' import { @@ -23,7 +21,7 @@ import { useShopperBasketsMutation } from './mutation' import {useBasket} from './query' -import {useCustomerBaskets} from '../ShopperCustomers/query' +import {useCustomerBaskets} from '../ShopperCustomers' import {CacheUpdateMatrixElement} from '../utils' const CUSTOMER_ID = 'CUSTOMER_ID' @@ -44,7 +42,10 @@ jest.mock('../useCustomerId.ts', () => { return jest.fn().mockReturnValue(CUSTOMER_ID) }) -const mutationPayloads = { +type MutationPayloads = { + [key in ShopperBasketsMutationType]?: {body: any; parameters: any} +} +const mutationPayloads: MutationPayloads = { updateBasket: { parameters: {basketId: BASKET_ID}, body: {} @@ -109,9 +110,27 @@ const mutationPayloads = { parameters: {basketId: BASKET_ID, shipmentId: SHIPMENT_ID}, body: {id: '001'} } -} as const +} +const oldCustomerBaskets = { + total: 1, + baskets: [{basketId: BASKET_ID, hello: 'world'}] +} + +const newCustomerBaskets = { + total: 1, + baskets: [{basketId: BASKET_ID, hello: 'world_modified'}] +} -const tests = (Object.keys(mutationPayloads) as Array).map( +const oldBasket = { + basketId: BASKET_ID, + hello: 'world' +} + +const newBasket = { + basketId: BASKET_ID, + hello: 'world_modified' +} +const tests = (Object.keys(mutationPayloads) as ShopperBasketsMutationType[]).map( (mutationName) => { const payload = mutationPayloads[mutationName] @@ -121,7 +140,11 @@ const tests = (Object.keys(mutationPayloads) as Array { - mockMutationEndpoints('/checkout/shopper-baskets/') + mockMutationEndpoints( + '/checkout/shopper-baskets/', + {errorResponse: 200}, + newBasket + ) mockRelatedQueries() const {result, waitForValueToChange} = renderHookWithProviders(() => { @@ -148,21 +171,24 @@ const tests = (Object.keys(mutationPayloads) as Array result.current.mutation.isSuccess) expect(result.current.mutation.isSuccess).toBe(true) - // On successful mutation, the query cache gets updated too. Let's assert it. const cacheUpdateMatrix = getCacheUpdateMatrix(CUSTOMER_ID) // @ts-ignore const matrixElement = cacheUpdateMatrix[mutationName](payload, {}) const {invalidate, update, remove}: CacheUpdateMatrixElement = matrixElement + const assertionData = { + basket: newBasket, + customerBaskets: newCustomerBaskets + } update?.forEach(({name}) => { // @ts-ignore - assertUpdateQuery(result.current.queries[name], NEW_DATA) + assertUpdateQuery(result.current.queries[name], assertionData[name]) }) invalidate?.forEach(({name}) => { // @ts-ignore - assertInvalidateQuery(result.current.queries[name], OLD_DATA) + assertInvalidateQuery(result.current.queries[name], oldCustomerBaskets) }) remove?.forEach(({name}) => { @@ -218,24 +244,24 @@ const mockRelatedQueries = () => { .get((uri) => { return uri.includes(basketEndpoint) }) - .reply(200, OLD_DATA) + .reply(200, oldBasket) nock(DEFAULT_TEST_HOST) .persist() .get((uri) => { return uri.includes(basketEndpoint) }) - .reply(200, NEW_DATA) + .reply(200, newBasket) // For get customer basket nock(DEFAULT_TEST_HOST) .get((uri) => { return uri.includes(customerEndpoint) }) - .reply(200, OLD_DATA) + .reply(200, oldCustomerBaskets) nock(DEFAULT_TEST_HOST) .persist() .get((uri) => { return uri.includes(customerEndpoint) }) - .reply(200, NEW_DATA) + .reply(200, newCustomerBaskets) } From 641138d2782f38ee6763a806ff85c76893f1619f Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 14:25:10 -0500 Subject: [PATCH 046/122] Add reminder to revert. --- packages/commerce-sdk-react/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/commerce-sdk-react/tsconfig.json b/packages/commerce-sdk-react/tsconfig.json index 7ac1dc8571..3ca92fe50e 100644 --- a/packages/commerce-sdk-react/tsconfig.json +++ b/packages/commerce-sdk-react/tsconfig.json @@ -99,6 +99,7 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, + // TODO: Revert when tests are working again "exclude": [ "**/*.test.ts", "**/*.test.tsx" From 9b6b29067553319f9a0e49ba04f3294050955119 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 14:25:19 -0500 Subject: [PATCH 047/122] Remove unnecessary default export. --- packages/commerce-sdk-react/src/hooks/index.ts | 2 -- packages/commerce-sdk-react/src/hooks/useAuthHelper.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/index.ts b/packages/commerce-sdk-react/src/hooks/index.ts index 2c6d3af25c..7546acce3b 100644 --- a/packages/commerce-sdk-react/src/hooks/index.ts +++ b/packages/commerce-sdk-react/src/hooks/index.ts @@ -14,9 +14,7 @@ export * from './ShopperOrders' export * from './ShopperProducts' export * from './ShopperPromotions' export * from './ShopperSearch' -// export {default as useAuthHelper} from './useAuthHelper' export * from './useAuthHelper' - export {default as useCommerceApi} from './useCommerceApi' export {default as useCustomerId} from './useCustomerId' export {default as useCustomerType} from './useCustomerType' diff --git a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts index d5c49fa6a0..b065fbacef 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthHelper.ts @@ -50,5 +50,3 @@ export function useAuthHelper( const method = auth[mutation].bind(auth) as MutationFunction return useMutation(auth.whenReady(method)) } - -export default useAuthHelper \ No newline at end of file From 86fc8235056f83b607123c30c619d34b9856999f Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 14:36:34 -0500 Subject: [PATCH 048/122] Cache update TODOs should return functions, not throw. --- packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts | 2 +- .../commerce-sdk-react/src/hooks/ShopperCustomers/config.ts | 2 +- packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts index fbbaaa25b1..d837668916 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts @@ -9,7 +9,7 @@ import {NotImplementedError} from '../utils' type Client = ApiClients['shopperContexts'] -const TODO = (method: keyof Client) => { +const TODO = (method: keyof Client) => () => { throw new NotImplementedError(`Cache logic for '${method}'`) } export const cacheUpdateMatrix: CacheUpdateMatrix = { diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts index b2cd0be6c2..5c10c5e5ec 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts @@ -10,7 +10,7 @@ import {and, matchesApiConfig, matchesPath, NotImplementedError, pathStartsWith} type Client = ApiClients['shopperCustomers'] const noop = () => ({}) -const TODO = (method: keyof Client) => { +const TODO = (method: keyof Client) => () => { throw new NotImplementedError(`Cache logic for '${method}'`) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts index f897a4e1af..2d16d8261d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts @@ -14,7 +14,7 @@ const basePath = (parameters: Client['clientConfig']['parameters']) => [ parameters.organizationId ] -const TODO = (method: keyof Client) => { +const TODO = (method: keyof Client) => () => { throw new NotImplementedError(`Cache logic for '${method}'`) } From 28441ade39c1e7a225111e9cfa8379c746b943dc Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 16:48:56 -0500 Subject: [PATCH 049/122] Allow user to override authorization header. --- .../commerce-sdk-react/src/hooks/useAuthorizationHeader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts index 1f16dcb0eb..e20a1d6327 100644 --- a/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts +++ b/packages/commerce-sdk-react/src/hooks/useAuthorizationHeader.ts @@ -22,8 +22,8 @@ export const useAuthorizationHeader = ( return await method({ ...options, headers: { - ...options.headers, - Authorization: `Bearer ${access_token}` + Authorization: `Bearer ${access_token}`, + ...options.headers } }) } From f517523be29160376130702ae7c0127421b95ad8 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 16 Feb 2023 17:00:00 -0500 Subject: [PATCH 050/122] Remove old TODO. --- packages/commerce-sdk-react/src/hooks/types.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index c653437cd7..076ef4356c 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -126,9 +126,7 @@ export type MergedOptions /** * Query key interface used by API query hooks. */ -export type ApiQueryKey = - // | readonly string[] // TODO: Is this needed? - readonly [...path: string[], parameters: Record] +export type ApiQueryKey = readonly [...path: string[], parameters: Record] /** * Interface to update a cached API response. From d403b4de0c4db6698b4f642dfe86ad7dbd48b26f Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 17 Feb 2023 11:49:30 -0500 Subject: [PATCH 051/122] Remove ability to set custom query key. Cache update logic requires knowing what query keys are used, allowing arbitrary keys makes it impossible to know. --- packages/commerce-sdk-react/src/hooks/useQuery.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index a2cae46f8b..561204d586 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -36,8 +36,7 @@ export const useQuery = < const netOptions = mergeOptions(hookConfig.client, apiOptions) const authenticatedMethod = useAuthorizationHeader(hookConfig.method) return useReactQuery( - // End user can override queryKey if they really want to... - queryOptions.queryKey ?? hookConfig.getQueryKey(netOptions), + hookConfig.getQueryKey(netOptions), () => authenticatedMethod(apiOptions), { enabled: From 7a0deab50809bf938141fcf005938eb47840a0f6 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 17 Feb 2023 15:27:30 -0500 Subject: [PATCH 052/122] Replace getQueryKey callback with just declaring query key. --- .../src/hooks/ShopperBaskets/query.ts | 237 ++++---- .../src/hooks/ShopperContexts/query.ts | 49 +- .../src/hooks/ShopperCustomers/query.ts | 566 +++++++++--------- .../src/hooks/ShopperExperience/query.ts | 73 +-- .../hooks/ShopperGiftCertificates/query.ts | 43 +- .../src/hooks/ShopperLogin/query.ts | 254 ++++---- .../src/hooks/ShopperOrders/query.ts | 136 +++-- .../src/hooks/ShopperProducts/query.ts | 153 ++--- .../src/hooks/ShopperPromotions/query.ts | 95 +-- .../src/hooks/ShopperSearch/query.ts | 90 +-- .../commerce-sdk-react/src/hooks/types.ts | 28 +- .../commerce-sdk-react/src/hooks/useQuery.ts | 44 +- .../commerce-sdk-react/src/hooks/utils.ts | 21 +- 13 files changed, 940 insertions(+), 849 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts index 37981d8f03..e96c7153af 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperBaskets'] @@ -20,28 +21,29 @@ type Client = ApiClients['shopperBaskets'] */ export const useBasket = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => client.getBasket(arg) + const method = async (options: Argument) => await client.getBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } /** @@ -53,36 +55,36 @@ export const useBasket = ( */ export const usePaymentMethodsForBasket = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPaymentMethodsForBasket(arg) + const method = async (options: Argument) => + await client.getPaymentMethodsForBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/payment-methods', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/payment-methods', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperBaskets#getPriceBooksForBasket`. @@ -93,33 +95,36 @@ export const usePaymentMethodsForBasket = ( */ export const usePriceBooksForBasket = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPriceBooksForBasket(arg) + const method = async (options: Argument) => + await client.getPriceBooksForBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/price-books', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/price-books', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperBaskets#getShippingMethodsForShipment`. @@ -130,38 +135,38 @@ export const usePriceBooksForBasket = ( */ export const useShippingMethodsForShipment = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => - client.getShippingMethodsForShipment(arg) + const method = async (options: Argument) => + await client.getShippingMethodsForShipment(options) const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/shipments/', + parameters.shipmentId, + '/shipping-methods', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/shipments/', - parameters.shipmentId, - '/shipping-methods', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperBaskets#getTaxesFromBasket`. @@ -172,30 +177,34 @@ export const useShippingMethodsForShipment = ( */ export const useTaxesFromBasket = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperBaskets: client} = useCommerceApi() - const method = (arg: Argument) => client.getTaxesFromBasket(arg) + const method = async (options: Argument) => + await client.getTaxesFromBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/baskets/', + parameters.basketId, + '/taxes', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/taxes', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts index 4522ba1f15..e98f637445 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperContexts'] @@ -20,29 +21,33 @@ type Client = ApiClients['shopperContexts'] */ export const useShopperContext = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperContexts: client} = useCommerceApi() - const method = (arg: Argument) => client.getShopperContext(arg) + const method = async (options: Argument) => + await client.getShopperContext(options) const requiredParameters = ['organizationId', 'usid'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/shopper-context/', + parameters.usid, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/shopper-context/', - parameters.usid, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts index b3e6bf6ed1..a8208761fc 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperCustomers'] @@ -20,35 +21,39 @@ type Client = ApiClients['shopperCustomers'] */ export const useExternalProfile = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => client.getExternalProfile(arg) + const method = async (options: Argument) => + await client.getExternalProfile(options) const requiredParameters = [ 'organizationId', 'externalId', 'authenticationProviderId', 'siteId' ] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/external-profile', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/external-profile', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getCustomer`. @@ -59,28 +64,30 @@ export const useExternalProfile = ( */ export const useCustomer = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => client.getCustomer(arg) + const method = async (options: Argument) => + await client.getCustomer(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } /** @@ -92,33 +99,37 @@ export const useCustomer = ( */ export const useCustomerAddress = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => client.getCustomerAddress(arg) + const method = async (options: Argument) => + await client.getCustomerAddress(options) const requiredParameters = ['organizationId', 'customerId', 'addressName', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/addresses/', + parameters.addressName, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/addresses/', - parameters.addressName, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getCustomerBaskets`. @@ -129,32 +140,36 @@ export const useCustomerAddress = ( */ export const useCustomerBaskets = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => client.getCustomerBaskets(arg) + const method = async (options: Argument) => + await client.getCustomerBaskets(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/baskets', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/baskets', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getCustomerOrders`. @@ -165,32 +180,36 @@ export const useCustomerBaskets = ( */ export const useCustomerOrders = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => client.getCustomerOrders(arg) + const method = async (options: Argument) => + await client.getCustomerOrders(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/orders', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/orders', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getCustomerPaymentInstrument`. @@ -201,42 +220,42 @@ export const useCustomerOrders = ( */ export const useCustomerPaymentInstrument = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => - client.getCustomerPaymentInstrument(arg) + const method = async (options: Argument) => + await client.getCustomerPaymentInstrument(options) const requiredParameters = [ 'organizationId', 'customerId', 'paymentInstrumentId', 'siteId' ] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/payment-instruments/', + parameters.paymentInstrumentId, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/payment-instruments/', - parameters.paymentInstrumentId, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getCustomerProductLists`. @@ -247,33 +266,36 @@ export const useCustomerPaymentInstrument = ( */ export const useCustomerProductLists = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => - client.getCustomerProductLists(arg) + const method = async (options: Argument) => + await client.getCustomerProductLists(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/product-lists', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/product-lists', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getCustomerProductList`. @@ -284,34 +306,37 @@ export const useCustomerProductLists = ( */ export const useCustomerProductList = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => - client.getCustomerProductList(arg) + const method = async (options: Argument) => + await client.getCustomerProductList(options) const requiredParameters = ['organizationId', 'customerId', 'listId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/product-lists/', + parameters.listId, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/product-lists/', - parameters.listId, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getCustomerProductListItem`. @@ -322,14 +347,11 @@ export const useCustomerProductList = ( */ export const useCustomerProductListItem = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => - client.getCustomerProductListItem(arg) + const method = async (options: Argument) => + await client.getCustomerProductListItem(options) const requiredParameters = [ 'organizationId', 'customerId', @@ -337,30 +359,33 @@ export const useCustomerProductListItem = ( 'itemId', 'siteId' ] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/customers/', + parameters.customerId, + '/product-lists/', + parameters.listId, + '/items/', + parameters.itemId, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/product-lists/', - parameters.listId, - '/items/', - parameters.itemId, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getPublicProductListsBySearchTerm`. @@ -371,34 +396,34 @@ export const useCustomerProductListItem = ( */ export const usePublicProductListsBySearchTerm = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPublicProductListsBySearchTerm(arg) + const method = async (options: Argument) => + await client.getPublicProductListsBySearchTerm(options) const requiredParameters = ['organizationId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/product-lists', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/product-lists', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getPublicProductList`. @@ -409,32 +434,35 @@ export const usePublicProductListsBySearchTerm = ( */ export const usePublicProductList = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPublicProductList(arg) + const method = async (options: Argument) => + await client.getPublicProductList(options) const requiredParameters = ['organizationId', 'listId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/product-lists/', + parameters.listId, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/product-lists/', - parameters.listId, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperCustomers#getProductListItem`. @@ -445,31 +473,35 @@ export const usePublicProductList = ( */ export const useProductListItem = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperCustomers: client} = useCommerceApi() - const method = (arg: Argument) => client.getProductListItem(arg) + const method = async (options: Argument) => + await client.getProductListItem(options) const requiredParameters = ['organizationId', 'listId', 'itemId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/product-lists/', + parameters.listId, + '/items/', + parameters.itemId, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/product-lists/', - parameters.listId, - '/items/', - parameters.itemId, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts index 3b2ec5b1ca..9a97b1509d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperExperience'] @@ -24,27 +25,28 @@ Either `categoryId` or `productId` must be given in addition to `aspectTypeId`. */ export const usePages = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperExperience: client} = useCommerceApi() - const method = (arg: Argument) => client.getPages(arg) + const method = async (options: Argument) => await client.getPages(options) const requiredParameters = ['organizationId', 'aspectTypeId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/pages', - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/pages', + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } /** @@ -58,27 +60,28 @@ export const usePages = ( */ export const usePage = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperExperience: client} = useCommerceApi() - const method = (arg: Argument) => client.getPage(arg) + const method = async (options: Argument) => await client.getPage(options) const requiredParameters = ['organizationId', 'pageId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/pages/', - parameters.pageId, - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/pages/', + parameters.pageId, + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts index 68c614b451..7a3257d9ff 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperGiftCertificates'] @@ -20,39 +21,39 @@ type Client = ApiClients['shopperGiftCertificates'] */ export const useGiftCertificate = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperGiftCertificates: client} = useCommerceApi() - const method = (arg: Argument) => client.getGiftCertificate(arg) + const method = async (options: Argument) => + await client.getGiftCertificate(options) const requiredParameters = ['organizationId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/gift-certificate', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/gift-certificate', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery( - apiOptions, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, { // !!! This is a violation of our design goal of minimal logic in the indivudal endpoint - // endpoint hooks. This is because this method is a POST method, rather than GET, + // endpoint hooks. This is because this method is a post method, rather than GET, // and its body contains secrets. Setting cacheTime to 0 avoids exposing the secrets in // the shared cache. cacheTime: 0, ...queryOptions }, { - client, method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters } ) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts index 940df75de0..2e16e87019 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperLogin'] @@ -20,34 +21,34 @@ type Client = ApiClients['shopperLogin'] */ export const useCredQualityUserInfo = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperLogin: client} = useCommerceApi() - const method = (arg: Argument) => - client.retrieveCredQualityUserInfo(arg) + const method = async (options: Argument) => + await client.retrieveCredQualityUserInfo(options) const requiredParameters = ['organizationId', 'username'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/cred-qual/user', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/cred-qual/user', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperLogin#authorizeCustomer`. @@ -60,36 +61,41 @@ This endpoint can be called from the front channel (the browser). */ export const useAuthorizeCustomer = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperLogin: client} = useCommerceApi() - const method = (arg: Argument) => client.authorizeCustomer(arg) + const method = async (options: Argument) => + await client.authorizeCustomer(options) const requiredParameters = [ 'organizationId', 'redirect_uri', 'response_type', 'client_id', + 'code_challenge' ] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/oauth2/authorize', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/oauth2/authorize', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperLogin#getTrustedAgentAuthorizationToken`. @@ -100,14 +106,11 @@ export const useAuthorizeCustomer = ( */ export const useTrustedAgentAuthorizationToken = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperLogin: client} = useCommerceApi() - const method = (arg: Argument) => - client.getTrustedAgentAuthorizationToken(arg) + const method = async (options: Argument) => + await client.getTrustedAgentAuthorizationToken(options) const requiredParameters = [ 'organizationId', 'client_id', @@ -118,25 +121,28 @@ export const useTrustedAgentAuthorizationToken = ( 'redirect_uri', 'response_type' ] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/oauth2/trusted-agent/authorize', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/oauth2/trusted-agent/authorize', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperLogin#getUserInfo`. @@ -147,27 +153,29 @@ export const useTrustedAgentAuthorizationToken = ( */ export const useUserInfo = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperLogin: client} = useCommerceApi() - const method = (arg: Argument) => client.getUserInfo(arg) + const method = async (options: Argument) => + await client.getUserInfo(options) const requiredParameters = ['organizationId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/oauth2/userinfo', - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/oauth2/userinfo', + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } /** @@ -179,34 +187,34 @@ export const useUserInfo = ( */ export const useWellknownOpenidConfiguration = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperLogin: client} = useCommerceApi() - const method = (arg: Argument) => - client.getWellknownOpenidConfiguration(arg) + const method = async (options: Argument) => + await client.getWellknownOpenidConfiguration(options) const requiredParameters = ['organizationId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/oauth2/.well-known/openid-configuration', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/oauth2/.well-known/openid-configuration', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperLogin#getJwksUri`. @@ -217,26 +225,28 @@ export const useWellknownOpenidConfiguration = ( */ export const useJwksUri = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperLogin: client} = useCommerceApi() - const method = (arg: Argument) => client.getJwksUri(arg) + const method = async (options: Argument) => + await client.getJwksUri(options) const requiredParameters = ['organizationId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/oauth2/jwks', - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/oauth2/jwks', + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts index 265ffe8561..eab9a34be5 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperOrders'] @@ -20,28 +21,29 @@ type Client = ApiClients['shopperOrders'] */ export const useOrder = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperOrders: client} = useCommerceApi() - const method = (arg: Argument) => client.getOrder(arg) + const method = async (options: Argument) => await client.getOrder(options) const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/orders/', - parameters.orderNo, - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/orders/', + parameters.orderNo, + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } /** @@ -53,36 +55,36 @@ export const useOrder = ( */ export const usePaymentMethodsForOrder = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperOrders: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPaymentMethodsForOrder(arg) + const method = async (options: Argument) => + await client.getPaymentMethodsForOrder(options) const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/orders/', + parameters.orderNo, + '/payment-methods', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/orders/', - parameters.orderNo, - '/payment-methods', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperOrders#getTaxesFromOrder`. @@ -95,30 +97,34 @@ for more information. */ export const useTaxesFromOrder = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperOrders: client} = useCommerceApi() - const method = (arg: Argument) => client.getTaxesFromOrder(arg) + const method = async (options: Argument) => + await client.getTaxesFromOrder(options) const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/orders/', + parameters.orderNo, + '/taxes', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/orders/', - parameters.orderNo, - '/taxes', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts index 3a5f65c2bb..e0bbe823e5 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperProducts'] @@ -20,27 +21,29 @@ type Client = ApiClients['shopperProducts'] */ export const useProducts = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperProducts: client} = useCommerceApi() - const method = (arg: Argument) => client.getProducts(arg) + const method = async (options: Argument) => + await client.getProducts(options) const requiredParameters = ['organizationId', 'ids', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/products', - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/products', + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } /** @@ -52,28 +55,30 @@ export const useProducts = ( */ export const useProduct = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperProducts: client} = useCommerceApi() - const method = (arg: Argument) => client.getProduct(arg) + const method = async (options: Argument) => + await client.getProduct(options) const requiredParameters = ['organizationId', 'id', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/products/', - parameters.id, - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/products/', + parameters.id, + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } /** @@ -85,28 +90,34 @@ export const useProduct = ( */ export const useCategories = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperProducts: client} = useCommerceApi() - const method = (arg: Argument) => client.getCategories(arg) + const method = async (options: Argument) => + await client.getCategories(options) const requiredParameters = ['organizationId', 'ids', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/categories', - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/categories', + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperProducts#getCategory`. @@ -119,27 +130,29 @@ parameter. The server only returns online categories. */ export const useCategory = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperProducts: client} = useCommerceApi() - const method = (arg: Argument) => client.getCategory(arg) + const method = async (options: Argument) => + await client.getCategory(options) const requiredParameters = ['organizationId', 'id', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/categories/', - parameters.id, - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/categories/', + parameters.id, + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>(netOptions, queryOptions, { method, - requiredParameters, - getQueryKey + queryKey, + requiredParameters }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts index 0cbaf132ea..be9c4ab019 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperPromotions'] @@ -20,28 +21,34 @@ type Client = ApiClients['shopperPromotions'] */ export const usePromotions = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperPromotions: client} = useCommerceApi() - const method = (arg: Argument) => client.getPromotions(arg) + const method = async (options: Argument) => + await client.getPromotions(options) const requiredParameters = ['organizationId', 'siteId', 'ids'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/promotions', - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/promotions', + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperPromotions#getPromotionsForCampaign`. @@ -56,33 +63,33 @@ promotions, since the server does not consider promotion qualifiers or schedules */ export const usePromotionsForCampaign = ( apiOptions: Argument, - queryOptions: Omit< - UseQueryOptions>, - 'queryFn' - > = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperPromotions: client} = useCommerceApi() - const method = (arg: Argument) => - client.getPromotionsForCampaign(arg) + const method = async (options: Argument) => + await client.getPromotionsForCampaign(options) const requiredParameters = ['organizationId', 'campaignId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/promotions/campaigns/', + parameters.campaignId, parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/promotions/campaigns/', - parameters.campaignId, - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts index 55e2c513cf..31506c36a7 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts @@ -4,10 +4,11 @@ * 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 {UseQueryOptions, UseQueryResult} from '@tanstack/react-query' -import {ApiClients, Argument, DataType, MergedOptions} from '../types' +import {UseQueryResult} from '@tanstack/react-query' +import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' +import {mergeOptions} from '../utils' type Client = ApiClients['shopperSearch'] @@ -21,28 +22,34 @@ the product search hit. The search result contains only products that are online */ export const useProductSearch = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperSearch: client} = useCommerceApi() - const method = (arg: Argument) => client.productSearch(arg) + const method = async (options: Argument) => + await client.productSearch(options) const requiredParameters = ['organizationId', 'siteId'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({parameters}: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/product-search', - // Full parameters last for easy lookup - parameters - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/product-search', + parameters + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } /** * A hook for `ShopperSearch#getSearchSuggestions`. @@ -53,29 +60,32 @@ export const useProductSearch = ( */ export const useSearchSuggestions = ( apiOptions: Argument, - queryOptions: Omit>, 'queryFn'> = {} + queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { const {shopperSearch: client} = useCommerceApi() - const method = (arg: Argument) => - client.getSearchSuggestions(arg) + const method = async (options: Argument) => + await client.getSearchSuggestions(options) const requiredParameters = ['organizationId', 'siteId', 'q'] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`; they are merged in the helper - // hook, so we use a callback here that receives that merged object. - const getQueryKey = ({ + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order + // to generate the correct query key. + const netOptions = mergeOptions(client, apiOptions) + const {parameters} = netOptions + const queryKey: ApiQueryKey = [ + '/organizations/', + parameters.organizationId, + '/search-suggestions', parameters - }: MergedOptions>) => - [ - '/organizations/', - parameters.organizationId, - '/search-suggestions', - // Full parameters last for easy lookup - parameters - ] as const + ] - return useQuery(apiOptions, queryOptions, { - client, - method, - requiredParameters, - getQueryKey - }) + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery>( + netOptions, + queryOptions, + { + method, + queryKey, + requiredParameters + } + ) } diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 076ef4356c..8b3e3cdafb 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -4,7 +4,7 @@ * 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 {Query, Updater} from '@tanstack/react-query' +import {Query, Updater, UseQueryOptions} from '@tanstack/react-query' import { ShopperBaskets, ShopperContexts, @@ -27,8 +27,7 @@ export type Prettify = NonNullable> * Marks the given keys as required. * WARNING: Does not work if T has an index signature. */ -// The outer Pick<...> is used to prettify the result type -type RequireKeys = Pick>, keyof T> +export type RequireKeys = Prettify>> /** Removes keys whose value is `never`. */ type RemoveNeverValues = { @@ -109,7 +108,7 @@ export type DataType = T extends ApiMethod ? R : nev * Merged headers and parameters from client config and options, mimicking the behavior * of commerce-sdk-isomorphic. */ -export type MergedOptions = RequireKeys< +export type MergedOptions = Required< ApiOptions< NonNullable, // `body` may not exist on `Options`, in which case it is `unknown` here. Due to the type @@ -117,16 +116,23 @@ export type MergedOptions // to indicate that the result type does not have a `body`. unknown extends Options['body'] ? never : Options['body'], NonNullable - >, - 'parameters' | 'headers' + > > -// --- CACHE HELPERS --- // +/** Query key interface used by API query hooks. */ +export type ApiQueryKey = Record> = + readonly [...path: string[], parameters: Params] -/** - * Query key interface used by API query hooks. - */ -export type ApiQueryKey = readonly [...path: string[], parameters: Record] +/** Query options for endpoint hooks. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ApiQueryOptions> = Prettify< + Omit< + UseQueryOptions, unknown, DataType, ApiQueryKey>, + 'queryFn' | 'queryKey' + > +> + +// --- CACHE HELPERS --- // /** * Interface to update a cached API response. diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index 561204d586..2ee4263fae 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -4,10 +4,10 @@ * 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 {useQuery as useReactQuery, UseQueryOptions, QueryKey} from '@tanstack/react-query' +import {useQuery as useReactQuery} from '@tanstack/react-query' import {useAuthorizationHeader} from './useAuthorizationHeader' -import {ApiClient, ApiMethod, ApiOptions, MergedOptions} from './types' -import {hasAllKeys, mergeOptions} from './utils' +import {ApiMethod, ApiOptions, ApiQueryKey, ApiQueryOptions, RequireKeys} from './types' +import {hasAllKeys} from './utils' /** * Helper for query hooks, contains most of the logic in order to keep individual hooks small. @@ -16,36 +16,24 @@ import {hasAllKeys, mergeOptions} from './utils' * @param hookConfig - Config values that vary per API endpoint * @internal */ -export const useQuery = < - Client extends ApiClient, - Options extends Omit, - Data, - Err, - QK extends QueryKey ->( +export const useQuery = , Data>( apiOptions: Options, - queryOptions: UseQueryOptions, + queryOptions: ApiQueryOptions>, hookConfig: { - client: Client method: ApiMethod - getQueryKey: (options: MergedOptions) => QK - requiredParameters: ReadonlyArray['parameters']> + queryKey: ApiQueryKey + requiredParameters: ReadonlyArray enabled?: boolean } ) => { - const netOptions = mergeOptions(hookConfig.client, apiOptions) const authenticatedMethod = useAuthorizationHeader(hookConfig.method) - return useReactQuery( - hookConfig.getQueryKey(netOptions), - () => authenticatedMethod(apiOptions), - { - enabled: - // Individual hooks can provide `enabled` checks that are done in ADDITION to - // the required parameter check - hookConfig.enabled !== false && - hasAllKeys(netOptions.parameters, hookConfig.requiredParameters), - // End users can always completely OVERRIDE the default `enabled` check - ...queryOptions - } - ) + return useReactQuery(hookConfig.queryKey, () => authenticatedMethod(apiOptions), { + enabled: + // Individual hooks can provide `enabled` checks that are done in ADDITION to + // the required parameter check + hookConfig.enabled !== false && + hasAllKeys(apiOptions.parameters, hookConfig.requiredParameters), + // End users can always completely OVERRIDE the default `enabled` check + ...queryOptions + }) } diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 354a7130a3..e445547b7e 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -105,19 +105,20 @@ export const mergeOptions = => { const merged = { - ...options, - headers: { - ...client.clientConfig.headers, - ...options.headers - }, + // Only include body if it is set + ...('body' in options + ? // I'm not entirely sure why these type assertions are necessary. It seems likely that + // the type definitions are more complex than they need to be. + {body: options.body as MergedOptions['body']} + : ({} as {body: never})), parameters: { ...client.clientConfig.parameters, ...options.parameters + }, + headers: { + ...client.clientConfig.parameters, + ...options.parameters } } - // I don't know why TypeScript complains if we don't include `body`, but this type assertion - // fixes it. The type error can also be fixed by adding `body: undefined` before `...options`, - // and then deleting `merged.body` if `options` doesn't have `body`. That's the same as just not - // including it in the first place, so I don't know what difference it makes to TypeScript... - return merged as typeof merged & {body?: undefined} + return merged } From 03f455b355b517a0bf328807847290585359e782 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 17 Feb 2023 15:27:59 -0500 Subject: [PATCH 053/122] Clean up types. --- .../src/hooks/ShopperBaskets/config.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts index c5eca1b286..4e487b567e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts @@ -17,8 +17,7 @@ import {and, matchesApiConfig, matchesPath} from '../utils' type Client = ApiClients['shopperBaskets'] type Basket = ShopperBasketsTypes.Basket -type BasketOptions = MergedOptions> -type BasketParameters = BasketOptions['parameters'] +type BasketParameters = MergedOptions>['parameters'] type CustomerBasketsResult = ShopperCustomersTypes.BasketsResult // Path helpers to avoid typos! @@ -37,7 +36,7 @@ const getCustomerBasketsPath = (customerId: string, parameters: BasketParameters const updateBasketQuery = ( customerId: string | null, - parameters: BasketOptions['parameters'], + parameters: BasketParameters, newBasket: Basket ): CacheUpdate => { // If we just check `!parameters.basketId`, then TypeScript doesn't infer that the value is @@ -77,7 +76,7 @@ const updateBasketQuery = ( const invalidateCustomerBasketsQuery = ( customerId: string | null, - parameters: BasketOptions['parameters'] + parameters: BasketParameters ): Pick => { if (!customerId) return {} return { @@ -92,7 +91,7 @@ const invalidateCustomerBasketsQuery = ( const updateBasket = ( customerId: string | null, - {parameters}: BasketOptions, + {parameters}: {parameters: BasketParameters}, response: Basket ): CacheUpdate => ({ ...updateBasketQuery(customerId, parameters, response), @@ -101,7 +100,7 @@ const updateBasket = ( const updateBasketWithResponseBasketId = ( customerId: string | null, - {parameters}: BasketOptions, + {parameters}: {parameters: BasketParameters}, response: Basket ): CacheUpdate => ({ ...updateBasketQuery(customerId, {...parameters, basketId: response.basketId}, response), From 28dcb7b2a2a668eb72bdeb8674e3f457ff161745 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 17 Feb 2023 15:29:04 -0500 Subject: [PATCH 054/122] Rename config.ts to cache.ts --- .../src/hooks/ShopperBaskets/{config.ts => cache.ts} | 0 .../commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts | 2 +- .../src/hooks/ShopperContexts/{config.ts => cache.ts} | 0 .../commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts | 2 +- .../src/hooks/ShopperCustomers/{config.ts => cache.ts} | 0 .../commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts | 2 +- .../src/hooks/ShopperLogin/{config.ts => cache.ts} | 0 packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts | 2 +- .../src/hooks/ShopperOrders/{config.ts => cache.ts} | 0 packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts | 2 +- 10 files changed, 5 insertions(+), 5 deletions(-) rename packages/commerce-sdk-react/src/hooks/ShopperBaskets/{config.ts => cache.ts} (100%) rename packages/commerce-sdk-react/src/hooks/ShopperContexts/{config.ts => cache.ts} (100%) rename packages/commerce-sdk-react/src/hooks/ShopperCustomers/{config.ts => cache.ts} (100%) rename packages/commerce-sdk-react/src/hooks/ShopperLogin/{config.ts => cache.ts} (100%) rename packages/commerce-sdk-react/src/hooks/ShopperOrders/{config.ts => cache.ts} (100%) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts similarity index 100% rename from packages/commerce-sdk-react/src/hooks/ShopperBaskets/config.ts rename to packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts index 2dd86ede84..0e44a4e7e1 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts @@ -9,7 +9,7 @@ import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' +import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperBaskets'] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/cache.ts similarity index 100% rename from packages/commerce-sdk-react/src/hooks/ShopperContexts/config.ts rename to packages/commerce-sdk-react/src/hooks/ShopperContexts/cache.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts index 4ff24fd270..fefbff71d4 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts @@ -9,7 +9,7 @@ import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' +import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperContexts'] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts similarity index 100% rename from packages/commerce-sdk-react/src/hooks/ShopperCustomers/config.ts rename to packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts index 7522231dd0..699cfe35d5 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts @@ -9,7 +9,7 @@ import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' +import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperCustomers'] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts similarity index 100% rename from packages/commerce-sdk-react/src/hooks/ShopperLogin/config.ts rename to packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts index 5a41b8f3b1..98d9dcd350 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts @@ -9,7 +9,7 @@ import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' +import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperLogin'] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts similarity index 100% rename from packages/commerce-sdk-react/src/hooks/ShopperOrders/config.ts rename to packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts index 7fa3cc8a1f..e190914a89 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts @@ -9,7 +9,7 @@ import {useMutation} from '../useMutation' import {UseMutationResult} from '@tanstack/react-query' import {NotImplementedError} from '../utils' import useCommerceApi from '../useCommerceApi' -import {cacheUpdateMatrix} from './config' +import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperOrders'] From d40554f85bbc9baa034afa2a15ce08a3c3c33892 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Sun, 19 Feb 2023 22:46:38 -0500 Subject: [PATCH 055/122] Restore type checking test files. --- packages/commerce-sdk-react/tsconfig.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/commerce-sdk-react/tsconfig.json b/packages/commerce-sdk-react/tsconfig.json index 3ca92fe50e..767fe15366 100644 --- a/packages/commerce-sdk-react/tsconfig.json +++ b/packages/commerce-sdk-react/tsconfig.json @@ -98,10 +98,5 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - // TODO: Revert when tests are working again - "exclude": [ - "**/*.test.ts", - "**/*.test.tsx" - ] + } } From 730de3a3baf60b43191ba295a926f9bbf9298714 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Sun, 19 Feb 2023 22:47:23 -0500 Subject: [PATCH 056/122] Suppress react query error logs during tests. --- packages/commerce-sdk-react/src/test-utils.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index 8f150a292f..80a027953c 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -30,6 +30,11 @@ export const DEFAULT_TEST_CONFIG = { export const createQueryClient = () => { return new QueryClient({ + logger: { + ...console, + // Disable error logs as we intentionally cause errors during tests + error() {} // eslint-disable-line @typescript-eslint/no-empty-function + }, // During testing, we want things to fail immediately defaultOptions: {queries: {retry: false}, mutations: {retry: false}} }) From 4a2c86dbf6b98f4f58b64a5a9f944c0eace9d804 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 11:54:51 -0500 Subject: [PATCH 057/122] Strip unused parameters before generating query key. --- .../src/hooks/ShopperBaskets/query.ts | 131 +++---- .../src/hooks/ShopperContexts/query.ts | 32 +- .../src/hooks/ShopperCustomers/query.ts | 336 +++++++++--------- .../src/hooks/ShopperExperience/query.ts | 52 ++- .../hooks/ShopperGiftCertificates/query.ts | 20 +- .../src/hooks/ShopperLogin/query.ts | 155 ++++---- .../src/hooks/ShopperOrders/query.ts | 75 ++-- .../src/hooks/ShopperProducts/query.ts | 96 +++-- .../src/hooks/ShopperPromotions/query.ts | 60 ++-- .../src/hooks/ShopperSearch/query.ts | 70 ++-- .../commerce-sdk-react/src/hooks/useQuery.ts | 11 +- .../commerce-sdk-react/src/hooks/utils.ts | 6 + 12 files changed, 577 insertions(+), 467 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts index e96c7153af..8bf697f663 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperBaskets'] @@ -23,24 +23,29 @@ export const useBasket = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Argument) => await client.getBasket(options) + const method = async (options: Options) => await client.getBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + const allParameters = [...requiredParameters, 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters @@ -57,34 +62,34 @@ export const usePaymentMethodsForBasket = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getPaymentMethodsForBasket(options) + const method = async (options: Options) => await client.getPaymentMethodsForBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + const allParameters = [...requiredParameters, 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/payment-methods', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperBaskets#getPriceBooksForBasket`. @@ -97,34 +102,34 @@ export const usePriceBooksForBasket = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getPriceBooksForBasket(options) + const method = async (options: Options) => await client.getPriceBooksForBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/price-books', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperBaskets#getShippingMethodsForShipment`. @@ -137,15 +142,19 @@ export const useShippingMethodsForShipment = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getShippingMethodsForShipment(options) + const method = async (options: Options) => await client.getShippingMethodsForShipment(options) const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const + const allParameters = [...requiredParameters, 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/baskets/', @@ -154,19 +163,15 @@ export const useShippingMethodsForShipment = ( parameters.shipmentId, '/shipping-methods', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperBaskets#getTaxesFromBasket`. @@ -179,32 +184,32 @@ export const useTaxesFromBasket = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getTaxesFromBasket(options) + const method = async (options: Options) => await client.getTaxesFromBasket(options) const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/baskets/', parameters.basketId, '/taxes', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts index e98f637445..b4d150d4cf 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperContexts'] @@ -23,31 +23,31 @@ export const useShopperContext = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperContexts: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getShopperContext(options) + const method = async (options: Options) => await client.getShopperContext(options) const requiredParameters = ['organizationId', 'usid'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/shopper-context/', parameters.usid, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts index a8208761fc..a105eebc45 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperCustomers'] @@ -23,37 +23,37 @@ export const useExternalProfile = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getExternalProfile(options) + const method = async (options: Options) => await client.getExternalProfile(options) const requiredParameters = [ 'organizationId', 'externalId', 'authenticationProviderId', 'siteId' ] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/external-profile', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getCustomer`. @@ -66,25 +66,29 @@ export const useCustomer = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomer(options) + const method = async (options: Options) => await client.getCustomer(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', parameters.customerId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters @@ -101,15 +105,19 @@ export const useCustomerAddress = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomerAddress(options) + const method = async (options: Options) => await client.getCustomerAddress(options) const requiredParameters = ['organizationId', 'customerId', 'addressName', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', @@ -117,19 +125,15 @@ export const useCustomerAddress = ( '/addresses/', parameters.addressName, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getCustomerBaskets`. @@ -142,34 +146,34 @@ export const useCustomerBaskets = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomerBaskets(options) + const method = async (options: Options) => await client.getCustomerBaskets(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', parameters.customerId, '/baskets', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getCustomerOrders`. @@ -182,34 +186,43 @@ export const useCustomerOrders = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomerOrders(options) + const method = async (options: Options) => await client.getCustomerOrders(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + const allParameters = [ + ...requiredParameters, + 'crossSites', + 'from', + 'until', + 'status', + + 'offset', + 'limit' + ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', parameters.customerId, '/orders', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getCustomerPaymentInstrument`. @@ -222,20 +235,24 @@ export const useCustomerPaymentInstrument = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomerPaymentInstrument(options) + const method = async (options: Options) => await client.getCustomerPaymentInstrument(options) const requiredParameters = [ 'organizationId', 'customerId', 'paymentInstrumentId', 'siteId' ] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', @@ -243,19 +260,15 @@ export const useCustomerPaymentInstrument = ( '/payment-instruments/', parameters.paymentInstrumentId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getCustomerProductLists`. @@ -268,34 +281,34 @@ export const useCustomerProductLists = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomerProductLists(options) + const method = async (options: Options) => await client.getCustomerProductLists(options) const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', parameters.customerId, '/product-lists', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getCustomerProductList`. @@ -308,15 +321,19 @@ export const useCustomerProductList = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomerProductList(options) + const method = async (options: Options) => await client.getCustomerProductList(options) const requiredParameters = ['organizationId', 'customerId', 'listId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', @@ -324,19 +341,15 @@ export const useCustomerProductList = ( '/product-lists/', parameters.listId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getCustomerProductListItem`. @@ -349,9 +362,10 @@ export const useCustomerProductListItem = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCustomerProductListItem(options) + const method = async (options: Options) => await client.getCustomerProductListItem(options) const requiredParameters = [ 'organizationId', 'customerId', @@ -359,11 +373,14 @@ export const useCustomerProductListItem = ( 'itemId', 'siteId' ] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/customers/', @@ -373,19 +390,15 @@ export const useCustomerProductListItem = ( '/items/', parameters.itemId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getPublicProductListsBySearchTerm`. @@ -398,32 +411,33 @@ export const usePublicProductListsBySearchTerm = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => + const method = async (options: Options) => await client.getPublicProductListsBySearchTerm(options) const requiredParameters = ['organizationId', 'siteId'] as const + const allParameters = [...requiredParameters, 'email', 'firstName', 'lastName'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/product-lists', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getPublicProductList`. @@ -436,33 +450,33 @@ export const usePublicProductList = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getPublicProductList(options) + const method = async (options: Options) => await client.getPublicProductList(options) const requiredParameters = ['organizationId', 'listId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/product-lists/', parameters.listId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperCustomers#getProductListItem`. @@ -475,15 +489,19 @@ export const useProductListItem = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getProductListItem(options) + const method = async (options: Options) => await client.getProductListItem(options) const requiredParameters = ['organizationId', 'listId', 'itemId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/product-lists/', @@ -491,17 +509,13 @@ export const useProductListItem = ( '/items/', parameters.itemId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts index 9a97b1509d..a719937119 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperExperience'] @@ -27,23 +27,32 @@ export const usePages = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperExperience: client} = useCommerceApi() - const method = async (options: Argument) => await client.getPages(options) + const method = async (options: Options) => await client.getPages(options) const requiredParameters = ['organizationId', 'aspectTypeId', 'siteId'] as const + const allParameters = [ + ...requiredParameters, + 'categoryId', + 'productId', + + 'aspectAttributes', + 'parameters', + + 'locale' + ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ - '/organizations/', - parameters.organizationId, - '/pages', - parameters - ] + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = ['/organizations/', parameters.organizationId, '/pages', parameters] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters @@ -62,24 +71,35 @@ export const usePage = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperExperience: client} = useCommerceApi() - const method = async (options: Argument) => await client.getPage(options) + const method = async (options: Options) => await client.getPage(options) const requiredParameters = ['organizationId', 'pageId', 'siteId'] as const + const allParameters = [ + ...requiredParameters, + 'aspectAttributes', + 'parameters', + + 'locale' + ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/pages/', parameters.pageId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts index 7a3257d9ff..463c003a1f 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperGiftCertificates'] @@ -23,24 +23,28 @@ export const useGiftCertificate = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperGiftCertificates: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getGiftCertificate(options) + const method = async (options: Options) => await client.getGiftCertificate(options) const requiredParameters = ['organizationId', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/gift-certificate', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( + return useQuery( netOptions, { // !!! This is a violation of our design goal of minimal logic in the indivudal endpoint diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts index 2e16e87019..53dbaf27bd 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperLogin'] @@ -23,32 +23,32 @@ export const useCredQualityUserInfo = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Argument) => - await client.retrieveCredQualityUserInfo(options) + const method = async (options: Options) => await client.retrieveCredQualityUserInfo(options) const requiredParameters = ['organizationId', 'username'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/cred-qual/user', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperLogin#authorizeCustomer`. @@ -63,9 +63,10 @@ export const useAuthorizeCustomer = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Argument) => - await client.authorizeCustomer(options) + const method = async (options: Options) => await client.authorizeCustomer(options) const requiredParameters = [ 'organizationId', 'redirect_uri', @@ -74,28 +75,35 @@ export const useAuthorizeCustomer = ( 'code_challenge' ] as const + const allParameters = [ + ...requiredParameters, + + 'scope', + 'state', + 'usid', + 'hint', + 'channel_id' + ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/oauth2/authorize', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperLogin#getTrustedAgentAuthorizationToken`. @@ -108,8 +116,10 @@ export const useTrustedAgentAuthorizationToken = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Argument) => + const method = async (options: Options) => await client.getTrustedAgentAuthorizationToken(options) const requiredParameters = [ 'organizationId', @@ -121,28 +131,27 @@ export const useTrustedAgentAuthorizationToken = ( 'redirect_uri', 'response_type' ] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/oauth2/trusted-agent/authorize', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperLogin#getUserInfo`. @@ -155,24 +164,28 @@ export const useUserInfo = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getUserInfo(options) + const method = async (options: Options) => await client.getUserInfo(options) const requiredParameters = ['organizationId'] as const + const allParameters = [...requiredParameters, 'channel_id'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/oauth2/userinfo', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters @@ -189,32 +202,32 @@ export const useWellknownOpenidConfiguration = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getWellknownOpenidConfiguration(options) + const method = async (options: Options) => await client.getWellknownOpenidConfiguration(options) const requiredParameters = ['organizationId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/oauth2/.well-known/openid-configuration', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperLogin#getJwksUri`. @@ -227,24 +240,28 @@ export const useJwksUri = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getJwksUri(options) + const method = async (options: Options) => await client.getJwksUri(options) const requiredParameters = ['organizationId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/oauth2/jwks', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts index eab9a34be5..60f5a5881e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperOrders'] @@ -23,24 +23,29 @@ export const useOrder = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperOrders: client} = useCommerceApi() - const method = async (options: Argument) => await client.getOrder(options) + const method = async (options: Options) => await client.getOrder(options) const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const + const allParameters = [...requiredParameters, 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/orders/', parameters.orderNo, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters @@ -57,34 +62,34 @@ export const usePaymentMethodsForOrder = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperOrders: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getPaymentMethodsForOrder(options) + const method = async (options: Options) => await client.getPaymentMethodsForOrder(options) const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const + const allParameters = [...requiredParameters, 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/orders/', parameters.orderNo, '/payment-methods', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperOrders#getTaxesFromOrder`. @@ -99,32 +104,32 @@ export const useTaxesFromOrder = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperOrders: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getTaxesFromOrder(options) + const method = async (options: Options) => await client.getTaxesFromOrder(options) const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const + const allParameters = [...requiredParameters] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/orders/', parameters.orderNo, '/taxes', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts index e0bbe823e5..0258e5749d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperProducts'] @@ -23,24 +23,36 @@ export const useProducts = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getProducts(options) + const method = async (options: Options) => await client.getProducts(options) const requiredParameters = ['organizationId', 'ids', 'siteId'] as const + const allParameters = [ + ...requiredParameters, + 'inventoryIds', + 'currency', + 'expand', + 'locale', + 'allImages', + 'perPricebook' + ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/products', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters @@ -57,25 +69,37 @@ export const useProduct = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getProduct(options) + const method = async (options: Options) => await client.getProduct(options) const requiredParameters = ['organizationId', 'id', 'siteId'] as const + const allParameters = [ + ...requiredParameters, + 'inventoryIds', + 'currency', + 'expand', + 'locale', + 'allImages', + 'perPricebook' + ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/products/', parameters.id, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters @@ -92,32 +116,32 @@ export const useCategories = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCategories(options) + const method = async (options: Options) => await client.getCategories(options) const requiredParameters = ['organizationId', 'ids', 'siteId'] as const + const allParameters = [...requiredParameters, 'levels', 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/categories', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperProducts#getCategory`. @@ -132,25 +156,29 @@ export const useCategory = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getCategory(options) + const method = async (options: Options) => await client.getCategory(options) const requiredParameters = ['organizationId', 'id', 'siteId'] as const + const allParameters = [...requiredParameters, 'levels', 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/categories/', parameters.id, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>(netOptions, queryOptions, { + return useQuery(netOptions, queryOptions, { method, queryKey, requiredParameters diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts index be9c4ab019..8b04ded797 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperPromotions'] @@ -23,32 +23,32 @@ export const usePromotions = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperPromotions: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getPromotions(options) + const method = async (options: Options) => await client.getPromotions(options) const requiredParameters = ['organizationId', 'siteId', 'ids'] as const + const allParameters = [...requiredParameters, 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/promotions', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperPromotions#getPromotionsForCampaign`. @@ -65,31 +65,31 @@ export const usePromotionsForCampaign = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperPromotions: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getPromotionsForCampaign(options) + const method = async (options: Options) => await client.getPromotionsForCampaign(options) const requiredParameters = ['organizationId', 'campaignId', 'siteId'] as const + const allParameters = [...requiredParameters, 'startDate', 'endDate', 'currency'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/promotions/campaigns/', parameters.campaignId, parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts index 31506c36a7..8d3e88f880 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {UseQueryResult} from '@tanstack/react-query' -import {ApiClients, ApiQueryKey, ApiQueryOptions, Argument, DataType} from '../types' +import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions} from '../utils' +import {mergeOptions, pick} from '../utils' type Client = ApiClients['shopperSearch'] @@ -24,32 +24,42 @@ export const useProductSearch = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperSearch: client} = useCommerceApi() - const method = async (options: Argument) => - await client.productSearch(options) + const method = async (options: Options) => await client.productSearch(options) const requiredParameters = ['organizationId', 'siteId'] as const + const allParameters = [ + ...requiredParameters, + 'q', + 'refine', + 'sort', + 'currency', + 'locale', + 'expand', + 'offset', + 'limit' + ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/product-search', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } /** * A hook for `ShopperSearch#getSearchSuggestions`. @@ -62,30 +72,30 @@ export const useSearchSuggestions = ( apiOptions: Argument, queryOptions: ApiQueryOptions = {} ): UseQueryResult> => { + type Options = Argument + type Data = DataType const {shopperSearch: client} = useCommerceApi() - const method = async (options: Argument) => - await client.getSearchSuggestions(options) + const method = async (options: Options) => await client.getSearchSuggestions(options) const requiredParameters = ['organizationId', 'siteId', 'q'] as const + const allParameters = [...requiredParameters, 'limit', 'currency', 'locale'] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - const {parameters} = netOptions - const queryKey: ApiQueryKey = [ + // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must + // exclude them when generating the query key. + const parameters = pick(netOptions.parameters, allParameters) + const queryKey = [ '/organizations/', parameters.organizationId, '/search-suggestions', parameters - ] + ] as const // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery>( - netOptions, - queryOptions, - { - method, - queryKey, - requiredParameters - } - ) + return useQuery(netOptions, queryOptions, { + method, + queryKey, + requiredParameters + }) } diff --git a/packages/commerce-sdk-react/src/hooks/useQuery.ts b/packages/commerce-sdk-react/src/hooks/useQuery.ts index 2ee4263fae..5b61be7179 100644 --- a/packages/commerce-sdk-react/src/hooks/useQuery.ts +++ b/packages/commerce-sdk-react/src/hooks/useQuery.ts @@ -6,7 +6,7 @@ */ import {useQuery as useReactQuery} from '@tanstack/react-query' import {useAuthorizationHeader} from './useAuthorizationHeader' -import {ApiMethod, ApiOptions, ApiQueryKey, ApiQueryOptions, RequireKeys} from './types' +import {ApiMethod, ApiOptions, ApiQueryKey, ApiQueryOptions} from './types' import {hasAllKeys} from './utils' /** @@ -16,13 +16,13 @@ import {hasAllKeys} from './utils' * @param hookConfig - Config values that vary per API endpoint * @internal */ -export const useQuery = , Data>( +export const useQuery = ( apiOptions: Options, queryOptions: ApiQueryOptions>, hookConfig: { method: ApiMethod - queryKey: ApiQueryKey - requiredParameters: ReadonlyArray + queryKey: ApiQueryKey> + requiredParameters: ReadonlyArray> enabled?: boolean } ) => { @@ -32,7 +32,8 @@ export const useQuery = , // Individual hooks can provide `enabled` checks that are done in ADDITION to // the required parameter check hookConfig.enabled !== false && - hasAllKeys(apiOptions.parameters, hookConfig.requiredParameters), + // The default `enabled` is "has all required parameters" + hasAllKeys(apiOptions.parameters ?? {}, hookConfig.requiredParameters), // End users can always completely OVERRIDE the default `enabled` check ...queryOptions }) diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index e445547b7e..d363d2a36e 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -122,3 +122,9 @@ export const mergeOptions = (obj: T, keys: readonly K[]): Pick => { + const picked = {} as Pick // Assertion is not true, yet, but we make it so! + keys.forEach((key) => (picked[key] = obj[key])) + return picked +} From 3bf4f0582b2ccfd4c0213c9a9d7b09f74c56b66b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 13:03:59 -0500 Subject: [PATCH 058/122] Fix tpe errors. --- .../commerce-sdk-react/src/auth/index.test.ts | 20 ++++-- packages/commerce-sdk-react/src/auth/index.ts | 2 +- .../src/hooks/query.test.tsx | 72 ++++++++++--------- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/packages/commerce-sdk-react/src/auth/index.test.ts b/packages/commerce-sdk-react/src/auth/index.test.ts index c5920db3b0..9f27e1a6ac 100644 --- a/packages/commerce-sdk-react/src/auth/index.test.ts +++ b/packages/commerce-sdk-react/src/auth/index.test.ts @@ -4,7 +4,7 @@ * 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 Auth from './' +import Auth, {AuthData} from './' import jwt from 'jsonwebtoken' import {helpers} from 'commerce-sdk-isomorphic' import * as utils from '../utils' @@ -85,7 +85,7 @@ describe('Auth', () => { test('this.data returns the storage value', () => { const auth = new Auth(config) - const sample = { + const sample: Omit & {refresh_token_guest?: string} = { refresh_token_guest: 'refresh_token_guest', access_token: 'access_token', customer_id: 'customer_id', @@ -97,7 +97,9 @@ describe('Auth', () => { usid: 'usid', customer_type: 'guest' } - const {refresh_token_guest, ...result} = {...sample, refresh_token: 'refresh_token_guest'} + // Convert stored format to exposed format + const result = {...sample, refresh_token: 'refresh_token_guest'} + delete result.refresh_token_guest Object.keys(sample).forEach((key) => { // @ts-expect-error private method @@ -161,7 +163,7 @@ describe('Auth', () => { test('ready - re-use valid access token', () => { const auth = new Auth(config) - const data = { + const data: Omit & {refresh_token_guest?: string} = { refresh_token_guest: 'refresh_token_guest', access_token: jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret'), customer_id: 'customer_id', @@ -173,7 +175,9 @@ describe('Auth', () => { usid: 'usid', customer_type: 'guest' } - const {refresh_token_guest, ...result} = {...data, refresh_token: 'refresh_token_guest'} + // Convert stored format to exposed format + const result = {...data, refresh_token: 'refresh_token_guest'} + delete result.refresh_token_guest Object.keys(data).forEach((key) => { // @ts-expect-error private method @@ -185,7 +189,7 @@ describe('Auth', () => { test('ready - use refresh token when access token is expired', async () => { const auth = new Auth(config) - const data = { + const data: Omit & {refresh_token_guest?: string} = { refresh_token_guest: 'refresh_token_guest', access_token: jwt.sign({exp: Math.floor(Date.now() / 1000) - 1000}, 'secret'), customer_id: 'customer_id', @@ -197,7 +201,9 @@ describe('Auth', () => { usid: 'usid', customer_type: 'guest' } - const {refresh_token_guest, ...result} = {...data, refresh_token: 'refresh_token_guest'} + // Convert stored format to exposed format + const result = {...data, refresh_token: 'refresh_token_guest'} + delete result.refresh_token_guest Object.keys(data).forEach((key) => { // @ts-expect-error private method diff --git a/packages/commerce-sdk-react/src/auth/index.ts b/packages/commerce-sdk-react/src/auth/index.ts index 90ed54c298..cb533e8850 100644 --- a/packages/commerce-sdk-react/src/auth/index.ts +++ b/packages/commerce-sdk-react/src/auth/index.ts @@ -36,7 +36,7 @@ interface JWTHeaders { * Plus, the getCustomer endpoint only works for registered user, it returns a 404 for a guest user, * and it's not easy to grab this info in user land, so we add it into the Auth object, and expose it via a hook */ -type AuthData = Prettify< +export type AuthData = Prettify< RemoveStringIndex & { customer_type: CustomerType idp_access_token: string diff --git a/packages/commerce-sdk-react/src/hooks/query.test.tsx b/packages/commerce-sdk-react/src/hooks/query.test.tsx index 48d4fc121e..92996a7cae 100644 --- a/packages/commerce-sdk-react/src/hooks/query.test.tsx +++ b/packages/commerce-sdk-react/src/hooks/query.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, salesforce.com, inc. + * Copyright (c) 2023, 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 @@ -43,165 +43,175 @@ const QUERY_TESTS = [ // ShopperBasket { name: 'useBasket', - hook: () => useBasket({basketId: 'test'}), + hook: () => useBasket({parameters: {basketId: 'test'}}), endpoint: /\/baskets\/test$/ }, { name: 'usePaymentMethodsForBasket', - hook: () => usePaymentMethodsForBasket({basketId: 'test'}), + hook: () => usePaymentMethodsForBasket({parameters: {basketId: 'test'}}), endpoint: /\/baskets\/test\/payment-methods$/ }, { name: 'usePriceBooksForBasket', - hook: () => usePriceBooksForBasket(), + hook: () => usePriceBooksForBasket({parameters: {basketId: 'test'}}), endpoint: new RegExp(''), notImplemented: true }, { name: 'useTaxesFromBasket', - hook: () => useTaxesFromBasket(), + hook: () => useTaxesFromBasket({parameters: {basketId: 'test'}}), endpoint: new RegExp(''), notImplemented: true }, { name: 'useShippingMethodsForShipment', - hook: () => useShippingMethodsForShipment({basketId: 'test', shipmentId: '123'}), + hook: () => + useShippingMethodsForShipment({parameters: {basketId: 'test', shipmentId: '123'}}), endpoint: /\/baskets\/test\/shipments\/123\/shipping-methods$/ }, // ShopperContext { name: 'useShopperContext', - hook: () => useShopperContext(), + hook: () => useShopperContext({parameters: {usid: 'test'}}), endpoint: new RegExp(''), notImplemented: true }, // ShopperCustomers { name: 'useExternalProfile', - hook: () => useExternalProfile(), + hook: () => + useExternalProfile({ + parameters: {externalId: 'externalId', authenticationProviderId: 'authProvider'} + }), endpoint: new RegExp(''), notImplemented: true }, { name: 'useCustomer', - hook: () => useCustomer({customerId: '123'}), + hook: () => useCustomer({parameters: {customerId: '123'}}), endpoint: /\/customers\/123$/ }, { name: 'useCustomerAddress', - hook: () => useCustomerAddress({customerId: '123', addressName: '456'}), + hook: () => useCustomerAddress({parameters: {customerId: '123', addressName: '456'}}), endpoint: /\/customers\/123\/addresses\/456$/ }, { name: 'useCustomerBaskets', - hook: () => useCustomerBaskets({customerId: '123'}), + hook: () => useCustomerBaskets({parameters: {customerId: '123'}}), endpoint: /\/customers\/123\/baskets$/ }, { name: 'useCustomerOrders', - hook: () => useCustomerOrders({customerId: '123'}), + hook: () => useCustomerOrders({parameters: {customerId: '123'}}), endpoint: /\/customers\/123\/orders$/ }, { name: 'useCustomerPaymentInstrument', - hook: () => useCustomerPaymentInstrument(), + hook: () => + useCustomerPaymentInstrument({ + parameters: {customerId: '123', paymentInstrumentId: '456'} + }), endpoint: new RegExp(''), notImplemented: true }, { name: 'useCustomerProductLists', - hook: () => useCustomerProductLists({customerId: '123'}), + hook: () => useCustomerProductLists({parameters: {customerId: '123'}}), endpoint: /\/customers\/123\/product-lists$/ }, { name: 'useCustomerProductList', - hook: () => useCustomerProductList({customerId: '123', listId: '456'}), + hook: () => useCustomerProductList({parameters: {customerId: '123', listId: '456'}}), endpoint: /\/customers\/123\/product-lists\/456$/ }, { name: 'useCustomerProductListItem', - hook: () => useCustomerProductListItem(), + hook: () => + useCustomerProductListItem({ + parameters: {customerId: '123', listId: '456', itemId: '789'} + }), endpoint: new RegExp(''), notImplemented: true }, { name: 'usePublicProductListsBySearchTerm', - hook: () => usePublicProductListsBySearchTerm(), + hook: () => usePublicProductListsBySearchTerm({parameters: {}}), endpoint: new RegExp(''), notImplemented: true }, { name: 'usePublicProductList', - hook: () => usePublicProductList(), + hook: () => usePublicProductList({parameters: {listId: '123'}}), endpoint: new RegExp(''), notImplemented: true }, { name: 'useProductListItem', - hook: () => useProductListItem(), + hook: () => useProductListItem({parameters: {listId: '123', itemId: '456'}}), endpoint: new RegExp(''), notImplemented: true }, // ShopperOrders { name: 'useOrder', - hook: () => useOrder({orderNo: '123'}), + hook: () => useOrder({parameters: {orderNo: '123'}}), endpoint: /\/orders\/123$/ }, { name: 'usePaymentMethodsForOrder', - hook: () => usePaymentMethodsForOrder(), + hook: () => usePaymentMethodsForOrder({parameters: {orderNo: '123'}}), endpoint: new RegExp(''), notImplemented: true }, { name: 'useTaxesFromOrder', - hook: () => useTaxesFromOrder(), + hook: () => useTaxesFromOrder({parameters: {orderNo: '123'}}), endpoint: new RegExp(''), notImplemented: true }, // ShopperProducts { name: 'useProducts', - hook: () => useProducts({ids: '123,456'}), + hook: () => useProducts({parameters: {ids: '123,456'}}), endpoint: /\/products$/ }, { name: 'useProduct', - hook: () => useProduct({id: '123'}), + hook: () => useProduct({parameters: {id: '123'}}), endpoint: /\/products\/123$/ }, { name: 'useCategories', - hook: () => useCategories({ids: '123,456'}), + hook: () => useCategories({parameters: {ids: '123,456'}}), endpoint: /\/categories$/ }, { name: 'useCategory', - hook: () => useCategory({id: '123'}), + hook: () => useCategory({parameters: {id: '123'}}), endpoint: /\/categories\/123$/ }, // ShopperPromotions { name: 'usePromotions', - hook: () => usePromotions({ids: '123,456'}), + hook: () => usePromotions({parameters: {ids: '123,456'}}), endpoint: /\/promotions$/ }, { name: 'usePromotionsForCampaign', - hook: () => usePromotionsForCampaign(), + hook: () => usePromotionsForCampaign({parameters: {campaignId: '123'}}), endpoint: new RegExp(''), notImplemented: true }, // ShopperSearch { name: 'useProductSearch', - hook: () => useProductSearch({q: 'test'}), + hook: () => useProductSearch({parameters: {q: 'test'}}), endpoint: /\/product-search$/ }, { name: 'useSearchSuggestions', - hook: () => useSearchSuggestions({q: 'test'}), + hook: () => useSearchSuggestions({parameters: {q: 'test'}}), endpoint: /\/search-suggestions$/ } ] @@ -215,7 +225,6 @@ test.each(QUERY_TESTS)('%j - 200 returns data', async ({hook, endpoint, notImple nock(DEFAULT_TEST_HOST) .get((uri) => endpoint.test(uri.split('?')[0])) .reply(200, data) - // @ts-ignore const {result, waitForNextUpdate} = renderHookWithProviders(hook) expect(result.current.data).toBe(undefined) expect(result.current.isLoading).toBe(true) @@ -233,7 +242,6 @@ test.each(QUERY_TESTS)('%j - 400 returns error', async ({hook, endpoint, notImpl nock(DEFAULT_TEST_HOST) .get((uri) => endpoint.test(uri.split('?')[0])) .reply(400) - // @ts-ignore const {result, waitForNextUpdate} = renderHookWithProviders(hook) expect(result.current.data).toBe(undefined) expect(result.current.isLoading).toBe(true) From 00ac08186e1f133012aca1147bd400feabd2d3a1 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 13:05:52 -0500 Subject: [PATCH 059/122] Partially fix type errors. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 177 ++++++------- .../hooks/ShopperCustomers/mutation.test.tsx | 248 +++++++++--------- .../src/hooks/ShopperOrders/mutation.test.tsx | 62 ++--- 3 files changed, 226 insertions(+), 261 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 28788ff7b9..f2b5af8630 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -15,14 +15,10 @@ import { mockMutationEndpoints, renderHookWithProviders } from '../../test-utils' -import { - getCacheUpdateMatrix, - ShopperBasketsMutationType, - useShopperBasketsMutation -} from './mutation' +import {ShopperBasketsMutation, useShopperBasketsMutation} from './mutation' import {useBasket} from './query' import {useCustomerBaskets} from '../ShopperCustomers' -import {CacheUpdateMatrixElement} from '../utils' +import {ApiClients, Argument} from '../types' const CUSTOMER_ID = 'CUSTOMER_ID' const BASKET_ID = 'BASKET_ID' @@ -43,7 +39,7 @@ jest.mock('../useCustomerId.ts', () => { }) type MutationPayloads = { - [key in ShopperBasketsMutationType]?: {body: any; parameters: any} + [Mutation in ShopperBasketsMutation]?: Argument } const mutationPayloads: MutationPayloads = { updateBasket: { @@ -55,8 +51,7 @@ const mutationPayloads: MutationPayloads = { body: {} }, deleteBasket: { - parameters: {basketId: BASKET_ID}, - body: {} + parameters: {basketId: BASKET_ID} }, addCouponToBasket: { parameters: {basketId: BASKET_ID}, @@ -64,11 +59,10 @@ const mutationPayloads: MutationPayloads = { }, addItemToBasket: { parameters: {basketId: BASKET_ID}, - body: {productId: PRODUCT_ID} + body: [{productId: PRODUCT_ID}] }, removeItemFromBasket: { - parameters: {basketId: BASKET_ID, itemId: ITEM_ID}, - body: {} + parameters: {basketId: BASKET_ID, itemId: ITEM_ID} }, addPaymentInstrumentToBasket: { parameters: {basketId: BASKET_ID}, @@ -79,16 +73,13 @@ const mutationPayloads: MutationPayloads = { body: {} }, mergeBasket: { - parameters: {}, - body: {} + parameters: {} }, removeCouponFromBasket: { - parameters: {basketId: BASKET_ID, couponItemId: COUPON_ID}, - body: {} + parameters: {basketId: BASKET_ID, couponItemId: COUPON_ID} }, removePaymentInstrumentFromBasket: { - parameters: {basketId: BASKET_ID, paymentInstrumentId: PAYMENT_INSTRUMENT_ID}, - body: {} + parameters: {basketId: BASKET_ID, paymentInstrumentId: PAYMENT_INSTRUMENT_ID} }, updateCustomerForBasket: { parameters: {basketId: BASKET_ID}, @@ -130,96 +121,92 @@ const newBasket = { basketId: BASKET_ID, hello: 'world_modified' } -const tests = (Object.keys(mutationPayloads) as ShopperBasketsMutationType[]).map( - (mutationName) => { - const payload = mutationPayloads[mutationName] - - return { - hook: mutationName, - cases: [ - { - name: 'success', - assertions: async () => { - mockMutationEndpoints( - '/checkout/shopper-baskets/', - {errorResponse: 200}, - newBasket - ) - mockRelatedQueries() - - const {result, waitForValueToChange} = renderHookWithProviders(() => { - const action = mutationName as ShopperBasketsMutationType - const mutation = useShopperBasketsMutation({action}) - - // All of the necessary query hooks needed to verify the cache-update logic - const queries = { - basket: useBasket({basketId: BASKET_ID}), - customerBaskets: useCustomerBaskets({customerId: CUSTOMER_ID}) - } +const tests = (Object.keys(mutationPayloads) as ShopperBasketsMutation[]).map((mutationName) => { + const payload = mutationPayloads[mutationName] + + return { + hook: mutationName, + cases: [ + { + name: 'success', + assertions: async () => { + mockMutationEndpoints( + '/checkout/shopper-baskets/', + {errorResponse: 200}, + newBasket + ) + mockRelatedQueries() + + const {result, waitForValueToChange} = renderHookWithProviders(() => { + const action = mutationName as ShopperBasketsMutation + const mutation = useShopperBasketsMutation(action) + + // All of the necessary query hooks needed to verify the cache-update logic + const queries = { + basket: useBasket({parameters: {basketId: BASKET_ID}}), + customerBaskets: useCustomerBaskets({ + parameters: {customerId: CUSTOMER_ID} + }) + } - return { - queries, - mutation - } - }) + return { + queries, + mutation + } + }) - await waitForValueToChange(() => result.current.queries.basket.data) + await waitForValueToChange(() => result.current.queries.basket.data) - act(() => { - result.current.mutation.mutate(payload) - }) + act(() => { + result.current.mutation.mutate(payload!) + }) - await waitForValueToChange(() => result.current.mutation.isSuccess) - expect(result.current.mutation.isSuccess).toBe(true) - // On successful mutation, the query cache gets updated too. Let's assert it. - const cacheUpdateMatrix = getCacheUpdateMatrix(CUSTOMER_ID) - // @ts-ignore - const matrixElement = cacheUpdateMatrix[mutationName](payload, {}) - const {invalidate, update, remove}: CacheUpdateMatrixElement = matrixElement + await waitForValueToChange(() => result.current.mutation.isSuccess) + expect(result.current.mutation.isSuccess).toBe(true) + // On successful mutation, the query cache gets updated too. Let's assert it. + const cacheUpdateMatrix = getCacheUpdateMatrix(CUSTOMER_ID) + const matrixElement = cacheUpdateMatrix[mutationName](payload, {}) + const {invalidate, update, remove}: CacheUpdateMatrixElement = matrixElement - const assertionData = { - basket: newBasket, - customerBaskets: newCustomerBaskets - } - update?.forEach(({name}) => { - // @ts-ignore - assertUpdateQuery(result.current.queries[name], assertionData[name]) - }) + const assertionData = { + basket: newBasket, + customerBaskets: newCustomerBaskets + } + update?.forEach(({name}) => { + assertUpdateQuery(result.current.queries[name], assertionData[name]) + }) - invalidate?.forEach(({name}) => { - // @ts-ignore - assertInvalidateQuery(result.current.queries[name], oldCustomerBaskets) - }) + invalidate?.forEach(({name}) => { + assertInvalidateQuery(result.current.queries[name], oldCustomerBaskets) + }) - remove?.forEach(({name}) => { - // @ts-ignore - assertRemoveQuery(result.current.queries[name]) - }) - } - }, - { - name: 'error', - assertions: async () => { - mockMutationEndpoints('/checkout/shopper-baskets/', {errorResponse: 500}) + remove?.forEach(({name}) => { + assertRemoveQuery(result.current.queries[name]) + }) + } + }, + { + name: 'error', + assertions: async () => { + mockMutationEndpoints('/checkout/shopper-baskets/', {errorResponse: 500}) - const {result, waitForNextUpdate} = renderHookWithProviders(() => { - const action = mutationName as ShopperBasketsMutationType - return useShopperBasketsMutation({action}) - }) + const {result, waitForNextUpdate} = renderHookWithProviders(() => { + const action = mutationName + return useShopperBasketsMutation(action) + }) - act(() => { - result.current.mutate(payload) - }) + act(() => { + result.current.mutate(payload) + }) - await waitForNextUpdate() + await waitForNextUpdate() - expect(result.current.error).toBeDefined() - } + expect(result.current.error).toBeDefined() } - ] - } + } + ] } -) +}) tests.forEach(({hook, cases}) => { describe(hook, () => { diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx index 5deafda214..1fdca6a8db 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx @@ -7,15 +7,9 @@ import React from 'react' import {renderWithProviders, createQueryClient, DEFAULT_TEST_HOST} from '../../test-utils' import {fireEvent, screen, waitFor} from '@testing-library/react' -import { - ShopperCustomersMutationType, - useShopperCustomersMutation, - shopperCustomersCacheUpdateMatrix, - SHOPPER_CUSTOMERS_NOT_IMPLEMENTED -} from './mutation' +import {ShopperCustomersMutation, useShopperCustomersMutation} from './mutation' import nock from 'nock' -import {QueryKey} from '@tanstack/react-query' -import {QueryKeyMap} from '../utils' +import {ApiClients, Argument} from '../types' jest.mock('../../auth/index.ts', () => { return jest.fn().mockImplementation(() => ({ @@ -31,7 +25,7 @@ const PAYMENT_INSTRUMENT_ID = 'PAYMENT_INSTRUMENT_ID' const PRODUCT_ID = 'PRODUCT_ID' type MutationPayloads = { - [key in ShopperCustomersMutationType]?: {body: any; parameters: any} + [Mutation in ShopperCustomersMutation]?: Argument } const mutationPayloads: MutationPayloads = { @@ -51,7 +45,6 @@ const mutationPayloads: MutationPayloads = { }, removeCustomerAddress: { - body: {}, parameters: {customerId: CUSTOMER_ID, addressName: `TestNewAddress`} }, @@ -71,7 +64,6 @@ const mutationPayloads: MutationPayloads = { }, deleteCustomerProductListItem: { - body: {}, parameters: {customerId: CUSTOMER_ID, listId: LIST_ID, itemId: ITEM_ID} }, @@ -94,16 +86,15 @@ const mutationPayloads: MutationPayloads = { parameters: {customerId: CUSTOMER_ID} }, deleteCustomerPaymentInstrument: { - body: {}, parameters: {customerId: CUSTOMER_ID, paymentInstrumentId: PAYMENT_INSTRUMENT_ID} } } interface CustomerMutationComponentParams { - action: ShopperCustomersMutationType + action: ShopperCustomersMutation } const CustomerMutationComponent = ({action}: CustomerMutationComponentParams) => { - const mutationHook = useShopperCustomersMutation({action}) + const mutationHook = useShopperCustomersMutation(action) return ( <> @@ -121,131 +112,125 @@ const CustomerMutationComponent = ({action}: CustomerMutationComponentParams) => ) } -const tests = (Object.keys(mutationPayloads) as ShopperCustomersMutationType[]).map( - (mutationName) => { - return { - hook: mutationName, - cases: [ - { - name: 'success', - assertions: async () => { - nock(DEFAULT_TEST_HOST) - .patch((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(200, {}) - .put((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(200, {}) - .post((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(200, {}) - .delete((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(204, {}) - - const queryClient = createQueryClient() - - renderWithProviders( - , - { - queryClient - } - ) - await waitFor(() => - screen.getByRole('button', { - name: mutationName - }) - ) - - const mutation: any = shopperCustomersCacheUpdateMatrix[mutationName] - - // Pre-populate cache with query keys we invalidate/update/remove onSuccess - const {invalidate, update, remove} = mutation( - mutationPayloads[mutationName], - {} - ) - - const queryKeys = [ - ...(invalidate || []), - ...(update || []), - ...(remove || []) - ] - - queryKeys.forEach(({key: queryKey}: QueryKeyMap) => { - queryClient.setQueryData(queryKey, () => ({test: true})) +const tests = (Object.keys(mutationPayloads) as ShopperCustomersMutation[]).map((mutationName) => { + return { + hook: mutationName, + cases: [ + { + name: 'success', + assertions: async () => { + nock(DEFAULT_TEST_HOST) + .patch((uri) => { + return uri.includes('/customer/shopper-customers/') }) - - const button = screen.getByRole('button', { + .reply(200, {}) + .put((uri) => { + return uri.includes('/customer/shopper-customers/') + }) + .reply(200, {}) + .post((uri) => { + return uri.includes('/customer/shopper-customers/') + }) + .reply(200, {}) + .delete((uri) => { + return uri.includes('/customer/shopper-customers/') + }) + .reply(204, {}) + + const queryClient = createQueryClient() + + renderWithProviders( + , + { + queryClient + } + ) + await waitFor(() => + screen.getByRole('button', { name: mutationName }) - - fireEvent.click(button) - await waitFor(() => screen.getByText(/isSuccess/i)) - expect(screen.getByText(/isSuccess/i)).toBeInTheDocument() - - // Assert changes in cache - update?.forEach(({key: queryKey}: QueryKeyMap) => { - expect(queryClient.getQueryData(queryKey)).not.toEqual({test: true}) + ) + + const mutation = shopperCustomersCacheUpdateMatrix[mutationName] + + // Pre-populate cache with query keys we invalidate/update/remove onSuccess + const {invalidate, update, remove} = mutation( + mutationPayloads[mutationName], + {} + ) + + const queryKeys = [...(invalidate || []), ...(update || []), ...(remove || [])] + + queryKeys.forEach(({key: queryKey}) => { + queryClient.setQueryData(queryKey, () => ({test: true})) + }) + + const button = screen.getByRole('button', { + name: mutationName + }) + + fireEvent.click(button) + await waitFor(() => screen.getByText(/isSuccess/i)) + expect(screen.getByText(/isSuccess/i)).toBeInTheDocument() + + // Assert changes in cache + update?.forEach(({key: queryKey}) => { + expect(queryClient.getQueryData(queryKey)).not.toEqual({test: true}) + }) + invalidate?.forEach(({key: queryKey}) => { + expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBeTruthy() + }) + remove?.forEach(({key: queryKey}) => { + expect(queryClient.getQueryState(queryKey)).toBeFalsy() + }) + } + }, + { + name: 'error', + assertions: async () => { + nock(DEFAULT_TEST_HOST) + .patch((uri) => { + return uri.includes('/customer/shopper-customers/') }) - invalidate?.forEach(({key: queryKey}: QueryKeyMap) => { - expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBeTruthy() + .reply(500, {}) + .put((uri) => { + return uri.includes('/customer/shopper-customers/') }) - remove?.forEach(({key: queryKey}: QueryKeyMap) => { - expect(queryClient.getQueryState(queryKey)).toBeFalsy() + .reply(500, {}) + .post((uri) => { + return uri.includes('/customer/shopper-customers/') }) - } - }, - { - name: 'error', - assertions: async () => { - nock(DEFAULT_TEST_HOST) - .patch((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - .put((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - .post((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - .delete((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - - renderWithProviders( - - ) - await waitFor(() => - screen.getByRole('button', { - name: mutationName - }) - ) - - const button = screen.getByRole('button', { + .reply(500, {}) + .delete((uri) => { + return uri.includes('/customer/shopper-customers/') + }) + .reply(500, {}) + + renderWithProviders( + + ) + await waitFor(() => + screen.getByRole('button', { name: mutationName }) + ) + + const button = screen.getByRole('button', { + name: mutationName + }) - fireEvent.click(button) - await waitFor(() => screen.getByText(/error/i)) - expect(screen.getByText(/error/i)).toBeInTheDocument() - } + fireEvent.click(button) + await waitFor(() => screen.getByText(/error/i)) + expect(screen.getByText(/error/i)).toBeInTheDocument() } - ] - } + } + ] } -) +}) tests.forEach(({hook, cases}) => { describe(hook, () => { @@ -258,12 +243,13 @@ tests.forEach(({hook, cases}) => { }) }) -test.each(SHOPPER_CUSTOMERS_NOT_IMPLEMENTED)( +// TODO: change to new format +test.each([] as ShopperCustomersMutation[])( '%j - throws error when not implemented', (methodName) => { - const action = methodName as ShopperCustomersMutationType + const action = methodName expect(() => { - useShopperCustomersMutation({action}) + useShopperCustomersMutation(action) }).toThrowError('This method is not implemented.') } ) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx index 4ba3020d5b..e394ba01f9 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, salesforce.com, inc. + * Copyright (c) 2023, 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 @@ -7,15 +7,9 @@ import React from 'react' import {renderWithProviders, DEFAULT_TEST_HOST, createQueryClient} from '../../test-utils' import {fireEvent, screen, waitFor} from '@testing-library/react' -import { - useShopperOrdersMutation, - ShopperOrdersMutationType, - shopperOrdersCacheUpdateMatrix, - SHOPPER_ORDERS_NOT_IMPLEMENTED -} from './mutation' +import {useShopperOrdersMutation, ShopperOrdersMutation} from './mutation' import nock from 'nock' -import {QueryKey} from '@tanstack/react-query' -import {QueryKeyMap} from '../utils' +import {ApiClients, Argument} from '../types' jest.mock('../../auth/index.ts', () => { return jest.fn().mockImplementation(() => ({ @@ -26,7 +20,7 @@ jest.mock('../../auth/index.ts', () => { const BASKET_ID = '12345' type MutationPayloads = { - [key in ShopperOrdersMutationType]?: {body: any; parameters: any} + [Mutation in ShopperOrdersMutation]?: Argument } const mutationPayloads: MutationPayloads = { @@ -37,24 +31,27 @@ const mutationPayloads: MutationPayloads = { } interface OrderMutationComponentParams { - action: ShopperOrdersMutationType + action: ShopperOrdersMutation } const OrderMutationComponent = ({action}: OrderMutationComponentParams) => { - const mutationHook = useShopperOrdersMutation({action}) + const mutationHook = useShopperOrdersMutation(action) + const error = mutationHook.error as Error | undefined + const payload = mutationPayloads[action] + if (!payload) throw new Error(`Missing payload for ${action}`) return (
- + - {mutationHook.error?.message &&

Error: {mutationHook.error?.message}

} + {error?.message &&

Error: {error.message}

}
{mutationHook.isSuccess && isSuccess}
) } -const tests = (Object.keys(mutationPayloads) as ShopperOrdersMutationType[]).map((mutationName) => { +const tests = (Object.keys(mutationPayloads) as ShopperOrdersMutation[]).map((mutationName) => { return { hook: mutationName, cases: [ @@ -69,7 +66,7 @@ const tests = (Object.keys(mutationPayloads) as ShopperOrdersMutationType[]).map const queryClient = createQueryClient() - const mutation: any = shopperOrdersCacheUpdateMatrix[mutationName] + const mutation = shopperOrdersCacheUpdateMatrix[mutationName] const {invalidate, update, remove} = mutation( mutationPayloads[mutationName], @@ -78,14 +75,12 @@ const tests = (Object.keys(mutationPayloads) as ShopperOrdersMutationType[]).map const queryKeys = [...(invalidate || []), ...(update || []), ...(remove || [])] - queryKeys.forEach(({key: queryKey}: QueryKeyMap) => { + queryKeys.forEach(({key: queryKey}) => { queryClient.setQueryData(queryKey, {test: true}) }) renderWithProviders( - , + , {queryClient} ) @@ -104,13 +99,13 @@ const tests = (Object.keys(mutationPayloads) as ShopperOrdersMutationType[]).map expect(screen.getByText(/isSuccess/i)).toBeInTheDocument() // Assert changes in cache - update?.forEach(({key: queryKey}: QueryKeyMap) => { + update?.forEach(({key: queryKey}) => { expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBeFalsy() }) - invalidate?.forEach(({key: queryKey}: QueryKeyMap) => { + invalidate?.forEach(({key: queryKey}) => { expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBeTruthy() }) - remove?.forEach(({key: queryKey}: QueryKeyMap) => { + remove?.forEach(({key: queryKey}) => { expect(queryClient.getQueryState(queryKey)).toBeFalsy() }) } @@ -125,9 +120,7 @@ const tests = (Object.keys(mutationPayloads) as ShopperOrdersMutationType[]).map .reply(500) renderWithProviders( - + ) await waitFor(() => screen.getByRole('button', { @@ -158,12 +151,11 @@ tests.forEach(({hook, cases}) => { }) }) -test.each(SHOPPER_ORDERS_NOT_IMPLEMENTED)( - '%j - throws error when not implemented', - (methodName) => { - const action = methodName as ShopperOrdersMutationType - expect(() => { - useShopperOrdersMutation({action}) - }).toThrowError('This method is not implemented.') - } -) +// TODO: Methods that haven't been implemented are no longer an explicit list, +// but are implicitly derived from their absence in the `cacheUpdateMatrix` of implementations. +test.each([])('%j - throws error when not implemented', (methodName) => { + const action = methodName as ShopperOrdersMutation + expect(() => { + useShopperOrdersMutation(action) + }).toThrowError('This method is not implemented.') +}) From 11fd9eb259fac0651182ab7b871242ec49ebd3cb Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 14:05:16 -0500 Subject: [PATCH 060/122] Update Shopper Baskets cache logic to use narrower parameters. --- .../src/hooks/ShopperBaskets/cache.ts | 99 ++++++++++++------- 1 file changed, 63 insertions(+), 36 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index 4e487b567e..ab0f7fe738 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -4,33 +4,43 @@ * 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 {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' import { - ApiClients, - ApiOptions, - CacheUpdate, - CacheUpdateMatrix, - CacheUpdateUpdate, - MergedOptions -} from '../types' -import {and, matchesApiConfig, matchesPath} from '../utils' + ShopperBaskets, + ShopperBasketsTypes, + ShopperCustomers, + ShopperCustomersTypes +} from 'commerce-sdk-isomorphic' +import {ApiClients, Argument, CacheUpdate, CacheUpdateMatrix, MergedOptions} from '../types' +import {and, matchesApiConfig, matchesPath, pick} from '../utils' type Client = ApiClients['shopperBaskets'] +/** Data returned by every Shopper Baskets endpoint (except `deleteBasket`) */ type Basket = ShopperBasketsTypes.Basket -type BasketParameters = MergedOptions>['parameters'] +/** Data returned by `getCustomerBaskets` */ type CustomerBasketsResult = ShopperCustomersTypes.BasketsResult +/** Parameters that get passed around, includes client config and possible parameters from other endpoints */ +type BasketParameters = MergedOptions>['parameters'] +/** Parameters that we actually send to the API for `getBasket` */ +type GetBasketParameters = Argument['getBasket']>['parameters'] +/** Parameters that we actually send to the API for `getCustomerBaskets` */ +type GetCustomerBasketsParameters = Argument< + ShopperCustomers<{shortCode: string}>['getCustomerBaskets'] +>['parameters'] // Path helpers to avoid typos! -const getBasePath = (parameters: BasketParameters) => ['/organizations/', parameters.organizationId] -const getBasketPath = (parameters: BasketParameters & {basketId: string}) => [ +const getBasePath = (parameters: {organizationId: string}) => [ + '/organizations/', + parameters.organizationId +] +const getBasketPath = (parameters: GetBasketParameters) => [ ...getBasePath(parameters), '/baskets/', parameters.basketId ] -const getCustomerBasketsPath = (customerId: string, parameters: BasketParameters) => [ +const getCustomerBasketsPath = (parameters: GetCustomerBasketsParameters) => [ ...getBasePath(parameters), '/customers/', - customerId, + parameters.customerId, '/baskets' // No trailing / as it's an aggregate endpoint ] @@ -39,21 +49,32 @@ const updateBasketQuery = ( parameters: BasketParameters, newBasket: Basket ): CacheUpdate => { - // If we just check `!parameters.basketId`, then TypeScript doesn't infer that the value is - // not `undefined`. We have to use this slightly convoluted predicate to give it that info. - const hasBasketId = (p: {basketId?: string}): p is {basketId: string} => Boolean(p.basketId) - if (!hasBasketId(parameters)) return {} - const updateBasket: CacheUpdateUpdate = { - // TODO: This method is used by multiple endpoints, so `parameters` could have properties - // that don't match getBasket - queryKey: [...getBasketPath(parameters), parameters] + // The `parameters` received includes the client config and parameters from other endpoints + // so we need to exclude unwanted parameters in the query key + const getBasketParameters: GetBasketParameters = pick(parameters, [ + 'basketId', + 'locale', + 'organizationId', + 'siteId' + ]) + const basketUpdate = { + queryKey: [...getBasketPath(parameters), getBasketParameters] as const } - if (!customerId) return {update: [updateBasket]} - const updateCustomerBaskets: CacheUpdateUpdate = { - // TODO: This method is used by multiple endpoints, so `parameters` could have properties - // that don't match getCustomerBaskets - queryKey: [...getCustomerBasketsPath(customerId, parameters), parameters], + // We can only update customer baskets if we have a customer! + if (!customerId) return {update: [basketUpdate]} + + // Same elision as done for `getBasket` + const getCustomerBasketsParameters: GetCustomerBasketsParameters = { + customerId, + organizationId: parameters.organizationId, + siteId: parameters.siteId + } + const customerBasketsUpdate = { + queryKey: [ + ...getCustomerBasketsPath(getCustomerBasketsParameters), + getCustomerBasketsParameters + ] as const, updater: (oldData?: CustomerBasketsResult): CustomerBasketsResult | undefined => { // do not update if response basket is not part of existing customer baskets if (!oldData?.baskets?.some((basket) => basket.basketId === parameters.basketId)) { @@ -71,19 +92,19 @@ const updateBasketQuery = ( } } } - return {update: [updateBasket, updateCustomerBaskets]} + return {update: [basketUpdate, customerBasketsUpdate]} } const invalidateCustomerBasketsQuery = ( customerId: string | null, - parameters: BasketParameters -): Pick => { + parameters: Omit +): CacheUpdate => { if (!customerId) return {} return { invalidate: [ and( matchesApiConfig(parameters), - matchesPath(getCustomerBasketsPath(customerId, parameters)) + matchesPath(getCustomerBasketsPath({...parameters, customerId})) ) ] } @@ -94,18 +115,23 @@ const updateBasket = ( {parameters}: {parameters: BasketParameters}, response: Basket ): CacheUpdate => ({ + // TODO: We only update the basket from the matching locale; we should also invalidate other locales ...updateBasketQuery(customerId, parameters, response), ...invalidateCustomerBasketsQuery(customerId, parameters) }) const updateBasketWithResponseBasketId = ( customerId: string | null, - {parameters}: {parameters: BasketParameters}, + {parameters}: {parameters: Omit}, response: Basket -): CacheUpdate => ({ - ...updateBasketQuery(customerId, {...parameters, basketId: response.basketId}, response), - ...invalidateCustomerBasketsQuery(customerId, parameters) -}) +): CacheUpdate => { + const {basketId} = response + return { + // TODO: We only update the basket from the matching locale; we should also invalidate other locales + ...(basketId && updateBasketQuery(customerId, {...parameters, basketId}, response)), + ...invalidateCustomerBasketsQuery(customerId, parameters) + } +} export const cacheUpdateMatrix: CacheUpdateMatrix = { addCouponToBasket: updateBasket, @@ -114,6 +140,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { addPaymentInstrumentToBasket: updateBasket, createBasket: updateBasketWithResponseBasketId, deleteBasket: (customerId, {parameters}) => ({ + // TODO: Convert invalidate to an update that removes the matching basket ...invalidateCustomerBasketsQuery(customerId, parameters), remove: [and(matchesApiConfig(parameters), matchesPath(getBasketPath(parameters)))] }), From 382fcb7a760f62af066a9e24ad80a9ceb86f5874 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 14:12:21 -0500 Subject: [PATCH 061/122] Test that all endpoints have hooks. --- .../src/hooks/ShopperBaskets/index.test.ts | 16 ++++++++++ .../commerce-sdk-react/src/test-utils.tsx | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts new file mode 100644 index 0000000000..e9f8b85fb3 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023, 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 {ShopperBaskets} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {ShopperBasketsMutations} from './mutation' +import * as queries from './query' + +describe('Shopper Baskets hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperBaskets, queries, ShopperBasketsMutations) + }) +}) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index 80a027953c..dbe6d1e73e 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -140,3 +140,32 @@ export const assertInvalidateQuery = ( export const assertRemoveQuery = (queryResult: UseQueryResult) => { expect(queryResult.data).not.toBeDefined() } + +const getQueryName = (method: string): string => { + const prefix = /^get|^retrieve/ + // Most query endpoints start with 'get'; replace it with 'use' + if (method.startsWith('get')) return method.replace(/^get/, 'use') + // Shopper Login retrieveCredQualityUserInfo is a special case + if (method.startsWith('retrieve')) return method.replace(/^retrieve/, 'use') + // Otherwise just prefix the method with 'use' and fix the case + return method.replace(/^./, (ltr) => `use${ltr.toUpperCase()}`) +} + +export const expectAllEndpointsHaveHooks = ( + SdkClass: {prototype: object}, + queryHooks: Record, + mutationsEnum: Record +) => { + const unimplemented = new Set(Object.getOwnPropertyNames(SdkClass.prototype)) + // Always present on a class; we can ignore + unimplemented.delete('constructor') + // Names of implemented mutation endpoints exist as values of the enum + Object.values(mutationsEnum).forEach((method) => unimplemented.delete(method)) + // Names of implemented query endpoints have been mangled when converted into hooks + unimplemented.forEach((method) => { + const queryName = getQueryName(method) + if (queryName in queryHooks) unimplemented.delete(method) + }) + // Convert to array for easier comparison / better jest output + expect([...unimplemented]).toEqual([]) +} From 6bf82722c216c34293e84b43d7741ef856a75b07 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 15:29:28 -0500 Subject: [PATCH 062/122] Create new mutation tests for refactored code. --- .../src/hooks/ShopperBaskets/mut.test.ts | 160 ++++++++++++++++++ .../commerce-sdk-react/src/test-utils.tsx | 18 +- 2 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts new file mode 100644 index 0000000000..66b7f0e8a9 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023, 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 {act} from '@testing-library/react' +import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + assertUpdateQuery, + DEFAULT_TEST_CONFIG, + HttpMethod, + mockEndpoint, + renderHookWithProviders +} from '../../test-utils' +import {useCustomerBaskets} from '../ShopperCustomers' +import {ApiClients, Argument} from '../types' +import {ShopperBasketsMutation, useShopperBasketsMutation} from './mutation' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Client = ApiClients['shopperBaskets'] +type Basket = ShopperBasketsTypes.Basket +type BasketsResult = ShopperCustomersTypes.BasketsResult + +/** Create an options object for Shopper Baskets endpoints, with `basketId` pre-filled. */ +const makeOptions = >( + parameters?: Argument['parameters'], + body?: Argument extends {body: infer B} ? B : never +): Argument => ({ + body: body ?? {}, + parameters: {basketId: BASKET_ID, ...parameters} +}) + +// --- getBasket constants --- // +const basketsEndpoint = '/checkout/shopper-baskets/' +const BASKET_ID = 'basket_id' +const getBasketOptions = makeOptions<'getBasket'>() +const oldBasket: Basket = {basketId: BASKET_ID, mockData: 'old basket'} +const newBasket: Basket = {basketId: BASKET_ID, mockData: 'new basket'} +// --- getCustomerBaskets constants --- // +const customersEndpoint = '/customer/shopper-customers/' +const CUSTOMER_ID = 'customer_id' +// Can't use `makeOptions()` here because it's Shopper Customers, not Shopper Baskets +const getCustomerBasketsOptions: Argument = { + parameters: { + customerId: CUSTOMER_ID + } +} +const oldCustomerBaskets: BasketsResult = { + // We aren't implementing the full basket, so we assert to pretend we are + baskets: [{basketId: 'other_basket'}, oldBasket] as BasketsResult['baskets'], + total: 2 +} +const newCustomerBaskets: BasketsResult = { + // We aren't implementing the full basket, so we assert to pretend we are + baskets: [{basketId: 'other_basket'}, newBasket] as BasketsResult['baskets'], + total: 2 +} + +// --- TEST CASES --- // +// This is an object rather than an array to more easily ensure we cover all mutations +const testData: Partial<{ + [Mut in Exclude]: [ + Mut, + HttpMethod, + Argument + ] +}> = { + updateBasket: ['updateBasket', 'patch', makeOptions<'updateBasket'>()], + mergeBasket: ['mergeBasket', 'post', makeOptions()] +} +// This is what we actually use for `test.each` +const testCases = Object.values(testData) + +describe('ShopperBaskets mutations', () => { + const storedCustomerIdKey = `${DEFAULT_TEST_CONFIG.siteId}_customer_id` + beforeAll(() => { + // Make sure we don't accidentally overwrite something + if (window.localStorage.length > 0) { + throw new Error('Unexpected data in local storage.') + } + window.localStorage.setItem(storedCustomerIdKey, CUSTOMER_ID) + }) + afterAll(() => { + window.localStorage.removeItem(storedCustomerIdKey) + }) + + // If we didn't use every request we mocked, we probably missed something! + afterEach(() => expect(nock.pendingMocks()).toEqual([])) + + test.each(testCases)('%s returns data on success', async (mutationName, method, options) => { + mockEndpoint(basketsEndpoint, oldBasket, method) + const {result, waitForValueToChange} = renderHookWithProviders(() => { + return useShopperBasketsMutation(mutationName) + }) + expect(result.current.data).toBeUndefined() + act(() => result.current.mutate(options)) + await waitForValueToChange(() => result.current.data) + expect(result.current.data).toEqual(oldBasket) + }) + test.each(testCases)('%s returns error on error', async (mutationName, method, options) => { + mockEndpoint(basketsEndpoint, {}, method, 400) + const {result, waitForValueToChange} = renderHookWithProviders(() => { + return useShopperBasketsMutation(mutationName) + }) + expect(result.current.error).toBeNull() + act(() => result.current.mutate(options)) + await waitForValueToChange(() => result.current.error) + expect(result.current.error).toBeInstanceOf(Error) + }) + test.each(testCases)( + '%s changes the cache on success', + async (mutationName, method, options) => { + mockEndpoint(basketsEndpoint, oldBasket) // getBasket + mockEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockEndpoint(basketsEndpoint, newBasket, method) // mutation endpoint + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitForValueToChange(() => result.current.basket.data) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + act(() => result.current.mutation.mutate(options)) + await waitForValueToChange(() => result.current.mutation.isSuccess) + assertUpdateQuery(result.current.basket, newBasket) + assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) + } + ) + test.each(testCases)( + '%s does not change cache on error', + async (mutationName, method, options) => { + mockEndpoint(basketsEndpoint, oldBasket) // getBasket + mockEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockEndpoint(basketsEndpoint, {}, method, 400) // mutation endpoint + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitForValueToChange(() => result.current.basket.data) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + act(() => result.current.mutation.mutate(options)) + await waitForValueToChange(() => result.current.mutation.isError) + assertUpdateQuery(result.current.basket, oldBasket) + assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) + } + ) + // TODO: Special case `deleteBasket` +}) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index dbe6d1e73e..e0d0f1f2c8 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -142,11 +142,10 @@ export const assertRemoveQuery = (queryResult: UseQueryResult) => { } const getQueryName = (method: string): string => { + // Most query endpoints start with 'get'; Shopper Login retrieveCredQualityUserInfo also exists const prefix = /^get|^retrieve/ - // Most query endpoints start with 'get'; replace it with 'use' - if (method.startsWith('get')) return method.replace(/^get/, 'use') - // Shopper Login retrieveCredQualityUserInfo is a special case - if (method.startsWith('retrieve')) return method.replace(/^retrieve/, 'use') + // If it exists, replace the prefix verb with 'use' + if (prefix.test(method)) return method.replace(prefix, 'use') // Otherwise just prefix the method with 'use' and fix the case return method.replace(/^./, (ltr) => `use${ltr.toUpperCase()}`) } @@ -169,3 +168,14 @@ export const expectAllEndpointsHaveHooks = ( // Convert to array for easier comparison / better jest output expect([...unimplemented]).toEqual([]) } + +export type HttpMethod = 'delete' | 'get' | 'patch' | 'post' | 'put' +export const mockEndpoint = ( + endpoint: string, + data: string | Record, + method: HttpMethod = 'get', + code = 200 +) => + nock(DEFAULT_TEST_HOST) + [method]((uri) => uri.includes(endpoint)) + .reply(code, data) From 2dd71ecbabcf317b9fa6d3ef5565f31892be7aa2 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 15:48:56 -0500 Subject: [PATCH 063/122] Add test to validate all mutations have cache update logic. --- .../src/hooks/ShopperBaskets/index.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts index e9f8b85fb3..484bd8afbf 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts @@ -6,6 +6,7 @@ */ import {ShopperBaskets} from 'commerce-sdk-isomorphic' import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {cacheUpdateMatrix} from './cache' import {ShopperBasketsMutations} from './mutation' import * as queries from './query' @@ -13,4 +14,11 @@ describe('Shopper Baskets hooks', () => { test('all endpoints have hooks', () => { expectAllEndpointsHaveHooks(ShopperBaskets, queries, ShopperBasketsMutations) }) + + test('all mutation hooks have cache update logic', () => { + // If this test fails, add the missing mutation as a no-op or a TODO that throws NotYetImplemented + const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() + const mutations = Object.values(ShopperBasketsMutations).sort() + expect(cacheUpdates).toEqual(mutations) + }) }) From 17621e455737b905c1f9421a89ffdfe81bf5bd46 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 16:27:29 -0500 Subject: [PATCH 064/122] Alphabetize cache updates. --- packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index ab0f7fe738..26b24b3343 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -136,7 +136,6 @@ const updateBasketWithResponseBasketId = ( export const cacheUpdateMatrix: CacheUpdateMatrix = { addCouponToBasket: updateBasket, addItemToBasket: updateBasket, - removeItemFromBasket: updateBasket, addPaymentInstrumentToBasket: updateBasket, createBasket: updateBasketWithResponseBasketId, deleteBasket: (customerId, {parameters}) => ({ @@ -146,6 +145,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { }), mergeBasket: updateBasketWithResponseBasketId, removeCouponFromBasket: updateBasket, + removeItemFromBasket: updateBasket, removePaymentInstrumentFromBasket: updateBasket, updateBasket: updateBasket, updateBillingAddressForBasket: updateBasket, From d49f16ae480eb2ce40f3b4984c9b4d65d2fec50b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 20 Feb 2023 17:03:18 -0500 Subject: [PATCH 065/122] Simplify mutation tests a bit. --- .../src/hooks/ShopperBaskets/mut.test.ts | 147 ++++++++++-------- .../commerce-sdk-react/src/test-utils.tsx | 53 +++---- 2 files changed, 103 insertions(+), 97 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts index 66b7f0e8a9..c3329be8ac 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts @@ -11,8 +11,8 @@ import nock from 'nock' import { assertUpdateQuery, DEFAULT_TEST_CONFIG, - HttpMethod, - mockEndpoint, + mockMutationEndpoints, + mockQueryEndpoint, renderHookWithProviders } from '../../test-utils' import {useCustomerBaskets} from '../ShopperCustomers' @@ -32,17 +32,17 @@ type BasketsResult = ShopperCustomersTypes.BasketsResult /** Create an options object for Shopper Baskets endpoints, with `basketId` pre-filled. */ const makeOptions = >( - parameters?: Argument['parameters'], - body?: Argument extends {body: infer B} ? B : never + body: Argument extends {body: infer B} ? B : undefined, + parameters?: Omit['parameters'], 'basketId'> ): Argument => ({ - body: body ?? {}, + body, parameters: {basketId: BASKET_ID, ...parameters} }) // --- getBasket constants --- // const basketsEndpoint = '/checkout/shopper-baskets/' const BASKET_ID = 'basket_id' -const getBasketOptions = makeOptions<'getBasket'>() +const getBasketOptions = makeOptions<'getBasket'>(undefined) const oldBasket: Basket = {basketId: BASKET_ID, mockData: 'old basket'} const newBasket: Basket = {basketId: BASKET_ID, mockData: 'new basket'} // --- getCustomerBaskets constants --- // @@ -66,38 +66,59 @@ const newCustomerBaskets: BasketsResult = { } // --- TEST CASES --- // +type NonDeleteMutation = Exclude +// TODO: Remove optional flag when all mutations are implemented +type TestMap = {[Mut in NonDeleteMutation]?: Argument} // This is an object rather than an array to more easily ensure we cover all mutations -const testData: Partial<{ - [Mut in Exclude]: [ - Mut, - HttpMethod, - Argument - ] -}> = { - updateBasket: ['updateBasket', 'patch', makeOptions<'updateBasket'>()], - mergeBasket: ['mergeBasket', 'post', makeOptions()] +const testMap: TestMap = { + // TODO: Figure out why commented-out mutations are failing + addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}), + addItemToBasket: makeOptions<'addItemToBasket'>([]), + addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}), + createBasket: makeOptions<'createBasket'>({}), + mergeBasket: makeOptions<'mergeBasket'>(undefined), + // removeCouponFromBasket: makeOptions<'removeCouponFromBasket'>(undefined), + // removeItemFromBasket: makeOptions<'removeItemFromBasket'>(undefined), + // removePaymentInstrumentFromBasket: makeOptions<'removePaymentInstrumentFromBasket'>(undefined), + updateBasket: makeOptions<'updateBasket'>({}), + updateBillingAddressForBasket: makeOptions<'updateBillingAddressForBasket'>({}), + updateCustomerForBasket: makeOptions<'updateCustomerForBasket'>({email: 'customer@email'}) + // updateItemInBasket: makeOptions<'updateItemInBasket'>({}), + // updatePaymentInstrumentInBasket: makeOptions<'updatePaymentInstrumentInBasket'>({}), + // updateShippingAddressForShipment: makeOptions<'updateShippingAddressForShipment'>({}), + // updateShippingMethodForShipment: makeOptions<'updateShippingMethodForShipment'>({id: 'ship'}) } // This is what we actually use for `test.each` -const testCases = Object.values(testData) +// Type assertion because the built-in type definition for `Object.entries` is limited :\ +const testCases = Object.entries(testMap) as Array< + [NonDeleteMutation, NonNullable] +> describe('ShopperBaskets mutations', () => { const storedCustomerIdKey = `${DEFAULT_TEST_CONFIG.siteId}_customer_id` beforeAll(() => { - // Make sure we don't accidentally overwrite something - if (window.localStorage.length > 0) { - throw new Error('Unexpected data in local storage.') - } + // Make sure we don't accidentally overwrite something before setting up our test state + if (window.localStorage.length > 0) throw new Error('Unexpected data in local storage.') window.localStorage.setItem(storedCustomerIdKey, CUSTOMER_ID) }) afterAll(() => { window.localStorage.removeItem(storedCustomerIdKey) }) - // If we didn't use every request we mocked, we probably missed something! - afterEach(() => expect(nock.pendingMocks()).toEqual([])) + beforeEach(() => { + nock.cleanAll() + expect(nock.pendingMocks()).toEqual([]) + }) + afterEach(() => { + // TODO: The counts are frequently off, is this an incorrect assumption? + // Every mutation could be one of DELETE | PATCH | POST | PUT. We mock them all, so that we + // don't need to know which method a mutation uses, but we want to validate that exactly ONE + // endpoint was used (otherwise something didn't work as expected!) + // expect(nock.pendingMocks().length).toBe(3) + }) - test.each(testCases)('%s returns data on success', async (mutationName, method, options) => { - mockEndpoint(basketsEndpoint, oldBasket, method) + test.each(testCases)('%s returns data on success', async (mutationName, options) => { + mockMutationEndpoints(basketsEndpoint, oldBasket) const {result, waitForValueToChange} = renderHookWithProviders(() => { return useShopperBasketsMutation(mutationName) }) @@ -106,8 +127,8 @@ describe('ShopperBaskets mutations', () => { await waitForValueToChange(() => result.current.data) expect(result.current.data).toEqual(oldBasket) }) - test.each(testCases)('%s returns error on error', async (mutationName, method, options) => { - mockEndpoint(basketsEndpoint, {}, method, 400) + test.each(testCases)('%s returns error on error', async (mutationName, options) => { + mockMutationEndpoints(basketsEndpoint, {}, 400) const {result, waitForValueToChange} = renderHookWithProviders(() => { return useShopperBasketsMutation(mutationName) }) @@ -116,45 +137,39 @@ describe('ShopperBaskets mutations', () => { await waitForValueToChange(() => result.current.error) expect(result.current.error).toBeInstanceOf(Error) }) - test.each(testCases)( - '%s changes the cache on success', - async (mutationName, method, options) => { - mockEndpoint(basketsEndpoint, oldBasket) // getBasket - mockEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets - mockEndpoint(basketsEndpoint, newBasket, method) // mutation endpoint - const {result, waitForValueToChange} = renderHookWithProviders(() => ({ - basket: queries.useBasket(getBasketOptions), - customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), - mutation: useShopperBasketsMutation(mutationName) - })) - await waitForValueToChange(() => result.current.basket.data) - expect(result.current.basket.data).toEqual(oldBasket) - expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) - act(() => result.current.mutation.mutate(options)) - await waitForValueToChange(() => result.current.mutation.isSuccess) - assertUpdateQuery(result.current.basket, newBasket) - assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) - } - ) - test.each(testCases)( - '%s does not change cache on error', - async (mutationName, method, options) => { - mockEndpoint(basketsEndpoint, oldBasket) // getBasket - mockEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets - mockEndpoint(basketsEndpoint, {}, method, 400) // mutation endpoint - const {result, waitForValueToChange} = renderHookWithProviders(() => ({ - basket: queries.useBasket(getBasketOptions), - customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), - mutation: useShopperBasketsMutation(mutationName) - })) - await waitForValueToChange(() => result.current.basket.data) - expect(result.current.basket.data).toEqual(oldBasket) - expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) - act(() => result.current.mutation.mutate(options)) - await waitForValueToChange(() => result.current.mutation.isError) - assertUpdateQuery(result.current.basket, oldBasket) - assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) - } - ) + test.each(testCases)('%s changes the cache on success', async (mutationName, options) => { + mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket + mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockMutationEndpoints(basketsEndpoint, newBasket) // this mutation + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitForValueToChange(() => result.current.basket.data) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + act(() => result.current.mutation.mutate(options)) + await waitForValueToChange(() => result.current.mutation.isSuccess) + assertUpdateQuery(result.current.basket, newBasket) + assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) + }) + test.each(testCases)('%s does not change cache on error', async (mutationName, options) => { + mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket + mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockMutationEndpoints(basketsEndpoint, {}, 400) // this mutation + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitForValueToChange(() => result.current.basket.data) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + act(() => result.current.mutation.mutate(options)) + await waitForValueToChange(() => result.current.mutation.isError) + assertUpdateQuery(result.current.basket, oldBasket) + assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) + }) // TODO: Special case `deleteBasket` }) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index e0d0f1f2c8..b0e24b9a3b 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -93,30 +93,32 @@ export function renderHookWithProviders( }) } +/** Mocks DELETE, PATCH, POST, and PUT so we don't have to look up which verb an endpoint uses. */ export const mockMutationEndpoints = ( matchingPath: string, - options?: {errorResponse: number}, - response = {} + response: string | Record, + statusCode = 200 ) => { - const responseStatus = options?.errorResponse ? options.errorResponse : 200 + const matcher = (uri: string) => uri.includes(matchingPath) + return nock(DEFAULT_TEST_HOST) + .delete(matcher) + .reply(statusCode, response) + .patch(matcher) + .reply(statusCode, response) + .put(matcher) + .reply(statusCode, response) + .post(matcher) + .reply(statusCode, response) +} - nock(DEFAULT_TEST_HOST) - .patch((uri) => { - return uri.includes(matchingPath) - }) - .reply(responseStatus, response) - .put((uri) => { - return uri.includes(matchingPath) - }) - .reply(responseStatus, response) - .post((uri) => { - return uri.includes(matchingPath) - }) - .reply(responseStatus, response) - .delete((uri) => { - return uri.includes(matchingPath) - }) - .reply(responseStatus, response) +/** Mocks a GET request to an endpoint. */ +export const mockQueryEndpoint = ( + matchingPath: string, + response: string | Record, + statusCode = 200 +) => { + const matcher = (uri: string) => uri.includes(matchingPath) + return nock(DEFAULT_TEST_HOST).get(matcher).reply(statusCode, response) } export const assertUpdateQuery = ( @@ -168,14 +170,3 @@ export const expectAllEndpointsHaveHooks = ( // Convert to array for easier comparison / better jest output expect([...unimplemented]).toEqual([]) } - -export type HttpMethod = 'delete' | 'get' | 'patch' | 'post' | 'put' -export const mockEndpoint = ( - endpoint: string, - data: string | Record, - method: HttpMethod = 'get', - code = 200 -) => - nock(DEFAULT_TEST_HOST) - [method]((uri) => uri.includes(endpoint)) - .reply(code, data) From 241cdfecbc03382ada777ceccbba2c73b89714f6 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 12:24:05 -0500 Subject: [PATCH 066/122] Fix nock adding duplicate request mocks. --- .../src/hooks/ShopperBaskets/mut.test.ts | 13 ++++--------- packages/commerce-sdk-react/src/test-utils.tsx | 16 +++++++--------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts index c3329be8ac..1dd8413917 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts @@ -105,16 +105,11 @@ describe('ShopperBaskets mutations', () => { window.localStorage.removeItem(storedCustomerIdKey) }) - beforeEach(() => { - nock.cleanAll() - expect(nock.pendingMocks()).toEqual([]) - }) + beforeEach(() => nock.cleanAll()) afterEach(() => { - // TODO: The counts are frequently off, is this an incorrect assumption? - // Every mutation could be one of DELETE | PATCH | POST | PUT. We mock them all, so that we - // don't need to know which method a mutation uses, but we want to validate that exactly ONE - // endpoint was used (otherwise something didn't work as expected!) - // expect(nock.pendingMocks().length).toBe(3) + // To avoid needing to know which HTTP verb a mutation uses (DELETE/PATCH/POST/PUT), + // we mock them all, and then validate that exactly one was used. + expect(nock.pendingMocks().length).toBe(3) }) test.each(testCases)('%s returns data on success', async (mutationName, options) => { diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index b0e24b9a3b..6077ccb770 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -100,15 +100,13 @@ export const mockMutationEndpoints = ( statusCode = 200 ) => { const matcher = (uri: string) => uri.includes(matchingPath) - return nock(DEFAULT_TEST_HOST) - .delete(matcher) - .reply(statusCode, response) - .patch(matcher) - .reply(statusCode, response) - .put(matcher) - .reply(statusCode, response) - .post(matcher) - .reply(statusCode, response) + // For some reason, re-using scope (i.e. nock() and chained methods) + // results in duplicate mocked requests, which breaks our validation + // of # of requests used. + nock(DEFAULT_TEST_HOST).delete(matcher).reply(statusCode, response) + nock(DEFAULT_TEST_HOST).patch(matcher).reply(statusCode, response) + nock(DEFAULT_TEST_HOST).put(matcher).reply(statusCode, response) + nock(DEFAULT_TEST_HOST).post(matcher).reply(statusCode, response) } /** Mocks a GET request to an endpoint. */ From 859032ce7f19029f547a2352f0c5c913e9e45af2 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 13:01:33 -0500 Subject: [PATCH 067/122] Make failing tests have more helpful failures. Show the hook's error instead of "hook timed out" or nock mismatch. --- .../src/hooks/ShopperBaskets/mut.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts index 1dd8413917..6ff0813714 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts @@ -119,7 +119,9 @@ describe('ShopperBaskets mutations', () => { }) expect(result.current.data).toBeUndefined() act(() => result.current.mutate(options)) - await waitForValueToChange(() => result.current.data) + // Watching `data` and `error` together provides better info for failing tests + await waitForValueToChange(() => result.current.data ?? result.current.error) + expect(result.current.error).toBeNull() expect(result.current.data).toEqual(oldBasket) }) test.each(testCases)('%s returns error on error', async (mutationName, options) => { @@ -145,7 +147,11 @@ describe('ShopperBaskets mutations', () => { expect(result.current.basket.data).toEqual(oldBasket) expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) act(() => result.current.mutation.mutate(options)) - await waitForValueToChange(() => result.current.mutation.isSuccess) + await waitForValueToChange( + // Watching `data` and `error` together provides better info for failing tests + () => result.current.mutation.data ?? result.current.mutation.error + ) + expect(result.current.mutation.error).toBeNull() assertUpdateQuery(result.current.basket, newBasket) assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) }) @@ -162,7 +168,8 @@ describe('ShopperBaskets mutations', () => { expect(result.current.basket.data).toEqual(oldBasket) expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) act(() => result.current.mutation.mutate(options)) - await waitForValueToChange(() => result.current.mutation.isError) + await waitForValueToChange(() => result.current.mutation.error) + expect(result.current.mutation.error).toBeInstanceOf(Error) assertUpdateQuery(result.current.basket, oldBasket) assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) }) From c5ad9c22d46d83629f2658dcc61a706457a7c119 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 13:09:35 -0500 Subject: [PATCH 068/122] Fix tests failing due to missing parameters. --- .../src/hooks/ShopperBaskets/mut.test.ts | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts index 6ff0813714..eb7bf0454b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts @@ -33,7 +33,7 @@ type BasketsResult = ShopperCustomersTypes.BasketsResult /** Create an options object for Shopper Baskets endpoints, with `basketId` pre-filled. */ const makeOptions = >( body: Argument extends {body: infer B} ? B : undefined, - parameters?: Omit['parameters'], 'basketId'> + parameters: Omit['parameters'], 'basketId'> ): Argument => ({ body, parameters: {basketId: BASKET_ID, ...parameters} @@ -42,7 +42,7 @@ const makeOptions = >( // --- getBasket constants --- // const basketsEndpoint = '/checkout/shopper-baskets/' const BASKET_ID = 'basket_id' -const getBasketOptions = makeOptions<'getBasket'>(undefined) +const getBasketOptions = makeOptions<'getBasket'>(undefined, {}) const oldBasket: Basket = {basketId: BASKET_ID, mockData: 'old basket'} const newBasket: Basket = {basketId: BASKET_ID, mockData: 'new basket'} // --- getCustomerBaskets constants --- // @@ -71,22 +71,34 @@ type NonDeleteMutation = Exclude type TestMap = {[Mut in NonDeleteMutation]?: Argument} // This is an object rather than an array to more easily ensure we cover all mutations const testMap: TestMap = { - // TODO: Figure out why commented-out mutations are failing - addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}), - addItemToBasket: makeOptions<'addItemToBasket'>([]), - addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}), - createBasket: makeOptions<'createBasket'>({}), - mergeBasket: makeOptions<'mergeBasket'>(undefined), - // removeCouponFromBasket: makeOptions<'removeCouponFromBasket'>(undefined), - // removeItemFromBasket: makeOptions<'removeItemFromBasket'>(undefined), - // removePaymentInstrumentFromBasket: makeOptions<'removePaymentInstrumentFromBasket'>(undefined), - updateBasket: makeOptions<'updateBasket'>({}), - updateBillingAddressForBasket: makeOptions<'updateBillingAddressForBasket'>({}), - updateCustomerForBasket: makeOptions<'updateCustomerForBasket'>({email: 'customer@email'}) - // updateItemInBasket: makeOptions<'updateItemInBasket'>({}), - // updatePaymentInstrumentInBasket: makeOptions<'updatePaymentInstrumentInBasket'>({}), - // updateShippingAddressForShipment: makeOptions<'updateShippingAddressForShipment'>({}), - // updateShippingMethodForShipment: makeOptions<'updateShippingMethodForShipment'>({id: 'ship'}) + addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}, {}), + addItemToBasket: makeOptions<'addItemToBasket'>([], {}), + addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}, {}), + createBasket: makeOptions<'createBasket'>({}, {}), + mergeBasket: makeOptions<'mergeBasket'>(undefined, {}), + removeCouponFromBasket: makeOptions<'removeCouponFromBasket'>(undefined, { + couponItemId: 'couponIemId' + }), + removeItemFromBasket: makeOptions<'removeItemFromBasket'>(undefined, {itemId: 'itemId'}), + removePaymentInstrumentFromBasket: makeOptions<'removePaymentInstrumentFromBasket'>(undefined, { + paymentInstrumentId: 'paymentInstrumentId' + }), + updateBasket: makeOptions<'updateBasket'>({}, {}), + updateBillingAddressForBasket: makeOptions<'updateBillingAddressForBasket'>({}, {}), + updateCustomerForBasket: makeOptions<'updateCustomerForBasket'>({email: 'customer@email'}, {}), + updateItemInBasket: makeOptions<'updateItemInBasket'>({}, {itemId: 'itemId'}), + updatePaymentInstrumentInBasket: makeOptions<'updatePaymentInstrumentInBasket'>( + {}, + {paymentInstrumentId: 'paymentInstrumentId'} + ), + updateShippingAddressForShipment: makeOptions<'updateShippingAddressForShipment'>( + {}, + {shipmentId: 'shipmentId'} + ), + updateShippingMethodForShipment: makeOptions<'updateShippingMethodForShipment'>( + {id: 'ship'}, + {shipmentId: 'shipmentId'} + ) } // This is what we actually use for `test.each` // Type assertion because the built-in type definition for `Object.entries` is limited :\ From 7696a88cbe6d684a05b905e80f0225595e0b2395 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 13:11:52 -0500 Subject: [PATCH 069/122] Replace old mutation test file with new one. --- .../src/hooks/ShopperBaskets/mut.test.ts | 189 --------- .../src/hooks/ShopperBaskets/mutation.test.ts | 373 ++++++++---------- 2 files changed, 154 insertions(+), 408 deletions(-) delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts deleted file mode 100644 index eb7bf0454b..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mut.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) 2023, 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 {act} from '@testing-library/react' -import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' -import nock from 'nock' -import { - assertUpdateQuery, - DEFAULT_TEST_CONFIG, - mockMutationEndpoints, - mockQueryEndpoint, - renderHookWithProviders -} from '../../test-utils' -import {useCustomerBaskets} from '../ShopperCustomers' -import {ApiClients, Argument} from '../types' -import {ShopperBasketsMutation, useShopperBasketsMutation} from './mutation' -import * as queries from './query' - -jest.mock('../../auth/index.ts', () => { - return jest.fn().mockImplementation(() => ({ - ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) - })) -}) - -type Client = ApiClients['shopperBaskets'] -type Basket = ShopperBasketsTypes.Basket -type BasketsResult = ShopperCustomersTypes.BasketsResult - -/** Create an options object for Shopper Baskets endpoints, with `basketId` pre-filled. */ -const makeOptions = >( - body: Argument extends {body: infer B} ? B : undefined, - parameters: Omit['parameters'], 'basketId'> -): Argument => ({ - body, - parameters: {basketId: BASKET_ID, ...parameters} -}) - -// --- getBasket constants --- // -const basketsEndpoint = '/checkout/shopper-baskets/' -const BASKET_ID = 'basket_id' -const getBasketOptions = makeOptions<'getBasket'>(undefined, {}) -const oldBasket: Basket = {basketId: BASKET_ID, mockData: 'old basket'} -const newBasket: Basket = {basketId: BASKET_ID, mockData: 'new basket'} -// --- getCustomerBaskets constants --- // -const customersEndpoint = '/customer/shopper-customers/' -const CUSTOMER_ID = 'customer_id' -// Can't use `makeOptions()` here because it's Shopper Customers, not Shopper Baskets -const getCustomerBasketsOptions: Argument = { - parameters: { - customerId: CUSTOMER_ID - } -} -const oldCustomerBaskets: BasketsResult = { - // We aren't implementing the full basket, so we assert to pretend we are - baskets: [{basketId: 'other_basket'}, oldBasket] as BasketsResult['baskets'], - total: 2 -} -const newCustomerBaskets: BasketsResult = { - // We aren't implementing the full basket, so we assert to pretend we are - baskets: [{basketId: 'other_basket'}, newBasket] as BasketsResult['baskets'], - total: 2 -} - -// --- TEST CASES --- // -type NonDeleteMutation = Exclude -// TODO: Remove optional flag when all mutations are implemented -type TestMap = {[Mut in NonDeleteMutation]?: Argument} -// This is an object rather than an array to more easily ensure we cover all mutations -const testMap: TestMap = { - addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}, {}), - addItemToBasket: makeOptions<'addItemToBasket'>([], {}), - addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}, {}), - createBasket: makeOptions<'createBasket'>({}, {}), - mergeBasket: makeOptions<'mergeBasket'>(undefined, {}), - removeCouponFromBasket: makeOptions<'removeCouponFromBasket'>(undefined, { - couponItemId: 'couponIemId' - }), - removeItemFromBasket: makeOptions<'removeItemFromBasket'>(undefined, {itemId: 'itemId'}), - removePaymentInstrumentFromBasket: makeOptions<'removePaymentInstrumentFromBasket'>(undefined, { - paymentInstrumentId: 'paymentInstrumentId' - }), - updateBasket: makeOptions<'updateBasket'>({}, {}), - updateBillingAddressForBasket: makeOptions<'updateBillingAddressForBasket'>({}, {}), - updateCustomerForBasket: makeOptions<'updateCustomerForBasket'>({email: 'customer@email'}, {}), - updateItemInBasket: makeOptions<'updateItemInBasket'>({}, {itemId: 'itemId'}), - updatePaymentInstrumentInBasket: makeOptions<'updatePaymentInstrumentInBasket'>( - {}, - {paymentInstrumentId: 'paymentInstrumentId'} - ), - updateShippingAddressForShipment: makeOptions<'updateShippingAddressForShipment'>( - {}, - {shipmentId: 'shipmentId'} - ), - updateShippingMethodForShipment: makeOptions<'updateShippingMethodForShipment'>( - {id: 'ship'}, - {shipmentId: 'shipmentId'} - ) -} -// This is what we actually use for `test.each` -// Type assertion because the built-in type definition for `Object.entries` is limited :\ -const testCases = Object.entries(testMap) as Array< - [NonDeleteMutation, NonNullable] -> - -describe('ShopperBaskets mutations', () => { - const storedCustomerIdKey = `${DEFAULT_TEST_CONFIG.siteId}_customer_id` - beforeAll(() => { - // Make sure we don't accidentally overwrite something before setting up our test state - if (window.localStorage.length > 0) throw new Error('Unexpected data in local storage.') - window.localStorage.setItem(storedCustomerIdKey, CUSTOMER_ID) - }) - afterAll(() => { - window.localStorage.removeItem(storedCustomerIdKey) - }) - - beforeEach(() => nock.cleanAll()) - afterEach(() => { - // To avoid needing to know which HTTP verb a mutation uses (DELETE/PATCH/POST/PUT), - // we mock them all, and then validate that exactly one was used. - expect(nock.pendingMocks().length).toBe(3) - }) - - test.each(testCases)('%s returns data on success', async (mutationName, options) => { - mockMutationEndpoints(basketsEndpoint, oldBasket) - const {result, waitForValueToChange} = renderHookWithProviders(() => { - return useShopperBasketsMutation(mutationName) - }) - expect(result.current.data).toBeUndefined() - act(() => result.current.mutate(options)) - // Watching `data` and `error` together provides better info for failing tests - await waitForValueToChange(() => result.current.data ?? result.current.error) - expect(result.current.error).toBeNull() - expect(result.current.data).toEqual(oldBasket) - }) - test.each(testCases)('%s returns error on error', async (mutationName, options) => { - mockMutationEndpoints(basketsEndpoint, {}, 400) - const {result, waitForValueToChange} = renderHookWithProviders(() => { - return useShopperBasketsMutation(mutationName) - }) - expect(result.current.error).toBeNull() - act(() => result.current.mutate(options)) - await waitForValueToChange(() => result.current.error) - expect(result.current.error).toBeInstanceOf(Error) - }) - test.each(testCases)('%s changes the cache on success', async (mutationName, options) => { - mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket - mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets - mockMutationEndpoints(basketsEndpoint, newBasket) // this mutation - const {result, waitForValueToChange} = renderHookWithProviders(() => ({ - basket: queries.useBasket(getBasketOptions), - customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), - mutation: useShopperBasketsMutation(mutationName) - })) - await waitForValueToChange(() => result.current.basket.data) - expect(result.current.basket.data).toEqual(oldBasket) - expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) - act(() => result.current.mutation.mutate(options)) - await waitForValueToChange( - // Watching `data` and `error` together provides better info for failing tests - () => result.current.mutation.data ?? result.current.mutation.error - ) - expect(result.current.mutation.error).toBeNull() - assertUpdateQuery(result.current.basket, newBasket) - assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) - }) - test.each(testCases)('%s does not change cache on error', async (mutationName, options) => { - mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket - mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets - mockMutationEndpoints(basketsEndpoint, {}, 400) // this mutation - const {result, waitForValueToChange} = renderHookWithProviders(() => ({ - basket: queries.useBasket(getBasketOptions), - customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), - mutation: useShopperBasketsMutation(mutationName) - })) - await waitForValueToChange(() => result.current.basket.data) - expect(result.current.basket.data).toEqual(oldBasket) - expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) - act(() => result.current.mutation.mutate(options)) - await waitForValueToChange(() => result.current.mutation.error) - expect(result.current.mutation.error).toBeInstanceOf(Error) - assertUpdateQuery(result.current.basket, oldBasket) - assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) - }) - // TODO: Special case `deleteBasket` -}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index f2b5af8630..eb7bf0454b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -1,254 +1,189 @@ /* - * Copyright (c) 2022, Salesforce, Inc. + * Copyright (c) 2023, 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 {act} from '@testing-library/react' +import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' import nock from 'nock' import { - assertInvalidateQuery, - assertRemoveQuery, assertUpdateQuery, - DEFAULT_TEST_HOST, + DEFAULT_TEST_CONFIG, mockMutationEndpoints, + mockQueryEndpoint, renderHookWithProviders } from '../../test-utils' -import {ShopperBasketsMutation, useShopperBasketsMutation} from './mutation' -import {useBasket} from './query' import {useCustomerBaskets} from '../ShopperCustomers' import {ApiClients, Argument} from '../types' - -const CUSTOMER_ID = 'CUSTOMER_ID' -const BASKET_ID = 'BASKET_ID' -const COUPON_ID = 'COUPON_ID' -const PRODUCT_ID = 'PRODUCT_ID' -const ITEM_ID = 'ITEM_ID' -const PAYMENT_INSTRUMENT_ID = 'PAYMENT_INSTRUMENT_ID' -const SHIPMENT_ID = 'SHIPMENT_ID' +import {ShopperBasketsMutation, useShopperBasketsMutation} from './mutation' +import * as queries from './query' jest.mock('../../auth/index.ts', () => { return jest.fn().mockImplementation(() => ({ - ready: jest.fn().mockResolvedValue({access_token: '123'}) + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) })) }) -jest.mock('../useCustomerId.ts', () => { - return jest.fn().mockReturnValue(CUSTOMER_ID) +type Client = ApiClients['shopperBaskets'] +type Basket = ShopperBasketsTypes.Basket +type BasketsResult = ShopperCustomersTypes.BasketsResult + +/** Create an options object for Shopper Baskets endpoints, with `basketId` pre-filled. */ +const makeOptions = >( + body: Argument extends {body: infer B} ? B : undefined, + parameters: Omit['parameters'], 'basketId'> +): Argument => ({ + body, + parameters: {basketId: BASKET_ID, ...parameters} }) -type MutationPayloads = { - [Mutation in ShopperBasketsMutation]?: Argument -} -const mutationPayloads: MutationPayloads = { - updateBasket: { - parameters: {basketId: BASKET_ID}, - body: {} - }, - updateBillingAddressForBasket: { - parameters: {basketId: BASKET_ID}, - body: {} - }, - deleteBasket: { - parameters: {basketId: BASKET_ID} - }, - addCouponToBasket: { - parameters: {basketId: BASKET_ID}, - body: {code: COUPON_ID} - }, - addItemToBasket: { - parameters: {basketId: BASKET_ID}, - body: [{productId: PRODUCT_ID}] - }, - removeItemFromBasket: { - parameters: {basketId: BASKET_ID, itemId: ITEM_ID} - }, - addPaymentInstrumentToBasket: { - parameters: {basketId: BASKET_ID}, - body: {paymentInstrumentId: PAYMENT_INSTRUMENT_ID} - }, - createBasket: { - parameters: {}, - body: {} - }, - mergeBasket: { - parameters: {} - }, - removeCouponFromBasket: { - parameters: {basketId: BASKET_ID, couponItemId: COUPON_ID} - }, - removePaymentInstrumentFromBasket: { - parameters: {basketId: BASKET_ID, paymentInstrumentId: PAYMENT_INSTRUMENT_ID} - }, - updateCustomerForBasket: { - parameters: {basketId: BASKET_ID}, - body: {email: 'alex@test.com'} - }, - updateItemInBasket: { - parameters: {basketId: BASKET_ID, itemId: ITEM_ID}, - body: {} - }, - updatePaymentInstrumentInBasket: { - parameters: {basketId: BASKET_ID, paymentInstrumentId: PAYMENT_INSTRUMENT_ID}, - body: {} - }, - updateShippingAddressForShipment: { - parameters: {basketId: BASKET_ID, shipmentId: SHIPMENT_ID}, - body: {} - }, - updateShippingMethodForShipment: { - parameters: {basketId: BASKET_ID, shipmentId: SHIPMENT_ID}, - body: {id: '001'} +// --- getBasket constants --- // +const basketsEndpoint = '/checkout/shopper-baskets/' +const BASKET_ID = 'basket_id' +const getBasketOptions = makeOptions<'getBasket'>(undefined, {}) +const oldBasket: Basket = {basketId: BASKET_ID, mockData: 'old basket'} +const newBasket: Basket = {basketId: BASKET_ID, mockData: 'new basket'} +// --- getCustomerBaskets constants --- // +const customersEndpoint = '/customer/shopper-customers/' +const CUSTOMER_ID = 'customer_id' +// Can't use `makeOptions()` here because it's Shopper Customers, not Shopper Baskets +const getCustomerBasketsOptions: Argument = { + parameters: { + customerId: CUSTOMER_ID } } -const oldCustomerBaskets = { - total: 1, - baskets: [{basketId: BASKET_ID, hello: 'world'}] -} - -const newCustomerBaskets = { - total: 1, - baskets: [{basketId: BASKET_ID, hello: 'world_modified'}] +const oldCustomerBaskets: BasketsResult = { + // We aren't implementing the full basket, so we assert to pretend we are + baskets: [{basketId: 'other_basket'}, oldBasket] as BasketsResult['baskets'], + total: 2 } - -const oldBasket = { - basketId: BASKET_ID, - hello: 'world' +const newCustomerBaskets: BasketsResult = { + // We aren't implementing the full basket, so we assert to pretend we are + baskets: [{basketId: 'other_basket'}, newBasket] as BasketsResult['baskets'], + total: 2 } -const newBasket = { - basketId: BASKET_ID, - hello: 'world_modified' +// --- TEST CASES --- // +type NonDeleteMutation = Exclude +// TODO: Remove optional flag when all mutations are implemented +type TestMap = {[Mut in NonDeleteMutation]?: Argument} +// This is an object rather than an array to more easily ensure we cover all mutations +const testMap: TestMap = { + addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}, {}), + addItemToBasket: makeOptions<'addItemToBasket'>([], {}), + addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}, {}), + createBasket: makeOptions<'createBasket'>({}, {}), + mergeBasket: makeOptions<'mergeBasket'>(undefined, {}), + removeCouponFromBasket: makeOptions<'removeCouponFromBasket'>(undefined, { + couponItemId: 'couponIemId' + }), + removeItemFromBasket: makeOptions<'removeItemFromBasket'>(undefined, {itemId: 'itemId'}), + removePaymentInstrumentFromBasket: makeOptions<'removePaymentInstrumentFromBasket'>(undefined, { + paymentInstrumentId: 'paymentInstrumentId' + }), + updateBasket: makeOptions<'updateBasket'>({}, {}), + updateBillingAddressForBasket: makeOptions<'updateBillingAddressForBasket'>({}, {}), + updateCustomerForBasket: makeOptions<'updateCustomerForBasket'>({email: 'customer@email'}, {}), + updateItemInBasket: makeOptions<'updateItemInBasket'>({}, {itemId: 'itemId'}), + updatePaymentInstrumentInBasket: makeOptions<'updatePaymentInstrumentInBasket'>( + {}, + {paymentInstrumentId: 'paymentInstrumentId'} + ), + updateShippingAddressForShipment: makeOptions<'updateShippingAddressForShipment'>( + {}, + {shipmentId: 'shipmentId'} + ), + updateShippingMethodForShipment: makeOptions<'updateShippingMethodForShipment'>( + {id: 'ship'}, + {shipmentId: 'shipmentId'} + ) } -const tests = (Object.keys(mutationPayloads) as ShopperBasketsMutation[]).map((mutationName) => { - const payload = mutationPayloads[mutationName] - - return { - hook: mutationName, - cases: [ - { - name: 'success', - assertions: async () => { - mockMutationEndpoints( - '/checkout/shopper-baskets/', - {errorResponse: 200}, - newBasket - ) - mockRelatedQueries() - - const {result, waitForValueToChange} = renderHookWithProviders(() => { - const action = mutationName as ShopperBasketsMutation - const mutation = useShopperBasketsMutation(action) - - // All of the necessary query hooks needed to verify the cache-update logic - const queries = { - basket: useBasket({parameters: {basketId: BASKET_ID}}), - customerBaskets: useCustomerBaskets({ - parameters: {customerId: CUSTOMER_ID} - }) - } - - return { - queries, - mutation - } - }) - - await waitForValueToChange(() => result.current.queries.basket.data) - - act(() => { - result.current.mutation.mutate(payload!) - }) - - await waitForValueToChange(() => result.current.mutation.isSuccess) - expect(result.current.mutation.isSuccess).toBe(true) - // On successful mutation, the query cache gets updated too. Let's assert it. - const cacheUpdateMatrix = getCacheUpdateMatrix(CUSTOMER_ID) - const matrixElement = cacheUpdateMatrix[mutationName](payload, {}) - const {invalidate, update, remove}: CacheUpdateMatrixElement = matrixElement - - const assertionData = { - basket: newBasket, - customerBaskets: newCustomerBaskets - } - update?.forEach(({name}) => { - assertUpdateQuery(result.current.queries[name], assertionData[name]) - }) - - invalidate?.forEach(({name}) => { - assertInvalidateQuery(result.current.queries[name], oldCustomerBaskets) - }) - - remove?.forEach(({name}) => { - assertRemoveQuery(result.current.queries[name]) - }) - } - }, - { - name: 'error', - assertions: async () => { - mockMutationEndpoints('/checkout/shopper-baskets/', {errorResponse: 500}) - - const {result, waitForNextUpdate} = renderHookWithProviders(() => { - const action = mutationName - return useShopperBasketsMutation(action) - }) - - act(() => { - result.current.mutate(payload) - }) - - await waitForNextUpdate() +// This is what we actually use for `test.each` +// Type assertion because the built-in type definition for `Object.entries` is limited :\ +const testCases = Object.entries(testMap) as Array< + [NonDeleteMutation, NonNullable] +> + +describe('ShopperBaskets mutations', () => { + const storedCustomerIdKey = `${DEFAULT_TEST_CONFIG.siteId}_customer_id` + beforeAll(() => { + // Make sure we don't accidentally overwrite something before setting up our test state + if (window.localStorage.length > 0) throw new Error('Unexpected data in local storage.') + window.localStorage.setItem(storedCustomerIdKey, CUSTOMER_ID) + }) + afterAll(() => { + window.localStorage.removeItem(storedCustomerIdKey) + }) - expect(result.current.error).toBeDefined() - } - } - ] - } -}) + beforeEach(() => nock.cleanAll()) + afterEach(() => { + // To avoid needing to know which HTTP verb a mutation uses (DELETE/PATCH/POST/PUT), + // we mock them all, and then validate that exactly one was used. + expect(nock.pendingMocks().length).toBe(3) + }) -tests.forEach(({hook, cases}) => { - describe(hook, () => { - beforeEach(() => { - jest.clearAllMocks() + test.each(testCases)('%s returns data on success', async (mutationName, options) => { + mockMutationEndpoints(basketsEndpoint, oldBasket) + const {result, waitForValueToChange} = renderHookWithProviders(() => { + return useShopperBasketsMutation(mutationName) }) - cases.forEach(({name, assertions}) => { - test(name, assertions) + expect(result.current.data).toBeUndefined() + act(() => result.current.mutate(options)) + // Watching `data` and `error` together provides better info for failing tests + await waitForValueToChange(() => result.current.data ?? result.current.error) + expect(result.current.error).toBeNull() + expect(result.current.data).toEqual(oldBasket) + }) + test.each(testCases)('%s returns error on error', async (mutationName, options) => { + mockMutationEndpoints(basketsEndpoint, {}, 400) + const {result, waitForValueToChange} = renderHookWithProviders(() => { + return useShopperBasketsMutation(mutationName) }) + expect(result.current.error).toBeNull() + act(() => result.current.mutate(options)) + await waitForValueToChange(() => result.current.error) + expect(result.current.error).toBeInstanceOf(Error) + }) + test.each(testCases)('%s changes the cache on success', async (mutationName, options) => { + mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket + mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockMutationEndpoints(basketsEndpoint, newBasket) // this mutation + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitForValueToChange(() => result.current.basket.data) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + act(() => result.current.mutation.mutate(options)) + await waitForValueToChange( + // Watching `data` and `error` together provides better info for failing tests + () => result.current.mutation.data ?? result.current.mutation.error + ) + expect(result.current.mutation.error).toBeNull() + assertUpdateQuery(result.current.basket, newBasket) + assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) + }) + test.each(testCases)('%s does not change cache on error', async (mutationName, options) => { + mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket + mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockMutationEndpoints(basketsEndpoint, {}, 400) // this mutation + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitForValueToChange(() => result.current.basket.data) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + act(() => result.current.mutation.mutate(options)) + await waitForValueToChange(() => result.current.mutation.error) + expect(result.current.mutation.error).toBeInstanceOf(Error) + assertUpdateQuery(result.current.basket, oldBasket) + assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) }) + // TODO: Special case `deleteBasket` }) - -const mockRelatedQueries = () => { - const basketEndpoint = '/checkout/shopper-baskets/' - const customerEndpoint = '/customer/shopper-customers/' - - // The queries would initially respond with 'old data'. - // And then subsequent responses would have 'new data' because of the cache updates. - - // For get basket - nock(DEFAULT_TEST_HOST) - .get((uri) => { - return uri.includes(basketEndpoint) - }) - .reply(200, oldBasket) - nock(DEFAULT_TEST_HOST) - .persist() - .get((uri) => { - return uri.includes(basketEndpoint) - }) - .reply(200, newBasket) - - // For get customer basket - nock(DEFAULT_TEST_HOST) - .get((uri) => { - return uri.includes(customerEndpoint) - }) - .reply(200, oldCustomerBaskets) - nock(DEFAULT_TEST_HOST) - .persist() - .get((uri) => { - return uri.includes(customerEndpoint) - }) - .reply(200, newCustomerBaskets) -} From 15e8aea43dfd61030c78ef2d36151010d95db47a Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 14:07:35 -0500 Subject: [PATCH 070/122] Remove unused helper type. --- packages/commerce-sdk-react/src/hooks/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 8b3e3cdafb..568cc068a3 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -42,9 +42,6 @@ type StringIndexNever = { /** Removes a string index type */ export type RemoveStringIndex = RemoveNeverValues> -/** Get the known (non-index) keys of a type. */ -export type KnownKeys = keyof RemoveStringIndex - // --- API CLIENTS --- // export type ApiClientConfigParams = { From 98e9fffaaa446b43bd4fa240e2487a5c9193eb10 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 14:45:29 -0500 Subject: [PATCH 071/122] Create hook success/error helpers. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 34 +++++++---------- .../commerce-sdk-react/src/test-utils.tsx | 38 ++++++++++++++++++- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index eb7bf0454b..b5c5749723 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -4,7 +4,6 @@ * 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 {act} from '@testing-library/react' import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' import nock from 'nock' @@ -13,7 +12,9 @@ import { DEFAULT_TEST_CONFIG, mockMutationEndpoints, mockQueryEndpoint, - renderHookWithProviders + renderHookWithProviders, + waitAndExpectError, + waitAndExpectSuccess } from '../../test-utils' import {useCustomerBaskets} from '../ShopperCustomers' import {ApiClients, Argument} from '../types' @@ -126,44 +127,38 @@ describe('ShopperBaskets mutations', () => { test.each(testCases)('%s returns data on success', async (mutationName, options) => { mockMutationEndpoints(basketsEndpoint, oldBasket) - const {result, waitForValueToChange} = renderHookWithProviders(() => { + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { return useShopperBasketsMutation(mutationName) }) expect(result.current.data).toBeUndefined() act(() => result.current.mutate(options)) // Watching `data` and `error` together provides better info for failing tests - await waitForValueToChange(() => result.current.data ?? result.current.error) - expect(result.current.error).toBeNull() + await waitAndExpectSuccess(wait, () => result.current) expect(result.current.data).toEqual(oldBasket) }) test.each(testCases)('%s returns error on error', async (mutationName, options) => { mockMutationEndpoints(basketsEndpoint, {}, 400) - const {result, waitForValueToChange} = renderHookWithProviders(() => { + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { return useShopperBasketsMutation(mutationName) }) expect(result.current.error).toBeNull() act(() => result.current.mutate(options)) - await waitForValueToChange(() => result.current.error) - expect(result.current.error).toBeInstanceOf(Error) + await waitAndExpectError(wait, () => result.current) }) test.each(testCases)('%s changes the cache on success', async (mutationName, options) => { mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets mockMutationEndpoints(basketsEndpoint, newBasket) // this mutation - const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ basket: queries.useBasket(getBasketOptions), customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), mutation: useShopperBasketsMutation(mutationName) })) - await waitForValueToChange(() => result.current.basket.data) + await waitAndExpectSuccess(wait, () => result.current.basket) expect(result.current.basket.data).toEqual(oldBasket) expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) act(() => result.current.mutation.mutate(options)) - await waitForValueToChange( - // Watching `data` and `error` together provides better info for failing tests - () => result.current.mutation.data ?? result.current.mutation.error - ) - expect(result.current.mutation.error).toBeNull() + await waitAndExpectSuccess(wait, () => result.current.mutation) assertUpdateQuery(result.current.basket, newBasket) assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) }) @@ -171,19 +166,18 @@ describe('ShopperBaskets mutations', () => { mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets mockMutationEndpoints(basketsEndpoint, {}, 400) // this mutation - const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ basket: queries.useBasket(getBasketOptions), customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), mutation: useShopperBasketsMutation(mutationName) })) - await waitForValueToChange(() => result.current.basket.data) + await waitAndExpectSuccess(wait, () => result.current.basket) expect(result.current.basket.data).toEqual(oldBasket) expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + expect(result.current.mutation.error).toBeNull() act(() => result.current.mutation.mutate(options)) - await waitForValueToChange(() => result.current.mutation.error) - expect(result.current.mutation.error).toBeInstanceOf(Error) + await waitAndExpectError(wait, () => result.current.mutation) assertUpdateQuery(result.current.basket, oldBasket) assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) }) - // TODO: Special case `deleteBasket` }) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index 6077ccb770..17f7fa65c4 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -7,8 +7,13 @@ import React from 'react' import {render, RenderOptions} from '@testing-library/react' -import {renderHook} from '@testing-library/react-hooks/dom' -import {QueryClient, QueryClientProvider, UseQueryResult} from '@tanstack/react-query' +import {renderHook, WaitForValueToChange} from '@testing-library/react-hooks/dom' +import { + QueryClient, + QueryClientProvider, + UseMutationResult, + UseQueryResult +} from '@tanstack/react-query' import nock from 'nock' import CommerceApiProvider, {CommerceApiProviderProps} from './provider' @@ -168,3 +173,32 @@ export const expectAllEndpointsHaveHooks = ( // Convert to array for easier comparison / better jest output expect([...unimplemented]).toEqual([]) } +/** Helper type for WaitForValueToChange with hooks */ +type GetHookResult = () => + | UseQueryResult + | UseMutationResult +/** Helper that waits for a hook to finish loading. */ +const waitForHookToFinish = async ( + wait: WaitForValueToChange, + getResult: GetHookResult +) => { + await wait(() => getResult().isSuccess || getResult().isError) +} +/** Helper that asserts that a hook is a success. */ +export const waitAndExpectSuccess = async ( + wait: WaitForValueToChange, + getResult: GetHookResult +) => { + await waitForHookToFinish(wait, getResult) + // Checking the error first gives us the best context for failing tests + expect(getResult().error).toBeNull() + expect(getResult().isSuccess).toBe(true) +} +/** Helper that asserts that a hook returned an error */ +export const waitAndExpectError = async ( + wait: WaitForValueToChange, + getResult: GetHookResult +) => { + await waitForHookToFinish(wait, getResult) + expect(getResult().error).toBeInstanceOf(Error) +} From 6764cb8b7a42186287f7b1353ca05c9f8f49984b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 16:58:49 -0500 Subject: [PATCH 072/122] Improve parameter matching. --- .../src/hooks/ShopperBaskets/cache.ts | 43 +++++++++----- .../commerce-sdk-react/src/hooks/types.ts | 1 + .../commerce-sdk-react/src/hooks/utils.ts | 57 +++++++++++++------ 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index 26b24b3343..09b7cfdff0 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -11,7 +11,7 @@ import { ShopperCustomersTypes } from 'commerce-sdk-isomorphic' import {ApiClients, Argument, CacheUpdate, CacheUpdateMatrix, MergedOptions} from '../types' -import {and, matchesApiConfig, matchesPath, pick} from '../utils' +import {and, matchesPath, matchParameters, pick} from '../utils' type Client = ApiClients['shopperBaskets'] /** Data returned by every Shopper Baskets endpoint (except `deleteBasket`) */ @@ -44,6 +44,23 @@ const getCustomerBasketsPath = (parameters: GetCustomerBasketsParameters) => [ '/baskets' // No trailing / as it's an aggregate endpoint ] +// Parameters helpers +/** + * Creates an object with *only* the parameters from `getBasket`, omitting unwanted parameters from + * other endpoints. (Extra parameters can break query key matching.) + */ +const toGetBasketParameters = (parameters: GetBasketParameters): GetBasketParameters => + pick(parameters, ['basketId', 'locale', 'organizationId', 'siteId']) +/** + * Creates an object with *only* the parameters from `getBasket`, omitting unwanted parameters from + * other endpoints. (Extra parameters can break query key matching.) + */ +const toGetCustomerBasketsParameters = ( + customerId: string, + parameters: Omit +): GetCustomerBasketsParameters => + pick({customerId, ...parameters}, ['customerId', 'organizationId', 'siteId']) + const updateBasketQuery = ( customerId: string | null, parameters: BasketParameters, @@ -51,12 +68,7 @@ const updateBasketQuery = ( ): CacheUpdate => { // The `parameters` received includes the client config and parameters from other endpoints // so we need to exclude unwanted parameters in the query key - const getBasketParameters: GetBasketParameters = pick(parameters, [ - 'basketId', - 'locale', - 'organizationId', - 'siteId' - ]) + const getBasketParameters = toGetBasketParameters(parameters) const basketUpdate = { queryKey: [...getBasketPath(parameters), getBasketParameters] as const } @@ -64,12 +76,8 @@ const updateBasketQuery = ( // We can only update customer baskets if we have a customer! if (!customerId) return {update: [basketUpdate]} - // Same elision as done for `getBasket` - const getCustomerBasketsParameters: GetCustomerBasketsParameters = { - customerId, - organizationId: parameters.organizationId, - siteId: parameters.siteId - } + // Similar elision as done for `getBasket` + const getCustomerBasketsParameters = toGetCustomerBasketsParameters(customerId, parameters) const customerBasketsUpdate = { queryKey: [ ...getCustomerBasketsPath(getCustomerBasketsParameters), @@ -103,7 +111,7 @@ const invalidateCustomerBasketsQuery = ( return { invalidate: [ and( - matchesApiConfig(parameters), + matchParameters(toGetCustomerBasketsParameters(customerId, parameters)), matchesPath(getCustomerBasketsPath({...parameters, customerId})) ) ] @@ -141,7 +149,12 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { deleteBasket: (customerId, {parameters}) => ({ // TODO: Convert invalidate to an update that removes the matching basket ...invalidateCustomerBasketsQuery(customerId, parameters), - remove: [and(matchesApiConfig(parameters), matchesPath(getBasketPath(parameters)))] + remove: [ + and( + matchParameters(toGetBasketParameters(parameters)), + matchesPath(getBasketPath(parameters)) + ) + ] }), mergeBasket: updateBasketWithResponseBasketId, removeCouponFromBasket: updateBasket, diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 568cc068a3..e0f10765e5 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -43,6 +43,7 @@ type StringIndexNever = { export type RemoveStringIndex = RemoveNeverValues> // --- API CLIENTS --- // +export type ApiParameter = string | number | boolean | string[] | number[] export type ApiClientConfigParams = { clientId: string diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index d363d2a36e..7d6ab63a80 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {Query, QueryClient} from '@tanstack/react-query' -import {ApiClient, ApiOptions, CacheUpdate, MergedOptions} from './types' +import {ApiClient, ApiOptions, ApiParameter, CacheUpdate, MergedOptions} from './types' /** Applies the set of cache updates to the query client. */ export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate, data: unknown) => { @@ -46,48 +46,61 @@ export const matchesPath = queryKey.length === 1 + search.length && search.every((lookup, idx) => queryKey[idx] === lookup) +/** Does an equality check for two API parameter values */ +const matchParameter = (search: ApiParameter, param: unknown): boolean => { + // 1. Are they matching primitives? + if (search === param) return true + // 2. They're not both primitives. Are they both arrays? + if (!Array.isArray(search) || !Array.isArray(param)) return false + // 3. They're both arrays. Are they the same length? + if (search.length !== param.length) return false + // 4. They're the same length. Do all of the values match? + return param.every((value, index) => search[index] === value) +} + /** * Creates a query predicate that determines whether the parameters of the query key exactly match - * the search object. + * the search object. NOTE: This returns `true` even when the query key has additional properties. */ export const matchParametersStrict = - (search: Record) => + (search: Record) => ({queryKey}: Query): boolean => { const parameters = queryKey[queryKey.length - 1] if (!isObject(parameters)) return false const searchEntries = Object.entries(search) return ( // Can't be a match if we're looking for more values than we have - searchEntries.length > Object.keys(parameters).length && - // TODO: Support arrays - parameters can also be string | number - searchEntries.every(([key, lookup]) => parameters[key] === lookup) + searchEntries.length <= Object.keys(parameters).length && + searchEntries.every(([key, lookup]) => matchParameter(lookup, parameters[key])) ) } /** * Creates a query predicate that determines whether the parameters of the query key match the - * search object, for the subset of given keys present on the search object. + * search object, if the value on the search object is not `undefined`. */ -const matchParameters = (parameters: Record, keys = Object.keys(parameters)) => { - const search: Record = {} +export const matchParameters = ( + parameters: Record, + keys = Object.keys(parameters) +) => { + const search: Record = {} for (const key of keys) { - if (parameters[key] !== undefined) search[key] = parameters[key] + const value = parameters[key] + if (value !== undefined) search[key] = value } return matchParametersStrict(search) } /** Creates a query predicate that matches against common API config parameters. */ -export const matchesApiConfig = (parameters: Record) => +export const matchesApiConfig = (parameters: Record) => matchParameters(parameters, [ + // NOTE: `shortCode` and `version` are omitted, as query keys are constructed from endpoint + // paths, but the two paarameters are only used to construct the base URI. 'clientId', 'currency', // TODO: maybe? 'locale', // TODO: maybe? 'organizationId', - 'shortCode', - 'siteId', - // Version is never used directly by us, but is set on the client config - // in `commerce-sdk-isomorphic`, so we include it here for completeness - 'version' + 'siteId' ]) /** Creates a query predicate that returns true if all of the given predicates return true. */ @@ -123,8 +136,16 @@ export const mergeOptions = (obj: T, keys: readonly K[]): Pick => { +/** Constructs a subset of the given object containing only the given keys. */ +export const pick = ( + obj: T, + keys: readonly K[] +): Pick => { const picked = {} as Pick // Assertion is not true, yet, but we make it so! - keys.forEach((key) => (picked[key] = obj[key])) + keys.forEach((key) => { + if (key in obj) { + picked[key] = obj[key] + } + }) return picked } From 465c74876c0cdfc66801da4f52c75e2cbab02e13 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 16:59:04 -0500 Subject: [PATCH 073/122] Add `deleteBasket` tests. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 113 ++++++++++++------ 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index b5c5749723..1228e551b5 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -4,10 +4,13 @@ * 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 {useQueryClient} from '@tanstack/react-query' import {act} from '@testing-library/react' import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' import nock from 'nock' import { + assertInvalidateQuery, + assertRemoveQuery, assertUpdateQuery, DEFAULT_TEST_CONFIG, mockMutationEndpoints, @@ -67,11 +70,11 @@ const newCustomerBaskets: BasketsResult = { } // --- TEST CASES --- // +/** All Shopper Baskets mutations except `deleteBasket` have the same cache update logic. */ type NonDeleteMutation = Exclude -// TODO: Remove optional flag when all mutations are implemented -type TestMap = {[Mut in NonDeleteMutation]?: Argument} // This is an object rather than an array to more easily ensure we cover all mutations -const testMap: TestMap = { +// TODO: Remove optional flag when all mutations are implemented +const testMap: {[Mut in NonDeleteMutation]?: Argument} = { addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}, {}), addItemToBasket: makeOptions<'addItemToBasket'>([], {}), addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}, {}), @@ -101,11 +104,14 @@ const testMap: TestMap = { {shipmentId: 'shipmentId'} ) } -// This is what we actually use for `test.each` +const deleteTestCase = ['deleteBasket', makeOptions<'deleteBasket'>(undefined, {})] as const + // Type assertion because the built-in type definition for `Object.entries` is limited :\ -const testCases = Object.entries(testMap) as Array< - [NonDeleteMutation, NonNullable] +const nonDeleteTestCases = Object.entries(testMap) as Array< + [ShopperBasketsMutation, Argument] > +// Most test cases only apply to non-delete test cases, some (error handling) can include deleteBasket +const allTestCases = [...nonDeleteTestCases, deleteTestCase] describe('ShopperBaskets mutations', () => { const storedCustomerIdKey = `${DEFAULT_TEST_CONFIG.siteId}_customer_id` @@ -119,24 +125,17 @@ describe('ShopperBaskets mutations', () => { }) beforeEach(() => nock.cleanAll()) - afterEach(() => { - // To avoid needing to know which HTTP verb a mutation uses (DELETE/PATCH/POST/PUT), - // we mock them all, and then validate that exactly one was used. - expect(nock.pendingMocks().length).toBe(3) - }) - - test.each(testCases)('%s returns data on success', async (mutationName, options) => { + test.each(nonDeleteTestCases)('`%s` returns data on success', async (mutationName, options) => { mockMutationEndpoints(basketsEndpoint, oldBasket) const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { return useShopperBasketsMutation(mutationName) }) expect(result.current.data).toBeUndefined() act(() => result.current.mutate(options)) - // Watching `data` and `error` together provides better info for failing tests await waitAndExpectSuccess(wait, () => result.current) expect(result.current.data).toEqual(oldBasket) }) - test.each(testCases)('%s returns error on error', async (mutationName, options) => { + test.each(allTestCases)('`%s` returns error on error', async (mutationName, options) => { mockMutationEndpoints(basketsEndpoint, {}, 400) const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { return useShopperBasketsMutation(mutationName) @@ -145,39 +144,79 @@ describe('ShopperBaskets mutations', () => { act(() => result.current.mutate(options)) await waitAndExpectError(wait, () => result.current) }) - test.each(testCases)('%s changes the cache on success', async (mutationName, options) => { + test.each(nonDeleteTestCases)( + '`%s` updates the cache on success', + async (mutationName, options) => { + mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket + mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockMutationEndpoints(basketsEndpoint, newBasket) // this mutation + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitAndExpectSuccess(wait, () => result.current.basket) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(wait, () => result.current.mutation) + assertUpdateQuery(result.current.basket, newBasket) + assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) + } + ) + test.each(allTestCases)( + '`%s` does not change cache on error', + async (mutationName, options) => { + mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket + mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets + mockMutationEndpoints(basketsEndpoint, {}, 400) // this mutation + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ + basket: queries.useBasket(getBasketOptions), + customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), + mutation: useShopperBasketsMutation(mutationName) + })) + await waitAndExpectSuccess(wait, () => result.current.basket) + expect(result.current.basket.data).toEqual(oldBasket) + expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) + expect(result.current.mutation.error).toBeNull() + act(() => result.current.mutation.mutate(options)) + await waitAndExpectError(wait, () => result.current.mutation) + assertUpdateQuery(result.current.basket, oldBasket) + assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) + } + ) + test('`deleteBasket` returns void on success', async () => { + // Almost the standard 'returns data' test, just a different return type + const [mutationName, options] = deleteTestCase + mockMutationEndpoints(basketsEndpoint, oldBasket) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperBasketsMutation(mutationName) + }) + expect(result.current.data).toBeUndefined() + act(() => result.current.mutate(options)) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toBeUndefined() + }) + test('`deleteBasket` removes the basket from the cache on success', async () => { + // Almost the standard 'updates cache' test, but the cache changes are different + const [mutationName, options] = deleteTestCase mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets mockMutationEndpoints(basketsEndpoint, newBasket) // this mutation const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ basket: queries.useBasket(getBasketOptions), customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), - mutation: useShopperBasketsMutation(mutationName) + mutation: useShopperBasketsMutation(mutationName), + queryClient: useQueryClient() // TODO: Remove })) await waitAndExpectSuccess(wait, () => result.current.basket) + // TODO customerBaskets: isSuccess isFetched isFetchedAfterMount isStale expect(result.current.basket.data).toEqual(oldBasket) expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) act(() => result.current.mutation.mutate(options)) await waitAndExpectSuccess(wait, () => result.current.mutation) - assertUpdateQuery(result.current.basket, newBasket) - assertUpdateQuery(result.current.customerBaskets, newCustomerBaskets) - }) - test.each(testCases)('%s does not change cache on error', async (mutationName, options) => { - mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket - mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets - mockMutationEndpoints(basketsEndpoint, {}, 400) // this mutation - const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ - basket: queries.useBasket(getBasketOptions), - customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), - mutation: useShopperBasketsMutation(mutationName) - })) - await waitAndExpectSuccess(wait, () => result.current.basket) - expect(result.current.basket.data).toEqual(oldBasket) - expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) - expect(result.current.mutation.error).toBeNull() - act(() => result.current.mutation.mutate(options)) - await waitAndExpectError(wait, () => result.current.mutation) - assertUpdateQuery(result.current.basket, oldBasket) - assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) + // TODO customerBaskets: isError isFetched isFetchedAfterMount isRefetchError isStale + assertRemoveQuery(result.current.basket) + assertInvalidateQuery(result.current.customerBaskets, oldCustomerBaskets) }) }) From 603552ddbd1032cbe5c514564530a57d27146cce Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 17:34:25 -0500 Subject: [PATCH 074/122] Add request mock to fix test. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 1228e551b5..4fc5e88367 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -4,7 +4,6 @@ * 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 {useQueryClient} from '@tanstack/react-query' import {act} from '@testing-library/react' import {ShopperBasketsTypes, ShopperCustomersTypes} from 'commerce-sdk-isomorphic' import nock from 'nock' @@ -68,6 +67,11 @@ const newCustomerBaskets: BasketsResult = { baskets: [{basketId: 'other_basket'}, newBasket] as BasketsResult['baskets'], total: 2 } +const deletedCustomerBaskets: BasketsResult = { + // We aren't implementing the full basket, so we assert to pretend we are + baskets: [{basketId: 'other_basket'}] as BasketsResult['baskets'], + total: 1 +} // --- TEST CASES --- // /** All Shopper Baskets mutations except `deleteBasket` have the same cache update logic. */ @@ -203,19 +207,17 @@ describe('ShopperBaskets mutations', () => { mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets mockMutationEndpoints(basketsEndpoint, newBasket) // this mutation + mockQueryEndpoint(customersEndpoint, deletedCustomerBaskets) // getCustomerBaskets refetch const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ basket: queries.useBasket(getBasketOptions), customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), - mutation: useShopperBasketsMutation(mutationName), - queryClient: useQueryClient() // TODO: Remove + mutation: useShopperBasketsMutation(mutationName) })) await waitAndExpectSuccess(wait, () => result.current.basket) - // TODO customerBaskets: isSuccess isFetched isFetchedAfterMount isStale expect(result.current.basket.data).toEqual(oldBasket) expect(result.current.customerBaskets.data).toEqual(oldCustomerBaskets) act(() => result.current.mutation.mutate(options)) await waitAndExpectSuccess(wait, () => result.current.mutation) - // TODO customerBaskets: isError isFetched isFetchedAfterMount isRefetchError isStale assertRemoveQuery(result.current.basket) assertInvalidateQuery(result.current.customerBaskets, oldCustomerBaskets) }) From cc0e0c2a1da953fb51daa2c81ec719990f51e71b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 17:43:19 -0500 Subject: [PATCH 075/122] Add TODO cache update logic for missing mutations. --- .../src/hooks/ShopperBaskets/cache.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index 09b7cfdff0..0be271cdb9 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -11,7 +11,7 @@ import { ShopperCustomersTypes } from 'commerce-sdk-isomorphic' import {ApiClients, Argument, CacheUpdate, CacheUpdateMatrix, MergedOptions} from '../types' -import {and, matchesPath, matchParameters, pick} from '../utils' +import {and, matchesPath, matchParameters, NotImplementedError, pick} from '../utils' type Client = ApiClients['shopperBaskets'] /** Data returned by every Shopper Baskets endpoint (except `deleteBasket`) */ @@ -141,11 +141,20 @@ const updateBasketWithResponseBasketId = ( } } +const TODO = (method: keyof Client) => () => { + throw new NotImplementedError(`Cache logic for '${method}'`) +} + export const cacheUpdateMatrix: CacheUpdateMatrix = { addCouponToBasket: updateBasket, + addGiftCertificateItemToBasket: TODO('addGiftCertificateItemToBasket'), addItemToBasket: updateBasket, addPaymentInstrumentToBasket: updateBasket, + addPriceBooksToBasket: TODO('addPriceBooksToBasket'), + addTaxesForBasket: TODO('addTaxesForBasket'), + addTaxesForBasketItem: TODO('addTaxesForBasketItem'), createBasket: updateBasketWithResponseBasketId, + createShipmentForBasket: TODO('createShipmentForBasket'), deleteBasket: (customerId, {parameters}) => ({ // TODO: Convert invalidate to an update that removes the matching basket ...invalidateCustomerBasketsQuery(customerId, parameters), @@ -158,13 +167,18 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { }), mergeBasket: updateBasketWithResponseBasketId, removeCouponFromBasket: updateBasket, + removeGiftCertificateItemFromBasket: TODO('removeGiftCertificateItemFromBasket'), removeItemFromBasket: updateBasket, removePaymentInstrumentFromBasket: updateBasket, + removeShipmentFromBasket: TODO('removeShipmentFromBasket'), + transferBasket: TODO('transferBasket'), updateBasket: updateBasket, updateBillingAddressForBasket: updateBasket, updateCustomerForBasket: updateBasket, + updateGiftCertificateItemInBasket: TODO('updateGiftCertificateItemInBasket'), updateItemInBasket: updateBasket, updatePaymentInstrumentInBasket: updateBasket, + updateShipmentForBasket: TODO('updateShipmentForBasket'), updateShippingAddressForShipment: updateBasket, updateShippingMethodForShipment: updateBasket } From acbc12d269d1f2146a0e2c539e513b52706ebb75 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 23:05:45 -0500 Subject: [PATCH 076/122] Change onSuccess from bound to unbound function. I'm not sure why, but testing mutations that throw an error in `onSuccess` only works with unbound functions. When an unbound function is used, the error is properly reported in the hook result. With a bound function, the test framework throws an error. --- packages/commerce-sdk-react/src/hooks/useMutation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/useMutation.ts b/packages/commerce-sdk-react/src/hooks/useMutation.ts index df1eee6784..5ae8b071e2 100644 --- a/packages/commerce-sdk-react/src/hooks/useMutation.ts +++ b/packages/commerce-sdk-react/src/hooks/useMutation.ts @@ -29,7 +29,7 @@ export const useMutation = < const authenticatedMethod = useAuthorizationHeader(hookConfig.method) return useReactQueryMutation(authenticatedMethod, { - onSuccess: (data, options) => { + onSuccess(data, options) { // commerce-sdk-isomorphic merges `clientConfig` and `options` under the hood, // so we also need to do that to get the "net" options that are actually sent to SCAPI. const netOptions = mergeOptions(hookConfig.client, options) From 9efac92f4ea1c7dc57e412afd948235b8b8031ba Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 23:09:17 -0500 Subject: [PATCH 077/122] Add test for not implemented mutations. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 4fc5e88367..1a3a3c4fbd 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -20,6 +20,7 @@ import { } from '../../test-utils' import {useCustomerBaskets} from '../ShopperCustomers' import {ApiClients, Argument} from '../types' +import {NotImplementedError} from '../utils' import {ShopperBasketsMutation, useShopperBasketsMutation} from './mutation' import * as queries from './query' @@ -78,7 +79,8 @@ const deletedCustomerBaskets: BasketsResult = { type NonDeleteMutation = Exclude // This is an object rather than an array to more easily ensure we cover all mutations // TODO: Remove optional flag when all mutations are implemented -const testMap: {[Mut in NonDeleteMutation]?: Argument} = { +type TestMap = {[Mut in NonDeleteMutation]?: Argument} +const testMap: TestMap = { addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}, {}), addItemToBasket: makeOptions<'addItemToBasket'>([], {}), addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}, {}), @@ -117,6 +119,23 @@ const nonDeleteTestCases = Object.entries(testMap) as Array< // Most test cases only apply to non-delete test cases, some (error handling) can include deleteBasket const allTestCases = [...nonDeleteTestCases, deleteTestCase] +// Not implemented checks are temporary to make sure we don't forget to add tests when adding +// implentations. When all mutations are added, the "not implemented" tests can be removed, +// and the `TestMap` type can be changed from optional keys to required keys. Doing so will +// leverage TypeScript to enforce having tests for all mutations. +const notImplTestCases: NonDeleteMutation[] = [ + 'addGiftCertificateItemToBasket', + 'addPriceBooksToBasket', + 'addTaxesForBasket', + 'addTaxesForBasketItem', + 'createShipmentForBasket', + 'removeGiftCertificateItemFromBasket', + 'removeShipmentFromBasket', + 'transferBasket', + 'updateGiftCertificateItemInBasket', + 'updateShipmentForBasket' +] + describe('ShopperBaskets mutations', () => { const storedCustomerIdKey = `${DEFAULT_TEST_CONFIG.siteId}_customer_id` beforeAll(() => { @@ -221,4 +240,7 @@ describe('ShopperBaskets mutations', () => { assertRemoveQuery(result.current.basket) assertInvalidateQuery(result.current.customerBaskets, oldCustomerBaskets) }) + test.each(notImplTestCases)('`%s` is not yet implemented', async (mutationName) => { + expect(() => useShopperBasketsMutation(mutationName)).toThrow(NotImplementedError) + }) }) From c39f0a8a563deb48009cd7a72f88aa5b7f78ed56 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 23:16:31 -0500 Subject: [PATCH 078/122] Pass not implemented tests. --- .../src/hooks/ShopperBaskets/cache.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index 0be271cdb9..0057660588 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -141,8 +141,14 @@ const updateBasketWithResponseBasketId = ( } } -const TODO = (method: keyof Client) => () => { - throw new NotImplementedError(`Cache logic for '${method}'`) +const TODO = (method: keyof Client): undefined => { + // This is kind of a hacky way of passing both the "not implemented" tests in mutations.test.ts + // and the "all hooks have cache logic" test in index.test.ts. The former expects `undefined` + // as a value and the latter expects the key to exist, both of which are satisfied by setting + // an explicit `undefined`. So that's all that this does, plus logging a TODO warning. + // Hacky, but temporary! + console.warn(`Cache logic for '${method}' is not yet implemented.`) + return undefined } export const cacheUpdateMatrix: CacheUpdateMatrix = { From 528d96a3e718c3d0eb704ec6ba6ae2e67d40bff7 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 21 Feb 2023 23:18:07 -0500 Subject: [PATCH 079/122] Remove unused import. --- packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index 0057660588..030a861b1d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -11,7 +11,7 @@ import { ShopperCustomersTypes } from 'commerce-sdk-isomorphic' import {ApiClients, Argument, CacheUpdate, CacheUpdateMatrix, MergedOptions} from '../types' -import {and, matchesPath, matchParameters, NotImplementedError, pick} from '../utils' +import {and, matchesPath, matchParameters, pick} from '../utils' type Client = ApiClients['shopperBaskets'] /** Data returned by every Shopper Baskets endpoint (except `deleteBasket`) */ From 1f02ab4947e270467222cb58d3b4b7106a93851f Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 22 Feb 2023 10:17:01 -0500 Subject: [PATCH 080/122] Implement `resetPassword` caching as a no-op. --- packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts index 5c10c5e5ec..a926907a6b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts @@ -167,7 +167,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { ] } }, - resetPassword: TODO('resetPassword'), + resetPassword: noop, updateCustomer(customerId, {parameters}) { if (!customerId) return {} const customerPath = getCustomerPath(customerId, parameters) From a5a321e2b0e0a08b0294a293394ce76af6caba39 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 22 Feb 2023 12:32:40 -0500 Subject: [PATCH 081/122] Add Shopper Baskets query tests. --- .../src/hooks/ShopperBaskets/query.test.ts | 62 +++++++++++++++++++ .../commerce-sdk-react/src/test-utils.tsx | 8 +-- 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts new file mode 100644 index 0000000000..f6982d292c --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import {Argument} from '../types' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const basketsEndpoint = '/checkout/shopper-baskets/' +// Not all endpoints use `shipmentId`, but unused parameters are safely discarded +const OPTIONS: Argument = { + parameters: {basketId: 'basketId', shipmentId: 'shipmentId'} +} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + useBasket: {basketId: 'basketId'}, + usePaymentMethodsForBasket: {applicablePaymentMethods: []}, + usePriceBooksForBasket: ['priceBookId'], + useShippingMethodsForShipment: {defaultShippingMethodId: 'defaultShippingMethodId'}, + useTaxesFromBasket: {taxes: {}} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Baskets query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(basketsEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(basketsEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index 17f7fa65c4..42becb751b 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -117,7 +117,7 @@ export const mockMutationEndpoints = ( /** Mocks a GET request to an endpoint. */ export const mockQueryEndpoint = ( matchingPath: string, - response: string | Record, + response: string | Record | unknown[], statusCode = 200 ) => { const matcher = (uri: string) => uri.includes(matchingPath) @@ -126,7 +126,7 @@ export const mockQueryEndpoint = ( export const assertUpdateQuery = ( queryResult: UseQueryResult, - newData: Record + newData: Record | unknown[] ) => { // query should be updated without a refetch expect(queryResult.data).toEqual(newData) @@ -135,7 +135,7 @@ export const assertUpdateQuery = ( export const assertInvalidateQuery = ( queryResult: UseQueryResult, - oldData: Record + oldData: Record | unknown[] ) => { // query should be invalidated and refetching expect(queryResult.data).toEqual(oldData) @@ -200,5 +200,5 @@ export const waitAndExpectError = async ( getResult: GetHookResult ) => { await waitForHookToFinish(wait, getResult) - expect(getResult().error).toBeInstanceOf(Error) + expect(getResult().isError).toBe(true) } From 1db46f007d6b5128531f6d523e0bb67b0cbd4feb Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 22 Feb 2023 12:45:53 -0500 Subject: [PATCH 082/122] Add "all endpoints have hooks" tests for all APIs. --- .../src/hooks/ShopperBaskets/index.test.ts | 2 +- .../src/hooks/ShopperContexts/index.test.ts | 24 +++++++++++++++++++ .../src/hooks/ShopperCustomers/index.test.ts | 24 +++++++++++++++++++ .../src/hooks/ShopperExperience/index.test.ts | 15 ++++++++++++ .../ShopperGiftCertificates/index.test.ts | 15 ++++++++++++ .../src/hooks/ShopperLogin/index.test.ts | 24 +++++++++++++++++++ .../src/hooks/ShopperOrders/index.test.ts | 24 +++++++++++++++++++ .../src/hooks/ShopperProducts/index.test.ts | 15 ++++++++++++ .../src/hooks/ShopperPromotions/index.test.ts | 15 ++++++++++++ .../src/hooks/ShopperSearch/index.test.ts | 15 ++++++++++++ .../commerce-sdk-react/src/test-utils.tsx | 2 +- 11 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts index 484bd8afbf..11cda45905 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts @@ -16,9 +16,9 @@ describe('Shopper Baskets hooks', () => { }) test('all mutation hooks have cache update logic', () => { - // If this test fails, add the missing mutation as a no-op or a TODO that throws NotYetImplemented const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() const mutations = Object.values(ShopperBasketsMutations).sort() + // If this test fails, add the missing mutation as a no-op with a TODO note expect(cacheUpdates).toEqual(mutations) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts new file mode 100644 index 0000000000..09f87971e7 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 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 {ShopperContexts} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {cacheUpdateMatrix} from './cache' +import {ShopperContextsMutations} from './mutation' +import * as queries from './query' + +describe('Shopper Contexts hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperContexts, queries, ShopperContextsMutations) + }) + + test('all mutation hooks have cache update logic', () => { + const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() + const mutations = Object.values(ShopperContextsMutations).sort() + // If this test fails, add the missing mutation as a no-op with a TODO note + expect(cacheUpdates).toEqual(mutations) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts new file mode 100644 index 0000000000..807bdd4567 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 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 {ShopperCustomers} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {cacheUpdateMatrix} from './cache' +import {ShopperCustomersMutations} from './mutation' +import * as queries from './query' + +describe('Shopper Customers hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperCustomers, queries, ShopperCustomersMutations) + }) + + test('all mutation hooks have cache update logic', () => { + const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() + const mutations = Object.values(ShopperCustomersMutations).sort() + // If this test fails, add the missing mutation as a no-op with a TODO note + expect(cacheUpdates).toEqual(mutations) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts new file mode 100644 index 0000000000..88ebb4e57d --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023, 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 {ShopperExperience} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import * as queries from './query' + +describe('Shopper Experience hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperExperience, queries) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts new file mode 100644 index 0000000000..77d72684d5 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023, 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 {ShopperGiftCertificates} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import * as queries from './query' + +describe('Shopper GiftCertificates hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperGiftCertificates, queries) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts new file mode 100644 index 0000000000..03128c9a38 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 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 {ShopperLogin} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {cacheUpdateMatrix} from './cache' +import {ShopperLoginMutations} from './mutation' +import * as queries from './query' + +describe('Shopper Login hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperLogin, queries, ShopperLoginMutations) + }) + + test('all mutation hooks have cache update logic', () => { + const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() + const mutations = Object.values(ShopperLoginMutations).sort() + // If this test fails, add the missing mutation as a no-op with a TODO note + expect(cacheUpdates).toEqual(mutations) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts new file mode 100644 index 0000000000..061217b3f9 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 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 {ShopperOrders} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {cacheUpdateMatrix} from './cache' +import {ShopperOrdersMutations} from './mutation' +import * as queries from './query' + +describe('Shopper Orders hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperOrders, queries, ShopperOrdersMutations) + }) + + test('all mutation hooks have cache update logic', () => { + const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() + const mutations = Object.values(ShopperOrdersMutations).sort() + // If this test fails, add the missing mutation as a no-op with a TODO note + expect(cacheUpdates).toEqual(mutations) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts new file mode 100644 index 0000000000..868d8a4019 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023, 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 {ShopperBaskets} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import * as queries from './query' + +describe('Shopper Baskets hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperBaskets, queries) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts new file mode 100644 index 0000000000..dae48180fe --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023, 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 {ShopperPromotions} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import * as queries from './query' + +describe('Shopper Promotions hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperPromotions, queries) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts new file mode 100644 index 0000000000..e80c3c4f22 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023, 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 {ShopperSearch} from 'commerce-sdk-isomorphic' +import {expectAllEndpointsHaveHooks} from '../../test-utils' +import * as queries from './query' + +describe('Shopper Search hooks', () => { + test('all endpoints have hooks', () => { + expectAllEndpointsHaveHooks(ShopperSearch, queries) + }) +}) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index 42becb751b..85b5d7a47d 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -158,7 +158,7 @@ const getQueryName = (method: string): string => { export const expectAllEndpointsHaveHooks = ( SdkClass: {prototype: object}, queryHooks: Record, - mutationsEnum: Record + mutationsEnum: Record = {} ) => { const unimplemented = new Set(Object.getOwnPropertyNames(SdkClass.prototype)) // Always present on a class; we can ignore From 874b2ff00dac11bd27d005de7936de69f994a5d5 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 22 Feb 2023 15:31:21 -0500 Subject: [PATCH 083/122] Fix failing index tests. --- .../src/hooks/ShopperLogin/cache.ts | 11 +++++++++++ .../src/hooks/ShopperProducts/index.test.ts | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts index 0a98ca9302..bfdb5d5bfe 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts @@ -7,9 +7,20 @@ import {ApiClients, CacheUpdateMatrix} from '../types' const noop = () => ({}) +const TODO = (method: string): undefined => { + // This is kind of a hacky way of passing both the "not implemented" tests in mutations.test.ts + // and the "all hooks have cache logic" test in index.test.ts. The former expects `undefined` + // as a value and the latter expects the key to exist, both of which are satisfied by setting + // an explicit `undefined`. So that's all that this does, plus logging a TODO warning. + // Hacky, but temporary! + console.warn(`Cache logic for '${method}' is not yet implemented.`) + return undefined +} + export const cacheUpdateMatrix: CacheUpdateMatrix = { authenticateCustomer: noop, authorizePasswordlessCustomer: noop, + logoutCustomer: TODO('logoutCustomer'), getAccessToken: noop, getSessionBridgeAccessToken: noop, getTrustedSystemAccessToken: noop, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts index 868d8a4019..9e595c2428 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts @@ -4,12 +4,12 @@ * 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 {ShopperBaskets} from 'commerce-sdk-isomorphic' +import {ShopperProducts} from 'commerce-sdk-isomorphic' import {expectAllEndpointsHaveHooks} from '../../test-utils' import * as queries from './query' -describe('Shopper Baskets hooks', () => { +describe('Shopper Products hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperBaskets, queries) + expectAllEndpointsHaveHooks(ShopperProducts, queries) }) }) From da1c71ecaceb5f5d022f7b532ab17e56dbdaf4a5 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 22 Feb 2023 23:25:47 -0500 Subject: [PATCH 084/122] Implement tests for all query hooks. --- .../src/hooks/ShopperBaskets/query.test.ts | 2 +- .../src/hooks/ShopperContexts/query.test.ts | 55 ++++++++++++ .../src/hooks/ShopperCustomers/query.test.ts | 85 +++++++++++++++++++ .../src/hooks/ShopperExperience/query.test.ts | 56 ++++++++++++ .../ShopperGiftCertificates/query.test.ts | 56 ++++++++++++ .../src/hooks/ShopperLogin/query.test.ts | 72 ++++++++++++++++ .../src/hooks/ShopperOrders/query.test.ts | 57 +++++++++++++ .../src/hooks/ShopperProducts/query.test.ts | 58 +++++++++++++ .../src/hooks/ShopperPromotions/query.test.ts | 56 ++++++++++++ .../src/hooks/ShopperSearch/query.test.ts | 58 +++++++++++++ .../commerce-sdk-react/src/test-utils.tsx | 2 +- 11 files changed, 555 insertions(+), 2 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperExperience/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperLogin/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperOrders/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperProducts/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.test.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperSearch/query.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts index f6982d292c..2043cba1bc 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.test.ts @@ -22,7 +22,7 @@ jest.mock('../../auth/index.ts', () => { type Queries = typeof queries const basketsEndpoint = '/checkout/shopper-baskets/' -// Not all endpoints use `shipmentId`, but unused parameters are safely discarded +// Not all endpoints use all parameters, but unused parameters are safely discarded const OPTIONS: Argument = { parameters: {basketId: 'basketId', shipmentId: 'shipmentId'} } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts new file mode 100644 index 0000000000..479beed444 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const contextsEndpoint = '/shopper/shopper-context/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {parameters: {usid: 'usid'}} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + useShopperContext: {customQualifiers: {}} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Baskets query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(contextsEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(contextsEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts new file mode 100644 index 0000000000..6815fdfd24 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const customersEndpoint = '/customer/shopper-customers/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = { + parameters: { + externalId: 'externalId', + authenticationProviderId: 'authenticationProviderId', + customerId: 'customerId', + addressName: 'addressName', + paymentInstrumentId: 'paymentInstrumentId', + listId: 'listId', + itemId: 'itemId' + } +} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + useCustomer: {customerId: 'customerId'}, + useCustomerAddress: {addressId: 'addressId', lastName: 'Human', countryCode: 'CA'}, + useCustomerBaskets: {total: 0, baskets: []}, + useCustomerOrders: {total: 0, data: [], limit: 0, offset: 0}, + useCustomerPaymentInstrument: { + paymentBankAccount: {}, + paymentCard: {cardType: 'fake'}, + paymentInstrumentId: 'paymentInstrumentId', + paymentMethodId: 'paymentMethodId' + }, + useCustomerProductList: {}, + useCustomerProductListItem: {priority: 9000, public: false, quantity: 0}, + useCustomerProductLists: {data: [], limit: 0, total: 0}, + useExternalProfile: { + authenticationProviderId: 'squirrel', + customerId: 'customerId', + externalId: 'external' + }, + useProductListItem: {id: 'id', priority: 0, type: 'thing'}, + usePublicProductList: {id: 'id', public: true, type: 'other'}, + usePublicProductListsBySearchTerm: {data: [], limit: 0, total: 0} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Customers query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(customersEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(customersEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.test.ts new file mode 100644 index 0000000000..354850ed95 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const experienceEndpoint = '/experience/shopper-experience/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {parameters: {pageId: 'pageId', aspectTypeId: 'aspectTypeId'}} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + usePage: {id: 'id', typeId: 'typeId'}, + usePages: {data: []} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Experience query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(experienceEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(experienceEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.test.ts new file mode 100644 index 0000000000..fb93a2dd7c --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, 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 {ShopperGiftCertificatesTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + mockMutationEndpoints, + mockQueryEndpoint, + renderHookWithProviders, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const giftCertificatesEndpoint = '/pricing/shopper-gift-certificates/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {body: {giftCertificateCode: 'code'}} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + // Type assertion so that we don't have to implement the full type + useGiftCertificate: {amount: 0, balance: 0} as ShopperGiftCertificatesTypes.GiftCertificate +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Gift Certificates query hooks', () => { + beforeEach(() => nock.cleanAll()) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + // getGiftCertificate uses POST, so we need the mutation mock helper to mock the right verb + mockMutationEndpoints(giftCertificatesEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(giftCertificatesEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.test.ts new file mode 100644 index 0000000000..01ed920435 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023, 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 {ShopperLoginTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + mockQueryEndpoint, + renderHookWithProviders, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const loginEndpoint = '/shopper/auth/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = { + parameters: { + channel_id: 'channel_id', + client_id: 'client_id', + code_challenge: 'code_challenge', + idp_origin: 'idp_origin', + login_id: 'login_id', + redirect_uri: 'redirect_uri', + response_type: 'response_type', + username: 'username' + } +} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + // Type assertion so that we don't have to implement the full response + useCredQualityUserInfo: {credQualityMeasure: 0} as ShopperLoginTypes.CredQualityUserResponse, + // These endpoints return type `Object`, which isn't helpful, so we just use some mock data + useJwksUri: {mockJwksUriData: true}, + useUserInfo: {mockUserInfo: true}, + useWellknownOpenidConfiguration: {mockWellknownOpenidConfiguration: true} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Login query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(loginEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(loginEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.test.ts new file mode 100644 index 0000000000..d21d46ff11 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const ordersEndpoint = '/checkout/shopper-orders/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {parameters: {orderNo: 'orderNo'}} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + useOrder: {orderNo: 'orderNo'}, + usePaymentMethodsForOrder: {applicablePaymentMethods: []}, + useTaxesFromOrder: {taxes: {}} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Orders query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(ordersEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(ordersEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.test.ts new file mode 100644 index 0000000000..e5d31c9495 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const productsEndpoint = '/product/shopper-products/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {parameters: {id: 'id', ids: 'ids'}} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + useCategories: {data: [], limit: 0, total: 0}, + useCategory: {id: 'categoryId'}, + useProduct: {id: 'productId'}, + useProducts: {data: [], limit: 0, total: 0} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Products query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(productsEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(productsEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.test.ts new file mode 100644 index 0000000000..d74c49a4c1 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const promotionsEndpoint = '/pricing/shopper-promotions/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {parameters: {campaignId: 'campaignId', ids: 'a,b'}} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + usePromotions: {count: 0, data: [], total: 0}, + usePromotionsForCampaign: {count: 0, data: [], total: 0} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Promotions query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(promotionsEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(promotionsEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.test.ts new file mode 100644 index 0000000000..d2dec37651 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, 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, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Queries = typeof queries +const searchEndpoint = '/search/shopper-search/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {parameters: {q: 'something'}} + +/** Map of query name to returned data type */ +type DataType = NonNullable['data']> +type TestMap = {[K in keyof Queries]: DataType} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + // Type assertion so we don't need to use the full type + useProductSearch: {query: 'pants'} as DataType<'useProductSearch'>, + useSearchSuggestions: {searchPhrase: 'search phrase'} +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Search query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks().length).toBe(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(searchEndpoint, data) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(searchEndpoint, {}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(wait, () => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index 85b5d7a47d..d5dd31dbf9 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -117,7 +117,7 @@ export const mockMutationEndpoints = ( /** Mocks a GET request to an endpoint. */ export const mockQueryEndpoint = ( matchingPath: string, - response: string | Record | unknown[], + response: string | object | unknown[], statusCode = 200 ) => { const matcher = (uri: string) => uri.includes(matchingPath) From 0ae5349aab82414c085fb459536db841b5bae126 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 22 Feb 2023 23:38:58 -0500 Subject: [PATCH 085/122] Remove hooks for `authorizeCustomer` and `getTrustedAgentAuthorizationToken`. These endpoints modify headers, rather than mutate or return data, so they don't make sense for either query or mutation hooks. (At least, for our current implementation.) --- .../src/hooks/ShopperLogin/index.test.ts | 7 +- .../src/hooks/ShopperLogin/query.ts | 103 ------------------ .../commerce-sdk-react/src/test-utils.tsx | 16 ++- 3 files changed, 18 insertions(+), 108 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts index 03128c9a38..475b3084b3 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts @@ -12,7 +12,12 @@ import * as queries from './query' describe('Shopper Login hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperLogin, queries, ShopperLoginMutations) + expectAllEndpointsHaveHooks(ShopperLogin, queries, ShopperLoginMutations, [ + // These methods generate headers - they don't mutate or return any data, so they don't make + // sense as query/mutation hooks (as currently implemented). + 'authorizeCustomer', + 'getTrustedAgentAuthorizationToken' + ]) }) test('all mutation hooks have cache update logic', () => { diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts index 53dbaf27bd..ee80851234 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts @@ -50,109 +50,6 @@ export const useCredQualityUserInfo = ( requiredParameters }) } -/** - * A hook for `ShopperLogin#authorizeCustomer`. - * Get an authorization code after authenticating a user against an identity provider (IDP). This is the first step of the OAuth 2.1 authorization code flow, where a user can log in via federation to the IDP configured for the client. After successfully logging in, the user gets an authorization code via a redirect URI. - -This endpoint can be called from the front channel (the browser). - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=authorizeCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#authorizecustomer} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const useAuthorizeCustomer = ( - apiOptions: Argument, - queryOptions: ApiQueryOptions = {} -): UseQueryResult> => { - type Options = Argument - type Data = DataType - const {shopperLogin: client} = useCommerceApi() - const method = async (options: Options) => await client.authorizeCustomer(options) - const requiredParameters = [ - 'organizationId', - 'redirect_uri', - 'response_type', - 'client_id', - - 'code_challenge' - ] as const - const allParameters = [ - ...requiredParameters, - - 'scope', - 'state', - 'usid', - 'hint', - 'channel_id' - ] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order - // to generate the correct query key. - const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/oauth2/authorize', - parameters - ] as const - - // For some reason, if we don't explicitly set these generic parameters, the inferred type for - // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery(netOptions, queryOptions, { - method, - queryKey, - requiredParameters - }) -} -/** - * A hook for `ShopperLogin#getTrustedAgentAuthorizationToken`. - * Obtains a new agent on behalf authorization token for a registered customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getTrustedAgentAuthorizationToken} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#gettrustedagentauthorizationtoken} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. - */ -export const useTrustedAgentAuthorizationToken = ( - apiOptions: Argument, - queryOptions: ApiQueryOptions = {} -): UseQueryResult> => { - type Options = Argument - type Data = DataType - const {shopperLogin: client} = useCommerceApi() - const method = async (options: Options) => - await client.getTrustedAgentAuthorizationToken(options) - const requiredParameters = [ - 'organizationId', - 'client_id', - 'channel_id', - 'code_challenge', - 'login_id', - 'idp_origin', - 'redirect_uri', - 'response_type' - ] as const - const allParameters = [...requiredParameters] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order - // to generate the correct query key. - const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/oauth2/trusted-agent/authorize', - parameters - ] as const - - // For some reason, if we don't explicitly set these generic parameters, the inferred type for - // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery(netOptions, queryOptions, { - method, - queryKey, - requiredParameters - }) -} /** * A hook for `ShopperLogin#getUserInfo`. * Returns a JSON listing of claims about the currently authenticated user. diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index d5dd31dbf9..caeb6a3ae6 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -155,10 +155,18 @@ const getQueryName = (method: string): string => { return method.replace(/^./, (ltr) => `use${ltr.toUpperCase()}`) } -export const expectAllEndpointsHaveHooks = ( - SdkClass: {prototype: object}, +/** + * Validates that all endpoints have hooks implemented + * @param SdkClass Class constructor from commerce-sdk-isomorphic to use as a source for endpoints + * @param queryHooks Object containing implemented query hooks + * @param mutationsEnum Enum containing mutation endpoint names + * @param exceptions List of endpoints that are intentionally NOT implemented + */ +export const expectAllEndpointsHaveHooks = ( + SdkClass: {prototype: T}, queryHooks: Record, - mutationsEnum: Record = {} + mutationsEnum: Record = {}, + exceptions: Array = [] ) => { const unimplemented = new Set(Object.getOwnPropertyNames(SdkClass.prototype)) // Always present on a class; we can ignore @@ -171,7 +179,7 @@ export const expectAllEndpointsHaveHooks = ( if (queryName in queryHooks) unimplemented.delete(method) }) // Convert to array for easier comparison / better jest output - expect([...unimplemented]).toEqual([]) + expect([...unimplemented]).toEqual(exceptions) } /** Helper type for WaitForValueToChange with hooks */ type GetHookResult = () => From 45b63fd12e8e17fb5d3c0a49152626ae90d6a35b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 13:43:31 -0500 Subject: [PATCH 086/122] Update "not implemented" tests to check if cache update logic exists. --- .../src/hooks/ShopperBaskets/index.test.ts | 24 +++++++++++-------- .../src/hooks/ShopperContexts/index.test.ts | 13 +++------- .../src/hooks/ShopperCustomers/index.test.ts | 17 ++++++------- .../src/hooks/ShopperExperience/index.test.ts | 5 ++-- .../ShopperGiftCertificates/index.test.ts | 5 ++-- .../src/hooks/ShopperLogin/index.test.ts | 15 ++++-------- .../src/hooks/ShopperOrders/index.test.ts | 13 +++------- .../src/hooks/ShopperProducts/index.test.ts | 5 ++-- .../src/hooks/ShopperPromotions/index.test.ts | 5 ++-- .../src/hooks/ShopperSearch/index.test.ts | 5 ++-- .../commerce-sdk-react/src/test-utils.tsx | 24 +++++++++---------- 11 files changed, 59 insertions(+), 72 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts index 11cda45905..4e8617737a 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts @@ -5,20 +5,24 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperBaskets} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' -import {ShopperBasketsMutations} from './mutation' import * as queries from './query' describe('Shopper Baskets hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperBaskets, queries, ShopperBasketsMutations) - }) - - test('all mutation hooks have cache update logic', () => { - const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() - const mutations = Object.values(ShopperBasketsMutations).sort() - // If this test fails, add the missing mutation as a no-op with a TODO note - expect(cacheUpdates).toEqual(mutations) + const unimplemented = getUnimplementedEndpoints(ShopperBaskets, queries, cacheUpdateMatrix) + expect(unimplemented).toEqual([ + 'transferBasket', + 'addGiftCertificateItemToBasket', + 'removeGiftCertificateItemFromBasket', + 'updateGiftCertificateItemInBasket', + 'addTaxesForBasketItem', + 'addPriceBooksToBasket', + 'createShipmentForBasket', + 'removeShipmentFromBasket', + 'updateShipmentForBasket', + 'addTaxesForBasket' + ]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts index 09f87971e7..042c06a1c9 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts @@ -5,20 +5,13 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperContexts} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' -import {ShopperContextsMutations} from './mutation' import * as queries from './query' describe('Shopper Contexts hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperContexts, queries, ShopperContextsMutations) - }) - - test('all mutation hooks have cache update logic', () => { - const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() - const mutations = Object.values(ShopperContextsMutations).sort() - // If this test fails, add the missing mutation as a no-op with a TODO note - expect(cacheUpdates).toEqual(mutations) + const unimplemented = getUnimplementedEndpoints(ShopperContexts, queries, cacheUpdateMatrix) + expect(unimplemented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts index 807bdd4567..c156e4d2d4 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts @@ -5,20 +5,17 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperCustomers} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' -import {ShopperCustomersMutations} from './mutation' import * as queries from './query' describe('Shopper Customers hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperCustomers, queries, ShopperCustomersMutations) - }) - - test('all mutation hooks have cache update logic', () => { - const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() - const mutations = Object.values(ShopperCustomersMutations).sort() - // If this test fails, add the missing mutation as a no-op with a TODO note - expect(cacheUpdates).toEqual(mutations) + const unimplemented = getUnimplementedEndpoints( + ShopperCustomers, + queries, + cacheUpdateMatrix + ) + expect(unimplemented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts index 88ebb4e57d..f6d3af0583 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/index.test.ts @@ -5,11 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperExperience} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import * as queries from './query' describe('Shopper Experience hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperExperience, queries) + const unimplemtented = getUnimplementedEndpoints(ShopperExperience, queries) + expect(unimplemtented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts index 77d72684d5..d4d9e4b84b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/index.test.ts @@ -5,11 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperGiftCertificates} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import * as queries from './query' describe('Shopper GiftCertificates hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperGiftCertificates, queries) + const unimplemented = getUnimplementedEndpoints(ShopperGiftCertificates, queries) + expect(unimplemented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts index 475b3084b3..17b2796f7d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts @@ -5,25 +5,20 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperLogin} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' -import {ShopperLoginMutations} from './mutation' import * as queries from './query' describe('Shopper Login hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperLogin, queries, ShopperLoginMutations, [ + const unimplemented = getUnimplementedEndpoints(ShopperLogin, queries, cacheUpdateMatrix) + expect(unimplemented).toEqual([ + // TODO: implement + 'logoutCustomer', // These methods generate headers - they don't mutate or return any data, so they don't make // sense as query/mutation hooks (as currently implemented). 'authorizeCustomer', 'getTrustedAgentAuthorizationToken' ]) }) - - test('all mutation hooks have cache update logic', () => { - const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() - const mutations = Object.values(ShopperLoginMutations).sort() - // If this test fails, add the missing mutation as a no-op with a TODO note - expect(cacheUpdates).toEqual(mutations) - }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts index 061217b3f9..cbfdf47132 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts @@ -5,20 +5,13 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperOrders} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' -import {ShopperOrdersMutations} from './mutation' import * as queries from './query' describe('Shopper Orders hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperOrders, queries, ShopperOrdersMutations) - }) - - test('all mutation hooks have cache update logic', () => { - const cacheUpdates = Object.keys(cacheUpdateMatrix).sort() - const mutations = Object.values(ShopperOrdersMutations).sort() - // If this test fails, add the missing mutation as a no-op with a TODO note - expect(cacheUpdates).toEqual(mutations) + const unimplemented = getUnimplementedEndpoints(ShopperOrders, queries, cacheUpdateMatrix) + expect(unimplemented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts index 9e595c2428..87dfd6526d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/index.test.ts @@ -5,11 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperProducts} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import * as queries from './query' describe('Shopper Products hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperProducts, queries) + const unimplemented = getUnimplementedEndpoints(ShopperProducts, queries) + expect(unimplemented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts index dae48180fe..30b7a5ba7d 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/index.test.ts @@ -5,11 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperPromotions} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import * as queries from './query' describe('Shopper Promotions hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperPromotions, queries) + const unimplemented = getUnimplementedEndpoints(ShopperPromotions, queries) + expect(unimplemented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts index e80c3c4f22..a58fadd2ac 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/index.test.ts @@ -5,11 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ShopperSearch} from 'commerce-sdk-isomorphic' -import {expectAllEndpointsHaveHooks} from '../../test-utils' +import {getUnimplementedEndpoints} from '../../test-utils' import * as queries from './query' describe('Shopper Search hooks', () => { test('all endpoints have hooks', () => { - expectAllEndpointsHaveHooks(ShopperSearch, queries) + const unimplemented = getUnimplementedEndpoints(ShopperSearch, queries) + expect(unimplemented).toEqual([]) }) }) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index caeb6a3ae6..03bc9b625a 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -156,30 +156,30 @@ const getQueryName = (method: string): string => { } /** - * Validates that all endpoints have hooks implemented + * Gets the list of API endpoints that have not yet been implemented * @param SdkClass Class constructor from commerce-sdk-isomorphic to use as a source for endpoints * @param queryHooks Object containing implemented query hooks - * @param mutationsEnum Enum containing mutation endpoint names - * @param exceptions List of endpoints that are intentionally NOT implemented + * @param mutationsEnum Enum containing mutation endpoint names + * @returns List of endpoints that don't have a query or mutation hook */ -export const expectAllEndpointsHaveHooks = ( - SdkClass: {prototype: T}, - queryHooks: Record, - mutationsEnum: Record = {}, - exceptions: Array = [] +export const getUnimplementedEndpoints = ( + SdkClass: {prototype: object}, + queryHooks: object, + mutationCacheUpdates: object = {} ) => { const unimplemented = new Set(Object.getOwnPropertyNames(SdkClass.prototype)) // Always present on a class; we can ignore unimplemented.delete('constructor') - // Names of implemented mutation endpoints exist as values of the enum - Object.values(mutationsEnum).forEach((method) => unimplemented.delete(method)) + // Mutations endpoints are implemented if they have a defined cache update function + Object.entries(mutationCacheUpdates).forEach(([method, implementation]) => { + if (implementation) unimplemented.delete(method) + }) // Names of implemented query endpoints have been mangled when converted into hooks unimplemented.forEach((method) => { const queryName = getQueryName(method) if (queryName in queryHooks) unimplemented.delete(method) }) - // Convert to array for easier comparison / better jest output - expect([...unimplemented]).toEqual(exceptions) + return [...unimplemented] } /** Helper type for WaitForValueToChange with hooks */ type GetHookResult = () => From 48d399d2d9e7ffe7793e608e3206c6de529a19c4 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 14:17:50 -0500 Subject: [PATCH 087/122] Extract reused type into type def. --- packages/commerce-sdk-react/src/auth/index.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/auth/index.test.ts b/packages/commerce-sdk-react/src/auth/index.test.ts index 9f27e1a6ac..402c341e44 100644 --- a/packages/commerce-sdk-react/src/auth/index.test.ts +++ b/packages/commerce-sdk-react/src/auth/index.test.ts @@ -39,6 +39,9 @@ jest.mock('../utils', () => ({ onClient: () => true })) +/** The auth data we store has a slightly different shape than what we use. */ +type StoredAuthData = Omit & {refresh_token_guest?: string} + const config = { clientId: 'clientId', organizationId: 'organizationId', @@ -85,7 +88,7 @@ describe('Auth', () => { test('this.data returns the storage value', () => { const auth = new Auth(config) - const sample: Omit & {refresh_token_guest?: string} = { + const sample: StoredAuthData = { refresh_token_guest: 'refresh_token_guest', access_token: 'access_token', customer_id: 'customer_id', @@ -163,7 +166,7 @@ describe('Auth', () => { test('ready - re-use valid access token', () => { const auth = new Auth(config) - const data: Omit & {refresh_token_guest?: string} = { + const data: StoredAuthData = { refresh_token_guest: 'refresh_token_guest', access_token: jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret'), customer_id: 'customer_id', @@ -189,7 +192,7 @@ describe('Auth', () => { test('ready - use refresh token when access token is expired', async () => { const auth = new Auth(config) - const data: Omit & {refresh_token_guest?: string} = { + const data: StoredAuthData = { refresh_token_guest: 'refresh_token_guest', access_token: jwt.sign({exp: Math.floor(Date.now() / 1000) - 1000}, 'secret'), customer_id: 'customer_id', From 3ce0fa605e5176718ec5d3e00c94ceaf578038a4 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 14:24:10 -0500 Subject: [PATCH 088/122] Update comment to reflect changed tests. --- .../commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index 030a861b1d..aeddf9165c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -141,12 +141,8 @@ const updateBasketWithResponseBasketId = ( } } -const TODO = (method: keyof Client): undefined => { - // This is kind of a hacky way of passing both the "not implemented" tests in mutations.test.ts - // and the "all hooks have cache logic" test in index.test.ts. The former expects `undefined` - // as a value and the latter expects the key to exist, both of which are satisfied by setting - // an explicit `undefined`. So that's all that this does, plus logging a TODO warning. - // Hacky, but temporary! +/** Logs a warning to console (on startup) and returns nothing (method is unimplemented). */ +const TODO = (method: keyof Client) => { console.warn(`Cache logic for '${method}' is not yet implemented.`) return undefined } From 83c18d0aac6b470c27230e00a29b61b858fe8cab Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 14:24:24 -0500 Subject: [PATCH 089/122] Remove unnecessary `async`. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 1a3a3c4fbd..7935af9ee8 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -240,7 +240,7 @@ describe('ShopperBaskets mutations', () => { assertRemoveQuery(result.current.basket) assertInvalidateQuery(result.current.customerBaskets, oldCustomerBaskets) }) - test.each(notImplTestCases)('`%s` is not yet implemented', async (mutationName) => { + test.only.each(notImplTestCases)('`%s` is not yet implemented', (mutationName) => { expect(() => useShopperBasketsMutation(mutationName)).toThrow(NotImplementedError) }) }) From 32b958a2d989d1305b06f994b9860751453e1988 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 14:25:07 -0500 Subject: [PATCH 090/122] Implement Shopper Contexts mutation tests. --- .../src/hooks/ShopperContexts/cache.ts | 7 +++--- .../hooks/ShopperContexts/mutation.test.ts | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/cache.ts index d837668916..f10f8fedee 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/cache.ts @@ -5,12 +5,13 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ApiClients, CacheUpdateMatrix} from '../types' -import {NotImplementedError} from '../utils' type Client = ApiClients['shopperContexts'] -const TODO = (method: keyof Client) => () => { - throw new NotImplementedError(`Cache logic for '${method}'`) +/** Logs a warning to console (on startup) and returns nothing (method is unimplemented). */ +const TODO = (method: keyof Client) => { + console.warn(`Cache logic for '${method}' is not yet implemented.`) + return undefined } export const cacheUpdateMatrix: CacheUpdateMatrix = { updateShopperContext: TODO('updateShopperContext'), diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.test.ts new file mode 100644 index 0000000000..5981d92f21 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023, 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 {NotImplementedError} from '../utils' +import {ShopperContextsMutation, useShopperContextsMutation} from './mutation' + +describe('Shopper Contexts mutation hooks', () => { + // Not implemented checks are temporary to make sure we don't forget to add tests when adding + // implentations. When all mutations are added, the "not implemented" tests can be removed. + const notImplTestCases: ShopperContextsMutation[] = [ + 'createShopperContext', + 'deleteShopperContext', + 'updateShopperContext' + ] + test.each(notImplTestCases)('`%s` is not yet implemented', async (mutationName) => { + expect(() => useShopperContextsMutation(mutationName)).toThrow(NotImplementedError) + }) +}) From c2286d96b48e0a3f2b40d2f3bf16ad02441837de Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 14:26:50 -0500 Subject: [PATCH 091/122] Rename `makeOptions` to `createOptions`. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 7935af9ee8..0ace2a92bd 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -35,7 +35,7 @@ type Basket = ShopperBasketsTypes.Basket type BasketsResult = ShopperCustomersTypes.BasketsResult /** Create an options object for Shopper Baskets endpoints, with `basketId` pre-filled. */ -const makeOptions = >( +const createOptions = >( body: Argument extends {body: infer B} ? B : undefined, parameters: Omit['parameters'], 'basketId'> ): Argument => ({ @@ -46,7 +46,7 @@ const makeOptions = >( // --- getBasket constants --- // const basketsEndpoint = '/checkout/shopper-baskets/' const BASKET_ID = 'basket_id' -const getBasketOptions = makeOptions<'getBasket'>(undefined, {}) +const getBasketOptions = createOptions<'getBasket'>(undefined, {}) const oldBasket: Basket = {basketId: BASKET_ID, mockData: 'old basket'} const newBasket: Basket = {basketId: BASKET_ID, mockData: 'new basket'} // --- getCustomerBaskets constants --- // @@ -81,36 +81,42 @@ type NonDeleteMutation = Exclude // TODO: Remove optional flag when all mutations are implemented type TestMap = {[Mut in NonDeleteMutation]?: Argument} const testMap: TestMap = { - addCouponToBasket: makeOptions<'addCouponToBasket'>({code: 'coupon'}, {}), - addItemToBasket: makeOptions<'addItemToBasket'>([], {}), - addPaymentInstrumentToBasket: makeOptions<'addPaymentInstrumentToBasket'>({}, {}), - createBasket: makeOptions<'createBasket'>({}, {}), - mergeBasket: makeOptions<'mergeBasket'>(undefined, {}), - removeCouponFromBasket: makeOptions<'removeCouponFromBasket'>(undefined, { + addCouponToBasket: createOptions<'addCouponToBasket'>({code: 'coupon'}, {}), + addItemToBasket: createOptions<'addItemToBasket'>([], {}), + addPaymentInstrumentToBasket: createOptions<'addPaymentInstrumentToBasket'>({}, {}), + createBasket: createOptions<'createBasket'>({}, {}), + mergeBasket: createOptions<'mergeBasket'>(undefined, {}), + removeCouponFromBasket: createOptions<'removeCouponFromBasket'>(undefined, { couponItemId: 'couponIemId' }), - removeItemFromBasket: makeOptions<'removeItemFromBasket'>(undefined, {itemId: 'itemId'}), - removePaymentInstrumentFromBasket: makeOptions<'removePaymentInstrumentFromBasket'>(undefined, { - paymentInstrumentId: 'paymentInstrumentId' - }), - updateBasket: makeOptions<'updateBasket'>({}, {}), - updateBillingAddressForBasket: makeOptions<'updateBillingAddressForBasket'>({}, {}), - updateCustomerForBasket: makeOptions<'updateCustomerForBasket'>({email: 'customer@email'}, {}), - updateItemInBasket: makeOptions<'updateItemInBasket'>({}, {itemId: 'itemId'}), - updatePaymentInstrumentInBasket: makeOptions<'updatePaymentInstrumentInBasket'>( + removeItemFromBasket: createOptions<'removeItemFromBasket'>(undefined, {itemId: 'itemId'}), + removePaymentInstrumentFromBasket: createOptions<'removePaymentInstrumentFromBasket'>( + undefined, + { + paymentInstrumentId: 'paymentInstrumentId' + } + ), + updateBasket: createOptions<'updateBasket'>({}, {}), + updateBillingAddressForBasket: createOptions<'updateBillingAddressForBasket'>({}, {}), + updateCustomerForBasket: createOptions<'updateCustomerForBasket'>( + {email: 'customer@email'}, + {} + ), + updateItemInBasket: createOptions<'updateItemInBasket'>({}, {itemId: 'itemId'}), + updatePaymentInstrumentInBasket: createOptions<'updatePaymentInstrumentInBasket'>( {}, {paymentInstrumentId: 'paymentInstrumentId'} ), - updateShippingAddressForShipment: makeOptions<'updateShippingAddressForShipment'>( + updateShippingAddressForShipment: createOptions<'updateShippingAddressForShipment'>( {}, {shipmentId: 'shipmentId'} ), - updateShippingMethodForShipment: makeOptions<'updateShippingMethodForShipment'>( + updateShippingMethodForShipment: createOptions<'updateShippingMethodForShipment'>( {id: 'ship'}, {shipmentId: 'shipmentId'} ) } -const deleteTestCase = ['deleteBasket', makeOptions<'deleteBasket'>(undefined, {})] as const +const deleteTestCase = ['deleteBasket', createOptions<'deleteBasket'>(undefined, {})] as const // Type assertion because the built-in type definition for `Object.entries` is limited :\ const nonDeleteTestCases = Object.entries(testMap) as Array< From 271d3a201a94be76c0d7eeae109647df76488388 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 15:11:29 -0500 Subject: [PATCH 092/122] Implement Shopper Login mutation tests. --- .../src/hooks/ShopperLogin/mutation.test.ts | 115 ++++++++++++++++++ .../commerce-sdk-react/src/test-utils.tsx | 2 +- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts new file mode 100644 index 0000000000..09841b4e93 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023, 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 {act} from '@testing-library/react' +import {ShopperLoginTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + mockMutationEndpoints, + renderHookWithProviders, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import {ApiClients, Argument, DataType} from '../types' +import {NotImplementedError} from '../utils' +import {ShopperLoginMutation, useShopperLoginMutation} from './mutation' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Client = ApiClients['shopperLogin'] +const loginEndpoint = '/shopper/auth/' +// Additional properties are ignored, so we can use this mega-options object for all endpoints +const OPTIONS = { + body: { + agent_id: 'agent_id', + channel_id: 'channel_id', + client_id: 'client_id', + code: 'code', + code_challenge: 'code_challenge', + code_verifier: 'code_verifier', + dwsid: 'dwsid', + grant_type: 'grant_type', + hint: 'hint', + idp_origin: 'idp_origin', + login_id: 'login_id', + mode: 'mode', + new_password: 'new_password', + pwd_action_token: 'pwd_action_token', + pwdless_login_token: 'pwdless_login_token', + redirect_uri: 'redirect_uri', + token: 'token', + user_id: 'user_id' + } +} +const TOKEN_RESPONSE: ShopperLoginTypes.TokenResponse = { + access_token: 'access_token', + customer_id: 'customer_id', + enc_user_id: 'enc_user_id', + expires_in: 0, + id_token: 'id_token', + refresh_token: 'refresh_tone', + token_type: 'token_type', + usid: 'usid' +} + +// --- TEST CASES --- // +type Implemented = Exclude +// This is an object rather than an array to more easily ensure we cover all mutations +type TestMap = {[Mut in Implemented]: [Argument, DataType]} +const testMap: TestMap = { + authenticateCustomer: [OPTIONS, undefined], + authorizePasswordlessCustomer: [OPTIONS, {}], + getAccessToken: [OPTIONS, TOKEN_RESPONSE], + getPasswordLessAccessToken: [OPTIONS, TOKEN_RESPONSE], + getPasswordResetToken: [OPTIONS, undefined], + getSessionBridgeAccessToken: [OPTIONS, TOKEN_RESPONSE], + getTrustedAgentAccessToken: [OPTIONS, TOKEN_RESPONSE], + getTrustedSystemAccessToken: [OPTIONS, TOKEN_RESPONSE], + introspectToken: [OPTIONS, {}], + resetPassword: [OPTIONS, undefined], + revokeToken: [OPTIONS, TOKEN_RESPONSE] +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[Implemented, TestMap[Implemented]]> + +// Not implemented checks are temporary to make sure we don't forget to add tests when adding +// implentations. When all mutations are added, the "not implemented" tests can be removed, +// and the `TestMap` type can be changed from optional keys to required keys. Doing so will +// leverage TypeScript to enforce having tests for all mutations. +const notImplTestCases = ['logoutCustomer'] as const + +describe('ShopperBaskets mutations', () => { + beforeEach(() => nock.cleanAll()) + test.each(testCases)('`%s` returns data on success', async (mutationName, [options, data]) => { + mockMutationEndpoints(loginEndpoint, data ?? {}) // Fallback for `void` endpoints + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperLoginMutation(mutationName) + }) + expect(result.current.data).toBeUndefined() + act(() => result.current.mutate(options)) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + }) + test.each(testCases)('`%s` returns error on error', async (mutationName, [options]) => { + mockMutationEndpoints(loginEndpoint, {error: true}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperLoginMutation(mutationName) + }) + expect(result.current.error).toBeNull() + act(() => result.current.mutate(options)) + await waitAndExpectError(wait, () => result.current) + // Validate that we get a `ResponseError` from commerce-sdk-isomorphic. Ideally, we could do + // `.toBeInstanceOf(ResponseError)`, but the class isn't exported. :\ + expect(result.current.error).toHaveProperty('response') + }) + test.each(notImplTestCases)('`%s` is not yet implemented', (mutationName) => { + expect(() => useShopperLoginMutation(mutationName)).toThrow(NotImplementedError) + }) +}) diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index 03bc9b625a..50d57bd783 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -101,7 +101,7 @@ export function renderHookWithProviders( /** Mocks DELETE, PATCH, POST, and PUT so we don't have to look up which verb an endpoint uses. */ export const mockMutationEndpoints = ( matchingPath: string, - response: string | Record, + response: string | object, statusCode = 200 ) => { const matcher = (uri: string) => uri.includes(matchingPath) From a4ba2e5a565337dbd2e351843bcb2406105acf38 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 15:11:49 -0500 Subject: [PATCH 093/122] Add extra ResponseError assertion. --- .../src/hooks/ShopperBaskets/mutation.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts index 0ace2a92bd..373e9eb9ee 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.test.ts @@ -120,7 +120,7 @@ const deleteTestCase = ['deleteBasket', createOptions<'deleteBasket'>(undefined, // Type assertion because the built-in type definition for `Object.entries` is limited :\ const nonDeleteTestCases = Object.entries(testMap) as Array< - [ShopperBasketsMutation, Argument] + [NonDeleteMutation, Argument] > // Most test cases only apply to non-delete test cases, some (error handling) can include deleteBasket const allTestCases = [...nonDeleteTestCases, deleteTestCase] @@ -165,13 +165,16 @@ describe('ShopperBaskets mutations', () => { expect(result.current.data).toEqual(oldBasket) }) test.each(allTestCases)('`%s` returns error on error', async (mutationName, options) => { - mockMutationEndpoints(basketsEndpoint, {}, 400) + mockMutationEndpoints(basketsEndpoint, {error: true}, 400) const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { return useShopperBasketsMutation(mutationName) }) expect(result.current.error).toBeNull() act(() => result.current.mutate(options)) await waitAndExpectError(wait, () => result.current) + // Validate that we get a `ResponseError` from commerce-sdk-isomorphic. Ideally, we could do + // `.toBeInstanceOf(ResponseError)`, but the class isn't exported. :\ + expect(result.current.error).toHaveProperty('response') }) test.each(nonDeleteTestCases)( '`%s` updates the cache on success', @@ -198,7 +201,7 @@ describe('ShopperBaskets mutations', () => { async (mutationName, options) => { mockQueryEndpoint(basketsEndpoint, oldBasket) // getBasket mockQueryEndpoint(customersEndpoint, oldCustomerBaskets) // getCustomerBaskets - mockMutationEndpoints(basketsEndpoint, {}, 400) // this mutation + mockMutationEndpoints(basketsEndpoint, {error: true}, 400) // this mutation const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ basket: queries.useBasket(getBasketOptions), customerBaskets: useCustomerBaskets(getCustomerBasketsOptions), @@ -210,6 +213,9 @@ describe('ShopperBaskets mutations', () => { expect(result.current.mutation.error).toBeNull() act(() => result.current.mutation.mutate(options)) await waitAndExpectError(wait, () => result.current.mutation) + // Validate that we get a `ResponseError` from commerce-sdk-isomorphic. Ideally, we could do + // `.toBeInstanceOf(ResponseError)`, but the class isn't exported. :\ + expect(result.current.mutation.error).toHaveProperty('response') assertUpdateQuery(result.current.basket, oldBasket) assertUpdateQuery(result.current.customerBaskets, oldCustomerBaskets) } From 2972eb2aa29f3b609507526c9487a5978681baca Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 15:20:01 -0500 Subject: [PATCH 094/122] Update test names. --- .../commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts | 2 +- .../commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts index 479beed444..76a944ba7a 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.test.ts @@ -32,7 +32,7 @@ const testMap: TestMap = { } // Type assertion is necessary because `Object.entries` is limited const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> -describe('Shopper Baskets query hooks', () => { +describe('Shopper Contexts query hooks', () => { beforeEach(() => nock.cleanAll()) afterEach(() => { expect(nock.pendingMocks().length).toBe(0) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts index 09841b4e93..d806fdeec7 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts @@ -85,7 +85,7 @@ const testCases = Object.entries(testMap) as Array<[Implemented, TestMap[Impleme // leverage TypeScript to enforce having tests for all mutations. const notImplTestCases = ['logoutCustomer'] as const -describe('ShopperBaskets mutations', () => { +describe('ShopperLogin mutations', () => { beforeEach(() => nock.cleanAll()) test.each(testCases)('`%s` returns data on success', async (mutationName, [options, data]) => { mockMutationEndpoints(loginEndpoint, data ?? {}) // Fallback for `void` endpoints From f78a1c192e84be00ba0f69eae02885091defdacc Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 16:51:47 -0500 Subject: [PATCH 095/122] Implement Shopper Orders mutation tests. --- .../src/hooks/ShopperOrders/cache.ts | 8 +- .../src/hooks/ShopperOrders/mutation.test.ts | 130 ++++++++++++++ .../src/hooks/ShopperOrders/mutation.test.tsx | 161 ------------------ 3 files changed, 135 insertions(+), 164 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts index 2d16d8261d..708c40ee9a 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ApiClients, CacheUpdateMatrix, CacheUpdateUpdate, CacheUpdateInvalidate} from '../types' -import {and, matchesApiConfig, matchesPath, NotImplementedError} from '../utils' +import {and, matchesApiConfig, matchesPath} from '../utils' type Client = ApiClients['shopperOrders'] @@ -14,8 +14,10 @@ const basePath = (parameters: Client['clientConfig']['parameters']) => [ parameters.organizationId ] -const TODO = (method: keyof Client) => () => { - throw new NotImplementedError(`Cache logic for '${method}'`) +/** Logs a warning to console (on startup) and returns nothing (method is unimplemented). */ +const TODO = (method: keyof Client) => { + console.warn(`Cache logic for '${method}' is not yet implemented.`) + return undefined } export const cacheUpdateMatrix: CacheUpdateMatrix = { diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts new file mode 100644 index 0000000000..1b78aba35f --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023, 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 {useQueryClient} from '@tanstack/react-query' +import {act} from '@testing-library/react' +import {ShopperOrdersTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + assertInvalidateQuery, + assertRemoveQuery, + assertUpdateQuery, + mockMutationEndpoints, + mockQueryEndpoint, + renderHookWithProviders, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import {useCustomerBaskets} from '../ShopperCustomers' +import {ApiClients, Argument} from '../types' +import {NotImplementedError} from '../utils' +import {ShopperOrdersMutation, useShopperOrdersMutation} from './mutation' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Client = ApiClients['shopperOrders'] +const ordersEndpoint = '/checkout/shopper-orders/' +const OPTIONS: Argument = {body: {basketId: 'basketId'}} +const ORDER: ShopperOrdersTypes.Order = {orderNo: '123', productItems: []} + +// --- TEST CASES --- // +/** Every mutation modifies an existing order, except `createOrder`, which creates one. */ +type NonCreateMutation = Exclude +// This is an object rather than an array to more easily ensure we cover all mutations +// TODO: Remove optional flag when all mutations are implemented +type TestMap = {[Mut in NonCreateMutation]?: Argument} +const testMap: TestMap = {} + +// Type assertion because the built-in type definition for `Object.entries` is limited :\ +const nonCreateTestCases = Object.entries(testMap) as ReadonlyArray< + [NonCreateMutation, TestMap[NonCreateMutation]] +> +const createTestCase = ['createOrder', OPTIONS] as const +const allTestCases = [...nonCreateTestCases, createTestCase] + +// Not implemented checks are temporary to make sure we don't forget to add tests when adding +// implentations. When all mutations are added, the "not implemented" tests can be removed, +// and the `TestMap` type can be changed from optional keys to required keys. Doing so will +// leverage TypeScript to enforce having tests for all mutations. +const notImplTestCases: ShopperOrdersMutation[][] = [ + ['createPaymentInstrumentForOrder'], + ['removePaymentInstrumentFromOrder'], + ['updatePaymentInstrumentForOrder'] +] + +describe('ShopperOrders mutations', () => { + beforeEach(() => nock.cleanAll()) + test.each(allTestCases)('`%s` returns data on success', async (mutationName, options) => { + mockMutationEndpoints(ordersEndpoint, ORDER) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperOrdersMutation(mutationName) + }) + expect(result.current.data).toBeUndefined() + act(() => { + // I'm not sure why this type assertion is necessary... :\ + type Opts = Parameters[0] + result.current.mutate(options as Opts) + }) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(ORDER) + }) + test.each(allTestCases)('`%s` returns error on error', async (mutationName) => { + mockMutationEndpoints(ordersEndpoint, {error: true}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperOrdersMutation(mutationName) + }) + expect(result.current.error).toBeNull() + act(() => result.current.mutate({body: {}})) + await waitAndExpectError(wait, () => result.current) + // Validate that we get a `ResponseError` from commerce-sdk-isomorphic. Ideally, we could do + // `.toBeInstanceOf(ResponseError)`, but the class isn't exported. :\ + expect(result.current.error).toHaveProperty('response') + }) + test('`createOrder` updates the cache on success', async () => { + const [mutationName, options] = createTestCase + mockMutationEndpoints(ordersEndpoint, ORDER) // createOrder + mockQueryEndpoint(ordersEndpoint, ORDER) // getOrder + const {result: mut, waitForValueToChange: wait} = renderHookWithProviders(() => ({ + queryClient: useQueryClient(), + mutation: useShopperOrdersMutation(mutationName) + })) + const cached = mut.current.queryClient.getQueriesData({type: 'all'}) + // The query cache should be empty before we do anything + expect(cached).toEqual([]) + act(() => mut.current.mutation.mutate(options)) + await waitAndExpectSuccess(wait, () => mut.current.mutation) + const {result: query} = renderHookWithProviders(() => + // We know `ORDER` has an `orderNo` because we set it, but the `Order` type forgets that + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + queries.useOrder({parameters: {orderNo: ORDER.orderNo!}}) + ) + await waitAndExpectSuccess(wait, () => query.current) + expect(query.current.data).toEqual(ORDER) + }) + test('`createOrder` does not update the cache on error', async () => { + const [mutationName, options] = createTestCase + mockMutationEndpoints(ordersEndpoint, {error: true}, 400) // createOrder + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => ({ + queryClient: useQueryClient(), + mutation: useShopperOrdersMutation(mutationName) + })) + const getQueries = () => result.current.queryClient.getQueriesData({type: 'all'}) + // The query cache should be empty before we do anything + expect(getQueries()).toEqual([]) + act(() => result.current.mutation.mutate(options)) + await waitAndExpectError(wait, () => result.current.mutation) + // The query cache should not have changed + expect(getQueries()).toEqual([]) + }) + test.each(notImplTestCases)('`%s` is not yet implemented', async (mutationName) => { + expect(() => useShopperOrdersMutation(mutationName)).toThrow(NotImplementedError) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx deleted file mode 100644 index e394ba01f9..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2023, 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 - */ -import React from 'react' -import {renderWithProviders, DEFAULT_TEST_HOST, createQueryClient} from '../../test-utils' -import {fireEvent, screen, waitFor} from '@testing-library/react' -import {useShopperOrdersMutation, ShopperOrdersMutation} from './mutation' -import nock from 'nock' -import {ApiClients, Argument} from '../types' - -jest.mock('../../auth/index.ts', () => { - return jest.fn().mockImplementation(() => ({ - ready: jest.fn().mockResolvedValue({access_token: '123'}) - })) -}) - -const BASKET_ID = '12345' - -type MutationPayloads = { - [Mutation in ShopperOrdersMutation]?: Argument -} - -const mutationPayloads: MutationPayloads = { - createOrder: { - body: {basketId: BASKET_ID}, - parameters: {} - } -} - -interface OrderMutationComponentParams { - action: ShopperOrdersMutation -} - -const OrderMutationComponent = ({action}: OrderMutationComponentParams) => { - const mutationHook = useShopperOrdersMutation(action) - const error = mutationHook.error as Error | undefined - const payload = mutationPayloads[action] - if (!payload) throw new Error(`Missing payload for ${action}`) - - return ( -
- - - {error?.message &&

Error: {error.message}

} -
- {mutationHook.isSuccess && isSuccess} -
- ) -} - -const tests = (Object.keys(mutationPayloads) as ShopperOrdersMutation[]).map((mutationName) => { - return { - hook: mutationName, - cases: [ - { - name: 'success', - assertions: async () => { - nock(DEFAULT_TEST_HOST) - .post((uri) => { - return uri.includes('/checkout/shopper-orders/') - }) - .reply(200, {}) - - const queryClient = createQueryClient() - - const mutation = shopperOrdersCacheUpdateMatrix[mutationName] - - const {invalidate, update, remove} = mutation( - mutationPayloads[mutationName], - {} - ) - - const queryKeys = [...(invalidate || []), ...(update || []), ...(remove || [])] - - queryKeys.forEach(({key: queryKey}) => { - queryClient.setQueryData(queryKey, {test: true}) - }) - - renderWithProviders( - , - {queryClient} - ) - - await waitFor(() => - screen.getByRole('button', { - name: mutationName - }) - ) - - const button = screen.getByRole('button', { - name: mutationName - }) - - fireEvent.click(button) - await waitFor(() => screen.getByText(/isSuccess/i)) - expect(screen.getByText(/isSuccess/i)).toBeInTheDocument() - - // Assert changes in cache - update?.forEach(({key: queryKey}) => { - expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBeFalsy() - }) - invalidate?.forEach(({key: queryKey}) => { - expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBeTruthy() - }) - remove?.forEach(({key: queryKey}) => { - expect(queryClient.getQueryState(queryKey)).toBeFalsy() - }) - } - }, - { - name: 'error', - assertions: async () => { - nock(DEFAULT_TEST_HOST) - .post((uri) => { - return uri.includes('/checkout/shopper-orders/') - }) - .reply(500) - - renderWithProviders( - - ) - await waitFor(() => - screen.getByRole('button', { - name: mutationName - }) - ) - - const button = screen.getByRole('button', { - name: mutationName - }) - fireEvent.click(button) - await waitFor(() => screen.getByText(/error/i)) - expect(screen.getByText(/error/i)).toBeInTheDocument() - } - } - ] - } -}) - -tests.forEach(({hook, cases}) => { - describe(hook, () => { - beforeEach(() => { - jest.clearAllMocks() - }) - cases.forEach(({name, assertions}) => { - test(name, assertions) - }) - }) -}) - -// TODO: Methods that haven't been implemented are no longer an explicit list, -// but are implicitly derived from their absence in the `cacheUpdateMatrix` of implementations. -test.each([])('%j - throws error when not implemented', (methodName) => { - const action = methodName as ShopperOrdersMutation - expect(() => { - useShopperOrdersMutation(action) - }).toThrowError('This method is not implemented.') -}) From 4a6409d86c244f294f2ea0a907b8df597c080db8 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 17:45:15 -0500 Subject: [PATCH 096/122] Convert TODO from throwing to just logging. --- .../src/hooks/ShopperCustomers/cache.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts index a926907a6b..515886a695 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts @@ -5,15 +5,16 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {ApiClientConfigParams, ApiClients, CacheUpdateMatrix} from '../types' -import {and, matchesApiConfig, matchesPath, NotImplementedError, pathStartsWith} from '../utils' +import {and, matchesApiConfig, matchesPath, pathStartsWith} from '../utils' type Client = ApiClients['shopperCustomers'] const noop = () => ({}) -const TODO = (method: keyof Client) => () => { - throw new NotImplementedError(`Cache logic for '${method}'`) +/** Logs a warning to console (on startup) and returns nothing (method is unimplemented). */ +const TODO = (method: keyof Client) => { + console.warn(`Cache logic for '${method}' is not yet implemented.`) + return undefined } - // Path helpers (avoid subtle typos!) const getCustomerPath = (customerId: string, parameters: ApiClientConfigParams) => [ '/organizations/', From 4d856af5b97ace01bcf11a49ba2e7229ab817197 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 17:47:34 -0500 Subject: [PATCH 097/122] Fix failing "all endpoints have hooks" tests. --- .../src/hooks/ShopperContexts/index.test.ts | 6 +++++- .../src/hooks/ShopperCustomers/index.test.ts | 10 +++++++++- .../src/hooks/ShopperOrders/index.test.ts | 6 +++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts index 042c06a1c9..91558de24a 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts @@ -12,6 +12,10 @@ import * as queries from './query' describe('Shopper Contexts hooks', () => { test('all endpoints have hooks', () => { const unimplemented = getUnimplementedEndpoints(ShopperContexts, queries, cacheUpdateMatrix) - expect(unimplemented).toEqual([]) + expect(unimplemented).toEqual([ + 'createShopperContext', + 'deleteShopperContext', + 'updateShopperContext' + ]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts index c156e4d2d4..b3a592c119 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts @@ -16,6 +16,14 @@ describe('Shopper Customers hooks', () => { queries, cacheUpdateMatrix ) - expect(unimplemented).toEqual([]) + expect(unimplemented).toEqual([ + 'invalidateCustomerAuth', + 'authorizeCustomer', + 'authorizeTrustedSystem', + 'registerExternalProfile', + 'updateCustomerPassword', + 'deleteCustomerProductList', + 'updateCustomerProductList' + ]) }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts index cbfdf47132..f57795fd15 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts @@ -12,6 +12,10 @@ import * as queries from './query' describe('Shopper Orders hooks', () => { test('all endpoints have hooks', () => { const unimplemented = getUnimplementedEndpoints(ShopperOrders, queries, cacheUpdateMatrix) - expect(unimplemented).toEqual([]) + expect(unimplemented).toEqual([ + 'createPaymentInstrumentForOrder', + 'removePaymentInstrumentFromOrder', + 'updatePaymentInstrumentForOrder' + ]) }) }) From 2d661180c419180038954cd0674da4feacffdac1 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Thu, 23 Feb 2023 17:49:18 -0500 Subject: [PATCH 098/122] Remove unused imports. --- .../src/hooks/ShopperOrders/mutation.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts index 1b78aba35f..38f6f18ca9 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.test.ts @@ -9,16 +9,12 @@ import {act} from '@testing-library/react' import {ShopperOrdersTypes} from 'commerce-sdk-isomorphic' import nock from 'nock' import { - assertInvalidateQuery, - assertRemoveQuery, - assertUpdateQuery, mockMutationEndpoints, mockQueryEndpoint, renderHookWithProviders, waitAndExpectError, waitAndExpectSuccess } from '../../test-utils' -import {useCustomerBaskets} from '../ShopperCustomers' import {ApiClients, Argument} from '../types' import {NotImplementedError} from '../utils' import {ShopperOrdersMutation, useShopperOrdersMutation} from './mutation' From 8246a6b6f07c4eccf0c442400ce220bd256d5802 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 24 Feb 2023 12:49:15 -0500 Subject: [PATCH 099/122] Introduce query key helpers. --- .../src/hooks/ShopperBaskets/query.ts | 89 ++---- .../src/hooks/ShopperBaskets/queryKey.ts | 138 ++++++++ .../hooks/ShopperBaskets/queryKeyHelpers.ts | 136 ++++++++ .../src/hooks/ShopperContexts/query.ts | 19 +- .../hooks/ShopperContexts/queryKeyHelpers.ts | 45 +++ .../src/hooks/ShopperCustomers/query.ts | 216 +++---------- .../hooks/ShopperCustomers/queryKeyHelpers.ts | 299 ++++++++++++++++++ .../src/hooks/ShopperExperience/query.ts | 40 +-- .../ShopperExperience/queryKeyHelpers.ts | 64 ++++ .../hooks/ShopperGiftCertificates/query.ts | 18 +- .../queryKeyHelpers.ts | 44 +++ .../src/hooks/ShopperLogin/query.ts | 63 ++-- .../src/hooks/ShopperLogin/queryKeyHelpers.ts | 83 +++++ .../src/hooks/ShopperOrders/query.ts | 53 +--- .../hooks/ShopperOrders/queryKeyHelpers.ts | 85 +++++ .../src/hooks/ShopperProducts/query.ts | 81 ++--- .../hooks/ShopperProducts/queryKeyHelpers.ts | 91 ++++++ .../src/hooks/ShopperPromotions/query.ts | 34 +- .../ShopperPromotions/queryKeyHelpers.ts | 68 ++++ .../src/hooks/ShopperSearch/query.ts | 43 +-- .../hooks/ShopperSearch/queryKeyHelpers.ts | 67 ++++ .../commerce-sdk-react/src/hooks/types.ts | 8 +- 22 files changed, 1288 insertions(+), 496 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts index 8bf697f663..710a002c4f 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperBaskets'] @@ -26,22 +27,14 @@ export const useBasket = ( type Options = Argument type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Options) => await client.getBasket(options) + const methodName = 'getBasket' const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - const allParameters = [...requiredParameters, 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -65,23 +58,14 @@ export const usePaymentMethodsForBasket = ( type Options = Argument type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Options) => await client.getPaymentMethodsForBasket(options) + const methodName = 'getPaymentMethodsForBasket' const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - const allParameters = [...requiredParameters, 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/payment-methods', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -105,23 +89,14 @@ export const usePriceBooksForBasket = ( type Options = Argument type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Options) => await client.getPriceBooksForBasket(options) + const methodName = 'getPriceBooksForBasket' const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/price-books', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -145,25 +120,14 @@ export const useShippingMethodsForShipment = ( type Options = Argument type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Options) => await client.getShippingMethodsForShipment(options) + const methodName = 'getShippingMethodsForShipment' const requiredParameters = ['organizationId', 'basketId', 'shipmentId', 'siteId'] as const - const allParameters = [...requiredParameters, 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/shipments/', - parameters.shipmentId, - '/shipping-methods', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -187,23 +151,14 @@ export const useTaxesFromBasket = ( type Options = Argument type Data = DataType const {shopperBaskets: client} = useCommerceApi() - const method = async (options: Options) => await client.getTaxesFromBasket(options) + const methodName = 'getTaxesFromBasket' const requiredParameters = ['organizationId', 'basketId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/baskets/', - parameters.basketId, - '/taxes', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts new file mode 100644 index 0000000000..b8c8475a4c --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023, 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 type {ShopperBaskets} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperBaskets<{shortCode: string}> +type Params> = NonNullable< + Argument['parameters'] +> +export type QueryKeys = { + getBasket: readonly ['/organizations/', string, '/baskets/', string, Params<'getBasket'>] + getPaymentMethodsForBasket: readonly [ + '/organizations/', + string, + '/baskets/', + string, + '/payment-methods', + Params<'getPaymentMethodsForBasket'> + ] + getPriceBooksForBasket: readonly [ + '/organizations/', + string, + '/baskets/', + string, + '/price-books', + Params<'getPriceBooksForBasket'> + ] + getShippingMethodsForShipment: readonly [ + '/organizations/', + string, + '/baskets/', + string, + '/shipments/', + string, + '/shipping-methods', + Params<'getShippingMethodsForShipment'> + ] + getTaxesFromBasket: readonly [ + '/organizations/', + string, + '/baskets/', + string, + '/taxes', + Params<'getTaxesFromBasket'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getBasket: QueryKeyHelper<'getBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId', 'locale']), + path: (params) => ['/organizations/', params.organizationId, '/baskets/', params.basketId], + queryKey: (params: Params<'getBasket'>) => [ + ...getBasket.path(params), + getBasket.parameters(params) + ] +} + +export const getPaymentMethodsForBasket: QueryKeyHelper<'getPaymentMethodsForBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId', 'locale']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/payment-methods' + ], + queryKey: (params: Params<'getPaymentMethodsForBasket'>) => [ + ...getPaymentMethodsForBasket.path(params), + getPaymentMethodsForBasket.parameters(params) + ] +} + +export const getPriceBooksForBasket: QueryKeyHelper<'getPriceBooksForBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/price-books' + ], + queryKey: (params: Params<'getPriceBooksForBasket'>) => [ + ...getPriceBooksForBasket.path(params), + getPriceBooksForBasket.parameters(params) + ] +} + +export const getShippingMethodsForShipment: QueryKeyHelper<'getShippingMethodsForShipment'> = { + parameters: (params) => + pick(params, ['organizationId', 'basketId', 'shipmentId', 'siteId', 'locale']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/shipments/', + params.shipmentId, + '/shipping-methods' + ], + queryKey: (params: Params<'getShippingMethodsForShipment'>) => [ + ...getShippingMethodsForShipment.path(params), + getShippingMethodsForShipment.parameters(params) + ] +} + +export const getTaxesFromBasket: QueryKeyHelper<'getTaxesFromBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/taxes' + ], + queryKey: (params: Params<'getTaxesFromBasket'>) => [ + ...getTaxesFromBasket.path(params), + getTaxesFromBasket.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts new file mode 100644 index 0000000000..db44c25be0 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023, 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 type {ShopperBaskets} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperBaskets<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getBasket: ['/organizations/', string, '/baskets/', string, Params<'getBasket'>] + getPaymentMethodsForBasket: [ + '/organizations/', + string, + '/baskets/', + string, + '/payment-methods', + Params<'getPaymentMethodsForBasket'> + ] + getPriceBooksForBasket: [ + '/organizations/', + string, + '/baskets/', + string, + '/price-books', + Params<'getPriceBooksForBasket'> + ] + getShippingMethodsForShipment: [ + '/organizations/', + string, + '/baskets/', + string, + '/shipments/', + string, + '/shipping-methods', + Params<'getShippingMethodsForShipment'> + ] + getTaxesFromBasket: [ + '/organizations/', + string, + '/baskets/', + string, + '/taxes', + Params<'getTaxesFromBasket'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getBasket: QueryKeyHelper<'getBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId', 'locale']), + path: (params) => ['/organizations/', params.organizationId, '/baskets/', params.basketId], + queryKey: (params: Params<'getBasket'>) => [ + ...getBasket.path(params), + getBasket.parameters(params) + ] +} + +export const getPaymentMethodsForBasket: QueryKeyHelper<'getPaymentMethodsForBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId', 'locale']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/payment-methods' + ], + queryKey: (params: Params<'getPaymentMethodsForBasket'>) => [ + ...getPaymentMethodsForBasket.path(params), + getPaymentMethodsForBasket.parameters(params) + ] +} + +export const getPriceBooksForBasket: QueryKeyHelper<'getPriceBooksForBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/price-books' + ], + queryKey: (params: Params<'getPriceBooksForBasket'>) => [ + ...getPriceBooksForBasket.path(params), + getPriceBooksForBasket.parameters(params) + ] +} + +export const getShippingMethodsForShipment: QueryKeyHelper<'getShippingMethodsForShipment'> = { + parameters: (params) => + pick(params, ['organizationId', 'basketId', 'shipmentId', 'siteId', 'locale']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/shipments/', + params.shipmentId, + '/shipping-methods' + ], + queryKey: (params: Params<'getShippingMethodsForShipment'>) => [ + ...getShippingMethodsForShipment.path(params), + getShippingMethodsForShipment.parameters(params) + ] +} + +export const getTaxesFromBasket: QueryKeyHelper<'getTaxesFromBasket'> = { + parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/baskets/', + params.basketId, + '/taxes' + ], + queryKey: (params: Params<'getTaxesFromBasket'>) => [ + ...getTaxesFromBasket.path(params), + getTaxesFromBasket.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts index b4d150d4cf..35bc3eab95 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperContexts'] @@ -26,22 +27,14 @@ export const useShopperContext = ( type Options = Argument type Data = DataType const {shopperContexts: client} = useCommerceApi() - const method = async (options: Options) => await client.getShopperContext(options) + const methodName = 'getShopperContext' const requiredParameters = ['organizationId', 'usid'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/shopper-context/', - parameters.usid, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts new file mode 100644 index 0000000000..b6e734731b --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, 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 type {ShopperContexts} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperContexts<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getShopperContext: [ + '/organizations/', + string, + '/shopper-context/', + string, + Params<'getShopperContext'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getShopperContext: QueryKeyHelper<'getShopperContext'> = { + parameters: (params) => pick(params, ['organizationId', 'usid']), + path: (params) => ['/organizations/', params.organizationId, '/shopper-context/', params.usid], + queryKey: (params: Params<'getShopperContext'>) => [ + ...getShopperContext.path(params), + getShopperContext.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts index a105eebc45..cdabc87ba8 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperCustomers'] @@ -26,26 +27,19 @@ export const useExternalProfile = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getExternalProfile(options) + const methodName = 'getExternalProfile' const requiredParameters = [ 'organizationId', 'externalId', 'authenticationProviderId', 'siteId' ] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/external-profile', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -69,22 +63,14 @@ export const useCustomer = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomer(options) + const methodName = 'getCustomer' const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -108,24 +94,14 @@ export const useCustomerAddress = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomerAddress(options) + const methodName = 'getCustomerAddress' const requiredParameters = ['organizationId', 'customerId', 'addressName', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/addresses/', - parameters.addressName, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -149,23 +125,14 @@ export const useCustomerBaskets = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomerBaskets(options) + const methodName = 'getCustomerBaskets' const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/baskets', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -189,32 +156,14 @@ export const useCustomerOrders = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomerOrders(options) + const methodName = 'getCustomerOrders' const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - const allParameters = [ - ...requiredParameters, - 'crossSites', - 'from', - 'until', - 'status', - 'offset', - 'limit' - ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/orders', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -238,29 +187,19 @@ export const useCustomerPaymentInstrument = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomerPaymentInstrument(options) + const methodName = 'getCustomerPaymentInstrument' const requiredParameters = [ 'organizationId', 'customerId', 'paymentInstrumentId', 'siteId' ] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/payment-instruments/', - parameters.paymentInstrumentId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -284,23 +223,14 @@ export const useCustomerProductLists = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomerProductLists(options) + const methodName = 'getCustomerProductLists' const requiredParameters = ['organizationId', 'customerId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/product-lists', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -324,24 +254,14 @@ export const useCustomerProductList = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomerProductList(options) + const methodName = 'getCustomerProductList' const requiredParameters = ['organizationId', 'customerId', 'listId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/product-lists/', - parameters.listId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -365,7 +285,7 @@ export const useCustomerProductListItem = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getCustomerProductListItem(options) + const methodName = 'getCustomerProductListItem' const requiredParameters = [ 'organizationId', 'customerId', @@ -373,24 +293,12 @@ export const useCustomerProductListItem = ( 'itemId', 'siteId' ] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/customers/', - parameters.customerId, - '/product-lists/', - parameters.listId, - '/items/', - parameters.itemId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -414,22 +322,14 @@ export const usePublicProductListsBySearchTerm = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => - await client.getPublicProductListsBySearchTerm(options) + const methodName = 'getPublicProductListsBySearchTerm' const requiredParameters = ['organizationId', 'siteId'] as const - const allParameters = [...requiredParameters, 'email', 'firstName', 'lastName'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/product-lists', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -453,22 +353,14 @@ export const usePublicProductList = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getPublicProductList(options) + const methodName = 'getPublicProductList' const requiredParameters = ['organizationId', 'listId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/product-lists/', - parameters.listId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -492,24 +384,14 @@ export const useProductListItem = ( type Options = Argument type Data = DataType const {shopperCustomers: client} = useCommerceApi() - const method = async (options: Options) => await client.getProductListItem(options) + const methodName = 'getProductListItem' const requiredParameters = ['organizationId', 'listId', 'itemId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/product-lists/', - parameters.listId, - '/items/', - parameters.itemId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts new file mode 100644 index 0000000000..703c6dc67c --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2023, 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 type {ShopperCustomers} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperCustomers<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getExternalProfile: [ + '/organizations/', + string, + '/customers/external-profile', + Params<'getExternalProfile'> + ] + getCustomer: ['/organizations/', string, '/customers/', string, Params<'getCustomer'>] + getCustomerAddress: [ + '/organizations/', + string, + '/customers/', + string, + '/addresses/', + string, + Params<'getCustomerAddress'> + ] + getCustomerBaskets: [ + '/organizations/', + string, + '/customers/', + string, + '/baskets', + Params<'getCustomerBaskets'> + ] + getCustomerOrders: [ + '/organizations/', + string, + '/customers/', + string, + '/orders', + Params<'getCustomerOrders'> + ] + getCustomerPaymentInstrument: [ + '/organizations/', + string, + '/customers/', + string, + '/payment-instruments/', + string, + Params<'getCustomerPaymentInstrument'> + ] + getCustomerProductLists: [ + '/organizations/', + string, + '/customers/', + string, + '/product-lists', + Params<'getCustomerProductLists'> + ] + getCustomerProductList: [ + '/organizations/', + string, + '/customers/', + string, + '/product-lists/', + string, + Params<'getCustomerProductList'> + ] + getCustomerProductListItem: [ + '/organizations/', + string, + '/customers/', + string, + '/product-lists/', + string, + '/items/', + string, + Params<'getCustomerProductListItem'> + ] + getPublicProductListsBySearchTerm: [ + '/organizations/', + string, + '/product-lists', + Params<'getPublicProductListsBySearchTerm'> + ] + getPublicProductList: [ + '/organizations/', + string, + '/product-lists/', + string, + Params<'getPublicProductList'> + ] + getProductListItem: [ + '/organizations/', + string, + '/product-lists/', + string, + '/items/', + string, + Params<'getProductListItem'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getExternalProfile: QueryKeyHelper<'getExternalProfile'> = { + parameters: (params) => + pick(params, ['organizationId', 'externalId', 'authenticationProviderId', 'siteId']), + path: (params) => ['/organizations/', params.organizationId, '/customers/external-profile'], + queryKey: (params: Params<'getExternalProfile'>) => [ + ...getExternalProfile.path(params), + getExternalProfile.parameters(params) + ] +} + +export const getCustomer: QueryKeyHelper<'getCustomer'> = { + parameters: (params) => pick(params, ['organizationId', 'customerId', 'siteId']), + path: (params) => ['/organizations/', params.organizationId, '/customers/', params.customerId], + queryKey: (params: Params<'getCustomer'>) => [ + ...getCustomer.path(params), + getCustomer.parameters(params) + ] +} + +export const getCustomerAddress: QueryKeyHelper<'getCustomerAddress'> = { + parameters: (params) => pick(params, ['organizationId', 'customerId', 'addressName', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/customers/', + params.customerId, + '/addresses/', + params.addressName + ], + queryKey: (params: Params<'getCustomerAddress'>) => [ + ...getCustomerAddress.path(params), + getCustomerAddress.parameters(params) + ] +} + +export const getCustomerBaskets: QueryKeyHelper<'getCustomerBaskets'> = { + parameters: (params) => pick(params, ['organizationId', 'customerId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/customers/', + params.customerId, + '/baskets' + ], + queryKey: (params: Params<'getCustomerBaskets'>) => [ + ...getCustomerBaskets.path(params), + getCustomerBaskets.parameters(params) + ] +} + +export const getCustomerOrders: QueryKeyHelper<'getCustomerOrders'> = { + parameters: (params) => + pick(params, [ + 'organizationId', + 'customerId', + 'crossSites', + 'from', + 'until', + 'status', + 'siteId', + 'offset', + 'limit' + ]), + path: (params) => [ + '/organizations/', + params.organizationId, + '/customers/', + params.customerId, + '/orders' + ], + queryKey: (params: Params<'getCustomerOrders'>) => [ + ...getCustomerOrders.path(params), + getCustomerOrders.parameters(params) + ] +} + +export const getCustomerPaymentInstrument: QueryKeyHelper<'getCustomerPaymentInstrument'> = { + parameters: (params) => + pick(params, ['organizationId', 'customerId', 'paymentInstrumentId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/customers/', + params.customerId, + '/payment-instruments/', + params.paymentInstrumentId + ], + queryKey: (params: Params<'getCustomerPaymentInstrument'>) => [ + ...getCustomerPaymentInstrument.path(params), + getCustomerPaymentInstrument.parameters(params) + ] +} + +export const getCustomerProductLists: QueryKeyHelper<'getCustomerProductLists'> = { + parameters: (params) => pick(params, ['organizationId', 'customerId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/customers/', + params.customerId, + '/product-lists' + ], + queryKey: (params: Params<'getCustomerProductLists'>) => [ + ...getCustomerProductLists.path(params), + getCustomerProductLists.parameters(params) + ] +} + +export const getCustomerProductList: QueryKeyHelper<'getCustomerProductList'> = { + parameters: (params) => pick(params, ['organizationId', 'customerId', 'listId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/customers/', + params.customerId, + '/product-lists/', + params.listId + ], + queryKey: (params: Params<'getCustomerProductList'>) => [ + ...getCustomerProductList.path(params), + getCustomerProductList.parameters(params) + ] +} + +export const getCustomerProductListItem: QueryKeyHelper<'getCustomerProductListItem'> = { + parameters: (params) => + pick(params, ['organizationId', 'customerId', 'listId', 'itemId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/customers/', + params.customerId, + '/product-lists/', + params.listId, + '/items/', + params.itemId + ], + queryKey: (params: Params<'getCustomerProductListItem'>) => [ + ...getCustomerProductListItem.path(params), + getCustomerProductListItem.parameters(params) + ] +} + +export const getPublicProductListsBySearchTerm: QueryKeyHelper<'getPublicProductListsBySearchTerm'> = + { + parameters: (params) => + pick(params, ['organizationId', 'email', 'firstName', 'lastName', 'siteId']), + path: (params) => ['/organizations/', params.organizationId, '/product-lists'], + queryKey: (params: Params<'getPublicProductListsBySearchTerm'>) => [ + ...getPublicProductListsBySearchTerm.path(params), + getPublicProductListsBySearchTerm.parameters(params) + ] + } + +export const getPublicProductList: QueryKeyHelper<'getPublicProductList'> = { + parameters: (params) => pick(params, ['organizationId', 'listId', 'siteId']), + path: (params) => ['/organizations/', params.organizationId, '/product-lists/', params.listId], + queryKey: (params: Params<'getPublicProductList'>) => [ + ...getPublicProductList.path(params), + getPublicProductList.parameters(params) + ] +} + +export const getProductListItem: QueryKeyHelper<'getProductListItem'> = { + parameters: (params) => pick(params, ['organizationId', 'listId', 'itemId', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/product-lists/', + params.listId, + '/items/', + params.itemId + ], + queryKey: (params: Params<'getProductListItem'>) => [ + ...getProductListItem.path(params), + getProductListItem.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts index a719937119..a46fce4596 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperExperience'] @@ -30,25 +31,14 @@ export const usePages = ( type Options = Argument type Data = DataType const {shopperExperience: client} = useCommerceApi() - const method = async (options: Options) => await client.getPages(options) + const methodName = 'getPages' const requiredParameters = ['organizationId', 'aspectTypeId', 'siteId'] as const - const allParameters = [ - ...requiredParameters, - 'categoryId', - 'productId', - 'aspectAttributes', - 'parameters', - - 'locale' - ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = ['/organizations/', parameters.organizationId, '/pages', parameters] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -74,28 +64,14 @@ export const usePage = ( type Options = Argument type Data = DataType const {shopperExperience: client} = useCommerceApi() - const method = async (options: Options) => await client.getPage(options) + const methodName = 'getPage' const requiredParameters = ['organizationId', 'pageId', 'siteId'] as const - const allParameters = [ - ...requiredParameters, - 'aspectAttributes', - 'parameters', - 'locale' - ] as const // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/pages/', - parameters.pageId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts new file mode 100644 index 0000000000..d1392a8662 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023, 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 type {ShopperExperience} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperExperience<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getPages: ['/organizations/', string, '/pages', Params<'getPages'>] + getPage: ['/organizations/', string, '/pages/', string, Params<'getPage'>] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getPages: QueryKeyHelper<'getPages'> = { + parameters: (params) => + pick(params, [ + 'organizationId', + 'categoryId', + 'productId', + 'aspectTypeId', + 'aspectAttributes', + 'parameters', + 'siteId', + 'locale' + ]), + path: (params) => ['/organizations/', params.organizationId, '/pages'], + queryKey: (params: Params<'getPages'>) => [ + ...getPages.path(params), + getPages.parameters(params) + ] +} + +export const getPage: QueryKeyHelper<'getPage'> = { + parameters: (params) => + pick(params, [ + 'organizationId', + 'pageId', + 'aspectAttributes', + 'parameters', + 'siteId', + 'locale' + ]), + path: (params) => ['/organizations/', params.organizationId, '/pages/', params.pageId], + queryKey: (params: Params<'getPage'>) => [...getPage.path(params), getPage.parameters(params)] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts index 463c003a1f..3f630af32e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperGiftCertificates'] @@ -26,21 +27,14 @@ export const useGiftCertificate = ( type Options = Argument type Data = DataType const {shopperGiftCertificates: client} = useCommerceApi() - const method = async (options: Options) => await client.getGiftCertificate(options) + const methodName = 'getGiftCertificate' const requiredParameters = ['organizationId', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/gift-certificate', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts new file mode 100644 index 0000000000..06e684ad5d --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, 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 type {ShopperGiftCertificates} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperGiftCertificates<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getGiftCertificate: [ + '/organizations/', + string, + '/gift-certificate', + Params<'getGiftCertificate'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getGiftCertificate: QueryKeyHelper<'getGiftCertificate'> = { + parameters: (params) => pick(params, ['organizationId', 'siteId']), + path: (params) => ['/organizations/', params.organizationId, '/gift-certificate'], + queryKey: (params: Params<'getGiftCertificate'>) => [ + ...getGiftCertificate.path(params), + getGiftCertificate.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts index ee80851234..ef3c828744 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperLogin'] @@ -26,21 +27,14 @@ export const useCredQualityUserInfo = ( type Options = Argument type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Options) => await client.retrieveCredQualityUserInfo(options) + const methodName = 'retrieveCredQualityUserInfo' const requiredParameters = ['organizationId', 'username'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/cred-qual/user', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -64,21 +58,14 @@ export const useUserInfo = ( type Options = Argument type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Options) => await client.getUserInfo(options) + const methodName = 'getUserInfo' const requiredParameters = ['organizationId'] as const - const allParameters = [...requiredParameters, 'channel_id'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/oauth2/userinfo', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -102,21 +89,14 @@ export const useWellknownOpenidConfiguration = ( type Options = Argument type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Options) => await client.getWellknownOpenidConfiguration(options) + const methodName = 'getWellknownOpenidConfiguration' const requiredParameters = ['organizationId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/oauth2/.well-known/openid-configuration', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -140,21 +120,14 @@ export const useJwksUri = ( type Options = Argument type Data = DataType const {shopperLogin: client} = useCommerceApi() - const method = async (options: Options) => await client.getJwksUri(options) + const methodName = 'getJwksUri' const requiredParameters = ['organizationId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/oauth2/jwks', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts new file mode 100644 index 0000000000..8d5cdc88ba --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023, 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 type {ShopperLogin} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperLogin<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + retrieveCredQualityUserInfo: [ + '/organizations/', + string, + '/cred-qual/user', + Params<'retrieveCredQualityUserInfo'> + ] + getUserInfo: ['/organizations/', string, '/oauth2/userinfo', Params<'getUserInfo'>] + getWellknownOpenidConfiguration: [ + '/organizations/', + string, + '/oauth2/.well-known/openid-configuration', + Params<'getWellknownOpenidConfiguration'> + ] + getJwksUri: ['/organizations/', string, '/oauth2/jwks', Params<'getJwksUri'>] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const retrieveCredQualityUserInfo: QueryKeyHelper<'retrieveCredQualityUserInfo'> = { + parameters: (params) => pick(params, ['organizationId', 'username']), + path: (params) => ['/organizations/', params.organizationId, '/cred-qual/user'], + queryKey: (params: Params<'retrieveCredQualityUserInfo'>) => [ + ...retrieveCredQualityUserInfo.path(params), + retrieveCredQualityUserInfo.parameters(params) + ] +} + +export const getUserInfo: QueryKeyHelper<'getUserInfo'> = { + parameters: (params) => pick(params, ['organizationId', 'channel_id']), + path: (params) => ['/organizations/', params.organizationId, '/oauth2/userinfo'], + queryKey: (params: Params<'getUserInfo'>) => [ + ...getUserInfo.path(params), + getUserInfo.parameters(params) + ] +} + +export const getWellknownOpenidConfiguration: QueryKeyHelper<'getWellknownOpenidConfiguration'> = { + parameters: (params) => pick(params, ['organizationId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/oauth2/.well-known/openid-configuration' + ], + queryKey: (params: Params<'getWellknownOpenidConfiguration'>) => [ + ...getWellknownOpenidConfiguration.path(params), + getWellknownOpenidConfiguration.parameters(params) + ] +} + +export const getJwksUri: QueryKeyHelper<'getJwksUri'> = { + parameters: (params) => pick(params, ['organizationId']), + path: (params) => ['/organizations/', params.organizationId, '/oauth2/jwks'], + queryKey: (params: Params<'getJwksUri'>) => [ + ...getJwksUri.path(params), + getJwksUri.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts index 60f5a5881e..34f75313a2 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperOrders'] @@ -26,22 +27,14 @@ export const useOrder = ( type Options = Argument type Data = DataType const {shopperOrders: client} = useCommerceApi() - const method = async (options: Options) => await client.getOrder(options) + const methodName = 'getOrder' const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const - const allParameters = [...requiredParameters, 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/orders/', - parameters.orderNo, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -65,23 +58,14 @@ export const usePaymentMethodsForOrder = ( type Options = Argument type Data = DataType const {shopperOrders: client} = useCommerceApi() - const method = async (options: Options) => await client.getPaymentMethodsForOrder(options) + const methodName = 'getPaymentMethodsForOrder' const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const - const allParameters = [...requiredParameters, 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/orders/', - parameters.orderNo, - '/payment-methods', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -107,23 +91,14 @@ export const useTaxesFromOrder = ( type Options = Argument type Data = DataType const {shopperOrders: client} = useCommerceApi() - const method = async (options: Options) => await client.getTaxesFromOrder(options) + const methodName = 'getTaxesFromOrder' const requiredParameters = ['organizationId', 'orderNo', 'siteId'] as const - const allParameters = [...requiredParameters] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/orders/', - parameters.orderNo, - '/taxes', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts new file mode 100644 index 0000000000..dc1482ce11 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023, 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 type {ShopperOrders} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperOrders<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getOrder: ['/organizations/', string, '/orders/', string, Params<'getOrder'>] + getPaymentMethodsForOrder: [ + '/organizations/', + string, + '/orders/', + string, + '/payment-methods', + Params<'getPaymentMethodsForOrder'> + ] + getTaxesFromOrder: [ + '/organizations/', + string, + '/orders/', + string, + '/taxes', + Params<'getTaxesFromOrder'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getOrder: QueryKeyHelper<'getOrder'> = { + parameters: (params) => pick(params, ['organizationId', 'orderNo', 'siteId', 'locale']), + path: (params) => ['/organizations/', params.organizationId, '/orders/', params.orderNo], + queryKey: (params: Params<'getOrder'>) => [ + ...getOrder.path(params), + getOrder.parameters(params) + ] +} + +export const getPaymentMethodsForOrder: QueryKeyHelper<'getPaymentMethodsForOrder'> = { + parameters: (params) => pick(params, ['organizationId', 'orderNo', 'siteId', 'locale']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/orders/', + params.orderNo, + '/payment-methods' + ], + queryKey: (params: Params<'getPaymentMethodsForOrder'>) => [ + ...getPaymentMethodsForOrder.path(params), + getPaymentMethodsForOrder.parameters(params) + ] +} + +export const getTaxesFromOrder: QueryKeyHelper<'getTaxesFromOrder'> = { + parameters: (params) => pick(params, ['organizationId', 'orderNo', 'siteId']), + path: (params) => [ + '/organizations/', + params.organizationId, + '/orders/', + params.orderNo, + '/taxes' + ], + queryKey: (params: Params<'getTaxesFromOrder'>) => [ + ...getTaxesFromOrder.path(params), + getTaxesFromOrder.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts index 0258e5749d..fedbaa302b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperProducts'] @@ -26,29 +27,14 @@ export const useProducts = ( type Options = Argument type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Options) => await client.getProducts(options) + const methodName = 'getProducts' const requiredParameters = ['organizationId', 'ids', 'siteId'] as const - const allParameters = [ - ...requiredParameters, - 'inventoryIds', - 'currency', - 'expand', - 'locale', - 'allImages', - 'perPricebook' - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/products', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -72,30 +58,14 @@ export const useProduct = ( type Options = Argument type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Options) => await client.getProduct(options) + const methodName = 'getProduct' const requiredParameters = ['organizationId', 'id', 'siteId'] as const - const allParameters = [ - ...requiredParameters, - 'inventoryIds', - 'currency', - 'expand', - 'locale', - 'allImages', - 'perPricebook' - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/products/', - parameters.id, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -119,21 +89,14 @@ export const useCategories = ( type Options = Argument type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Options) => await client.getCategories(options) + const methodName = 'getCategories' const requiredParameters = ['organizationId', 'ids', 'siteId'] as const - const allParameters = [...requiredParameters, 'levels', 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/categories', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -159,22 +122,14 @@ export const useCategory = ( type Options = Argument type Data = DataType const {shopperProducts: client} = useCommerceApi() - const method = async (options: Options) => await client.getCategory(options) + const methodName = 'getCategory' const requiredParameters = ['organizationId', 'id', 'siteId'] as const - const allParameters = [...requiredParameters, 'levels', 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/categories/', - parameters.id, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts new file mode 100644 index 0000000000..53d6d1c434 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023, 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 type {ShopperProducts} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperProducts<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getProducts: ['/organizations/', string, '/products', Params<'getProducts'>] + getProduct: ['/organizations/', string, '/products/', string, Params<'getProduct'>] + getCategories: ['/organizations/', string, '/categories', Params<'getCategories'>] + getCategory: ['/organizations/', string, '/categories/', string, Params<'getCategory'>] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getProducts: QueryKeyHelper<'getProducts'> = { + parameters: (params) => + pick(params, [ + 'organizationId', + 'ids', + 'inventoryIds', + 'currency', + 'expand', + 'locale', + 'allImages', + 'perPricebook', + 'siteId' + ]), + path: (params) => ['/organizations/', params.organizationId, '/products'], + queryKey: (params: Params<'getProducts'>) => [ + ...getProducts.path(params), + getProducts.parameters(params) + ] +} + +export const getProduct: QueryKeyHelper<'getProduct'> = { + parameters: (params) => + pick(params, [ + 'organizationId', + 'id', + 'inventoryIds', + 'currency', + 'expand', + 'locale', + 'allImages', + 'perPricebook', + 'siteId' + ]), + path: (params) => ['/organizations/', params.organizationId, '/products/', params.id], + queryKey: (params: Params<'getProduct'>) => [ + ...getProduct.path(params), + getProduct.parameters(params) + ] +} + +export const getCategories: QueryKeyHelper<'getCategories'> = { + parameters: (params) => pick(params, ['organizationId', 'ids', 'levels', 'locale', 'siteId']), + path: (params) => ['/organizations/', params.organizationId, '/categories'], + queryKey: (params: Params<'getCategories'>) => [ + ...getCategories.path(params), + getCategories.parameters(params) + ] +} + +export const getCategory: QueryKeyHelper<'getCategory'> = { + parameters: (params) => pick(params, ['organizationId', 'id', 'levels', 'locale', 'siteId']), + path: (params) => ['/organizations/', params.organizationId, '/categories/', params.id], + queryKey: (params: Params<'getCategory'>) => [ + ...getCategory.path(params), + getCategory.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts index 8b04ded797..e3a6886400 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperPromotions'] @@ -26,21 +27,14 @@ export const usePromotions = ( type Options = Argument type Data = DataType const {shopperPromotions: client} = useCommerceApi() - const method = async (options: Options) => await client.getPromotions(options) + const methodName = 'getPromotions' const requiredParameters = ['organizationId', 'siteId', 'ids'] as const - const allParameters = [...requiredParameters, 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/promotions', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -68,22 +62,14 @@ export const usePromotionsForCampaign = ( type Options = Argument type Data = DataType const {shopperPromotions: client} = useCommerceApi() - const method = async (options: Options) => await client.getPromotionsForCampaign(options) + const methodName = 'getPromotionsForCampaign' const requiredParameters = ['organizationId', 'campaignId', 'siteId'] as const - const allParameters = [...requiredParameters, 'startDate', 'endDate', 'currency'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/promotions/campaigns/', - parameters.campaignId, - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts new file mode 100644 index 0000000000..05a5b2d650 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023, 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 type {ShopperPromotions} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperPromotions<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + getPromotions: ['/organizations/', string, '/promotions', Params<'getPromotions'>] + getPromotionsForCampaign: [ + '/organizations/', + string, + '/promotions/campaigns/', + string, + Params<'getPromotionsForCampaign'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getPromotions: QueryKeyHelper<'getPromotions'> = { + parameters: (params) => pick(params, ['organizationId', 'siteId', 'ids', 'locale']), + path: (params) => ['/organizations/', params.organizationId, '/promotions'], + queryKey: (params: Params<'getPromotions'>) => [ + ...getPromotions.path(params), + getPromotions.parameters(params) + ] +} + +export const getPromotionsForCampaign: QueryKeyHelper<'getPromotionsForCampaign'> = { + parameters: (params) => + pick(params, [ + 'organizationId', + 'campaignId', + 'siteId', + 'startDate', + 'endDate', + 'currency' + ]), + path: (params) => [ + '/organizations/', + params.organizationId, + '/promotions/campaigns/', + params.campaignId + ], + queryKey: (params: Params<'getPromotionsForCampaign'>) => [ + ...getPromotionsForCampaign.path(params), + getPromotionsForCampaign.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts index 8d3e88f880..3420f3737c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts @@ -8,7 +8,8 @@ import {UseQueryResult} from '@tanstack/react-query' import {ApiClients, ApiQueryOptions, Argument, DataType} from '../types' import useCommerceApi from '../useCommerceApi' import {useQuery} from '../useQuery' -import {mergeOptions, pick} from '../utils' +import {mergeOptions} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperSearch'] @@ -27,31 +28,14 @@ export const useProductSearch = ( type Options = Argument type Data = DataType const {shopperSearch: client} = useCommerceApi() - const method = async (options: Options) => await client.productSearch(options) + const methodName = 'productSearch' const requiredParameters = ['organizationId', 'siteId'] as const - const allParameters = [ - ...requiredParameters, - 'q', - 'refine', - 'sort', - 'currency', - 'locale', - 'expand', - 'offset', - 'limit' - ] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/product-search', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. @@ -75,21 +59,14 @@ export const useSearchSuggestions = ( type Options = Argument type Data = DataType const {shopperSearch: client} = useCommerceApi() - const method = async (options: Options) => await client.getSearchSuggestions(options) + const methodName = 'getSearchSuggestions' const requiredParameters = ['organizationId', 'siteId', 'q'] as const - const allParameters = [...requiredParameters, 'limit', 'currency', 'locale'] as const + // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order // to generate the correct query key. const netOptions = mergeOptions(client, apiOptions) - // `client.clientConfig` can have parameters that are not relevant to this endpoint, so we must - // exclude them when generating the query key. - const parameters = pick(netOptions.parameters, allParameters) - const queryKey = [ - '/organizations/', - parameters.organizationId, - '/search-suggestions', - parameters - ] as const + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + const method = async (options: Options) => await client[methodName](options) // For some reason, if we don't explicitly set these generic parameters, the inferred type for // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts new file mode 100644 index 0000000000..1fff07af1a --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023, 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 type {ShopperSearch} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pick} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperSearch<{shortCode: string}> +type Params = NonNullable['parameters']> +type QueryKeys = { + productSearch: ['/organizations/', string, '/product-search', Params<'productSearch'>] + getSearchSuggestions: [ + '/organizations/', + string, + '/search-suggestions', + Params<'getSearchSuggestions'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** + * Reduces the given parameters (which may have additional, unknown properties) to an object + * containing *only* the properties required for an endpoint. + */ + parameters: (params: Params) => Params + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const productSearch: QueryKeyHelper<'productSearch'> = { + parameters: (params) => + pick(params, [ + 'organizationId', + 'siteId', + 'q', + 'refine', + 'sort', + 'currency', + 'locale', + 'expand', + 'offset', + 'limit' + ]), + path: (params) => ['/organizations/', params.organizationId, '/product-search'], + queryKey: (params: Params<'productSearch'>) => [ + ...productSearch.path(params), + productSearch.parameters(params) + ] +} + +export const getSearchSuggestions: QueryKeyHelper<'getSearchSuggestions'> = { + parameters: (params) => + pick(params, ['organizationId', 'siteId', 'q', 'limit', 'currency', 'locale']), + path: (params) => ['/organizations/', params.organizationId, '/search-suggestions'], + queryKey: (params: Params<'getSearchSuggestions'>) => [ + ...getSearchSuggestions.path(params), + getSearchSuggestions.parameters(params) + ] +} diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index e0f10765e5..addc260931 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -39,9 +39,15 @@ type StringIndexNever = { [K in keyof T]: string extends K ? never : T[K] } -/** Removes a string index type */ +/** Removes a string index type. */ export type RemoveStringIndex = RemoveNeverValues> +/** Remove the last entry from a readonly tuple type. */ +export type ExcludeTail = T extends readonly [...infer Head, unknown] + ? Readonly + : // If it's a plain array, rather than a tuple, then removing the last element has no effect + T + // --- API CLIENTS --- // export type ApiParameter = string | number | boolean | string[] | number[] From ca4bc5cbdaa42b3c0f3598c1d7964e595755b2a7 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 24 Feb 2023 12:49:24 -0500 Subject: [PATCH 100/122] Remove no longer needed query test file. All query hooks are now ttested in their respective folders. --- .../src/hooks/query.test.tsx | 259 ------------------ 1 file changed, 259 deletions(-) delete mode 100644 packages/commerce-sdk-react/src/hooks/query.test.tsx diff --git a/packages/commerce-sdk-react/src/hooks/query.test.tsx b/packages/commerce-sdk-react/src/hooks/query.test.tsx deleted file mode 100644 index 92996a7cae..0000000000 --- a/packages/commerce-sdk-react/src/hooks/query.test.tsx +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (c) 2023, 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 - */ -import nock from 'nock' -import {renderHookWithProviders, DEFAULT_TEST_HOST} from '../test-utils' -import { - useBasket, - usePaymentMethodsForBasket, - usePriceBooksForBasket, - useShippingMethodsForShipment, - useTaxesFromBasket -} from './ShopperBaskets/query' -import {useProductSearch, useSearchSuggestions} from './ShopperSearch/query' -import {useShopperContext} from './ShopperContexts/query' -import { - useExternalProfile, - useCustomer, - useCustomerAddress, - useCustomerBaskets, - useCustomerOrders, - useCustomerPaymentInstrument, - useCustomerProductLists, - useCustomerProductList, - useCustomerProductListItem, - usePublicProductListsBySearchTerm, - usePublicProductList, - useProductListItem -} from './ShopperCustomers/query' -import {useOrder, usePaymentMethodsForOrder, useTaxesFromOrder} from './ShopperOrders/query' -import {useProducts, useProduct, useCategories, useCategory} from './ShopperProducts/query' -import {usePromotions, usePromotionsForCampaign} from './ShopperPromotions/query' - -jest.mock('../auth/index.ts', () => { - return jest.fn().mockImplementation(() => ({ - ready: jest.fn().mockResolvedValue({access_token: '123'}) - })) -}) - -const QUERY_TESTS = [ - // ShopperBasket - { - name: 'useBasket', - hook: () => useBasket({parameters: {basketId: 'test'}}), - endpoint: /\/baskets\/test$/ - }, - { - name: 'usePaymentMethodsForBasket', - hook: () => usePaymentMethodsForBasket({parameters: {basketId: 'test'}}), - endpoint: /\/baskets\/test\/payment-methods$/ - }, - { - name: 'usePriceBooksForBasket', - hook: () => usePriceBooksForBasket({parameters: {basketId: 'test'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'useTaxesFromBasket', - hook: () => useTaxesFromBasket({parameters: {basketId: 'test'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'useShippingMethodsForShipment', - hook: () => - useShippingMethodsForShipment({parameters: {basketId: 'test', shipmentId: '123'}}), - endpoint: /\/baskets\/test\/shipments\/123\/shipping-methods$/ - }, - // ShopperContext - { - name: 'useShopperContext', - hook: () => useShopperContext({parameters: {usid: 'test'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - // ShopperCustomers - { - name: 'useExternalProfile', - hook: () => - useExternalProfile({ - parameters: {externalId: 'externalId', authenticationProviderId: 'authProvider'} - }), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'useCustomer', - hook: () => useCustomer({parameters: {customerId: '123'}}), - endpoint: /\/customers\/123$/ - }, - { - name: 'useCustomerAddress', - hook: () => useCustomerAddress({parameters: {customerId: '123', addressName: '456'}}), - endpoint: /\/customers\/123\/addresses\/456$/ - }, - { - name: 'useCustomerBaskets', - hook: () => useCustomerBaskets({parameters: {customerId: '123'}}), - endpoint: /\/customers\/123\/baskets$/ - }, - { - name: 'useCustomerOrders', - hook: () => useCustomerOrders({parameters: {customerId: '123'}}), - endpoint: /\/customers\/123\/orders$/ - }, - { - name: 'useCustomerPaymentInstrument', - hook: () => - useCustomerPaymentInstrument({ - parameters: {customerId: '123', paymentInstrumentId: '456'} - }), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'useCustomerProductLists', - hook: () => useCustomerProductLists({parameters: {customerId: '123'}}), - endpoint: /\/customers\/123\/product-lists$/ - }, - { - name: 'useCustomerProductList', - hook: () => useCustomerProductList({parameters: {customerId: '123', listId: '456'}}), - endpoint: /\/customers\/123\/product-lists\/456$/ - }, - { - name: 'useCustomerProductListItem', - hook: () => - useCustomerProductListItem({ - parameters: {customerId: '123', listId: '456', itemId: '789'} - }), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'usePublicProductListsBySearchTerm', - hook: () => usePublicProductListsBySearchTerm({parameters: {}}), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'usePublicProductList', - hook: () => usePublicProductList({parameters: {listId: '123'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'useProductListItem', - hook: () => useProductListItem({parameters: {listId: '123', itemId: '456'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - // ShopperOrders - { - name: 'useOrder', - hook: () => useOrder({parameters: {orderNo: '123'}}), - endpoint: /\/orders\/123$/ - }, - { - name: 'usePaymentMethodsForOrder', - hook: () => usePaymentMethodsForOrder({parameters: {orderNo: '123'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - { - name: 'useTaxesFromOrder', - hook: () => useTaxesFromOrder({parameters: {orderNo: '123'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - // ShopperProducts - { - name: 'useProducts', - hook: () => useProducts({parameters: {ids: '123,456'}}), - endpoint: /\/products$/ - }, - { - name: 'useProduct', - hook: () => useProduct({parameters: {id: '123'}}), - endpoint: /\/products\/123$/ - }, - { - name: 'useCategories', - hook: () => useCategories({parameters: {ids: '123,456'}}), - endpoint: /\/categories$/ - }, - { - name: 'useCategory', - hook: () => useCategory({parameters: {id: '123'}}), - endpoint: /\/categories\/123$/ - }, - // ShopperPromotions - { - name: 'usePromotions', - hook: () => usePromotions({parameters: {ids: '123,456'}}), - endpoint: /\/promotions$/ - }, - { - name: 'usePromotionsForCampaign', - hook: () => usePromotionsForCampaign({parameters: {campaignId: '123'}}), - endpoint: new RegExp(''), - notImplemented: true - }, - // ShopperSearch - { - name: 'useProductSearch', - hook: () => useProductSearch({parameters: {q: 'test'}}), - endpoint: /\/product-search$/ - }, - { - name: 'useSearchSuggestions', - hook: () => useSearchSuggestions({parameters: {q: 'test'}}), - endpoint: /\/search-suggestions$/ - } -] - -test.each(QUERY_TESTS)('%j - 200 returns data', async ({hook, endpoint, notImplemented}) => { - if (notImplemented) { - return - } - - const data = {test: true} - nock(DEFAULT_TEST_HOST) - .get((uri) => endpoint.test(uri.split('?')[0])) - .reply(200, data) - const {result, waitForNextUpdate} = renderHookWithProviders(hook) - expect(result.current.data).toBe(undefined) - expect(result.current.isLoading).toBe(true) - - await waitForNextUpdate() - - expect(result.current.isLoading).toBe(false) - expect(result.current.data).toEqual(data) -}) - -test.each(QUERY_TESTS)('%j - 400 returns error', async ({hook, endpoint, notImplemented}) => { - if (notImplemented) { - return - } - nock(DEFAULT_TEST_HOST) - .get((uri) => endpoint.test(uri.split('?')[0])) - .reply(400) - const {result, waitForNextUpdate} = renderHookWithProviders(hook) - expect(result.current.data).toBe(undefined) - expect(result.current.isLoading).toBe(true) - - await waitForNextUpdate() - - expect(result.current.isLoading).toBe(false) - expect(result.current.error).toBeTruthy() -}) - -test.each(QUERY_TESTS)('%j - throws error when not implemented', async ({hook, notImplemented}) => { - if (notImplemented) { - expect(() => hook()).toThrowError('This method is not implemented.') - } -}) From ea2b699c3757a0ed4bad22995dc05bd93788052a Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 24 Feb 2023 14:51:23 -0500 Subject: [PATCH 101/122] Update hook usage. --- .../use-shopper-baskets/use-basket/index.tsx | 2 +- .../use-payment-method-for-basket/index.tsx | 2 +- .../test-commerce-sdk-react/app/pages/query-errors.tsx | 5 +++-- .../app/pages/use-shopper-customer.tsx | 10 +++++++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-basket/index.tsx b/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-basket/index.tsx index 08466b719e..a81c95edbf 100644 --- a/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-basket/index.tsx +++ b/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-basket/index.tsx @@ -9,7 +9,7 @@ import Json from '../../Json' import {useBasket} from 'commerce-sdk-react-preview' export const UseBasket = ({basketId}: {basketId: string}): ReactElement | null => { - const {isLoading, error, data} = useBasket({basketId}, {enabled: !!basketId}) + const {isLoading, error, data} = useBasket({parameters: {basketId}}) if (!basketId) { return null diff --git a/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-payment-method-for-basket/index.tsx b/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-payment-method-for-basket/index.tsx index abc0be80f9..4d47528832 100644 --- a/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-payment-method-for-basket/index.tsx +++ b/packages/test-commerce-sdk-react/app/components/use-shopper-baskets/use-payment-method-for-basket/index.tsx @@ -9,7 +9,7 @@ import Json from '../../Json' import {usePaymentMethodsForBasket} from 'commerce-sdk-react-preview' export const UsePaymentMethodsForBasket = ({basketId}: {basketId: string}): ReactElement | null => { - const {isLoading, error, data} = usePaymentMethodsForBasket({basketId}, {enabled: !!basketId}) + const {isLoading, error, data} = usePaymentMethodsForBasket({parameters: {basketId}}) if (!basketId) { return null diff --git a/packages/test-commerce-sdk-react/app/pages/query-errors.tsx b/packages/test-commerce-sdk-react/app/pages/query-errors.tsx index 730347a834..37291ab2e0 100644 --- a/packages/test-commerce-sdk-react/app/pages/query-errors.tsx +++ b/packages/test-commerce-sdk-react/app/pages/query-errors.tsx @@ -10,8 +10,9 @@ import {useProducts, useProduct} from 'commerce-sdk-react-preview' import Json from '../components/Json' const QueryErrors = () => { - // @ts-ignore - const products = useProducts({parameters: {FOO: ''}}) + // TODO: `products` no longer has an error, because the query is not enabled unless all required + // parameters are passed + const products = useProducts({parameters: {FOO: ''} as Record}) const product = useProduct({parameters: {id: '25502228Mxxx'}}) return ( diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx index 24562c5c28..0f6fc6985e 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-customer.tsx @@ -223,6 +223,12 @@ function UseCustomer() { } ] + const loginError = loginRegisteredUser.error + const loginErrorMessage = + typeof loginError === 'object' && loginError !== null && 'message' in loginError + ? loginError.message + : loginError + return ( <>

ShopperCustomer page

@@ -241,9 +247,7 @@ function UseCustomer() { > loginRegisteredUser - {loginRegisteredUser.error?.message && ( -

Error: {loginRegisteredUser.error?.message}

- )} + {loginError &&

Error: {loginErrorMessage}

} ) : ( <> From e7073cdb47ec5b9396e8aeabaf6ca67859092c7b Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 24 Feb 2023 15:30:04 -0500 Subject: [PATCH 102/122] Implement basic Shopper Customers mutation tests. --- .../hooks/ShopperCustomers/mutation.test.ts | 181 +++++++++++++ .../hooks/ShopperCustomers/mutation.test.tsx | 255 ------------------ 2 files changed, 181 insertions(+), 255 deletions(-) create mode 100644 packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts new file mode 100644 index 0000000000..e39028e3ef --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2023, 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 {act} from '@testing-library/react' +import {ShopperCustomersTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + mockMutationEndpoints, + renderHookWithProviders, + waitAndExpectError, + waitAndExpectSuccess +} from '../../test-utils' +import {ShopperCustomersMutation, useShopperCustomersMutation} from '../ShopperCustomers' +import {ApiClients, Argument, DataType} from '../types' +import {NotImplementedError} from '../utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + return jest.fn().mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue({access_token: 'access_token'}) + })) +}) + +type Client = ApiClients['shopperCustomers'] +const customersEndpoint = '/customer/shopper-customers/' +const ADDRESS: ShopperCustomersTypes.CustomerAddress = { + addressId: 'address', + countryCode: 'CA', + lastName: 'Doofenschmirtz' +} +const PRODUCT_LIST_ITEM: ShopperCustomersTypes.CustomerProductListItem = { + id: 'productListItemId', + priority: 0, + public: false, + quantity: 0 +} +const PAYMENT_INSTRUMENT: ShopperCustomersTypes.CustomerPaymentInstrument = { + paymentBankAccount: {}, + paymentCard: {cardType: 'fake'}, + paymentInstrumentId: 'paymentInstrumentId', + paymentMethodId: 'paymentMethodId' +} +/** All Shopper Customers parameters. Can be used for all endpoints, as unused params are ignored. */ +const PARAMETERS = { + addressName: 'addressName', + customerId: 'customerId', + itemId: 'itemId', + listId: 'listId', + paymentInstrumentId: 'paymentInstrumentId' +} +const createOptions = ( + body: Argument extends {body: infer B} ? B : undefined +): Argument => ({body, parameters: PARAMETERS}) + +// --- TEST CASES --- // +// This is an object rather than an array to more easily ensure we cover all mutations +// TODO: Remove optional flag when all mutations are implemented +type TestMap = {[Mut in ShopperCustomersMutation]?: [Argument, DataType]} +const testMap: TestMap = { + // Invalidate customer, update created item endpoint + createCustomerAddress: [createOptions<'createCustomerAddress'>(ADDRESS), ADDRESS], + // Invalidate customer, update created item endpoint + createCustomerPaymentInstrument: [ + createOptions<'createCustomerPaymentInstrument'>({ + bankRoutingNumber: 'bank', + giftCertificateCode: 'code', + // Type assertion so we don't need the full type + paymentCard: {} as ShopperCustomersTypes.CustomerPaymentCardRequest, + paymentMethodId: 'paymentMethodId' + }), + PAYMENT_INSTRUMENT + ], + // Invalidate product listS, update created item endpoint + createCustomerProductList: [ + createOptions<'createCustomerProductList'>({id: 'productListId'}), + {id: 'productListId'} + ], + // Invalidate product listS, update PL, update created item endpoint + createCustomerProductListItem: [ + createOptions<'createCustomerProductListItem'>(PRODUCT_LIST_ITEM), + PRODUCT_LIST_ITEM + ], + // Invalidate customer, remove + deleteCustomerPaymentInstrument: [ + createOptions<'deleteCustomerPaymentInstrument'>(undefined), + undefined + ], + // Invalidate product listS? invalidate PL, remove + deleteCustomerProductListItem: [ + createOptions<'deleteCustomerProductListItem'>(undefined), + undefined + ], + // noop + getResetPasswordToken: [ + createOptions<'getResetPasswordToken'>({login: 'login'}), + {email: 'customer@email', expiresInMinutes: 10, login: 'login', resetToken: 'token'} + ], + // noop + registerCustomer: [ + createOptions<'registerCustomer'>({customer: {}, password: 'hunter2'}), + {customerId: 'customerId'} + ], + // Invalidate customer, remove + removeCustomerAddress: [createOptions<'removeCustomerAddress'>(undefined), undefined], + // noop + resetPassword: [ + createOptions<'resetPassword'>({ + resetToken: 'token', + newPassword: 'hunter3', + login: 'login' + }), + undefined + ], + updateCustomer: [createOptions<'updateCustomer'>({}), {customerId: 'customerId'}], + // Update customer, invalidate * + updateCustomerAddress: [ + createOptions<'updateCustomerAddress'>({ + addressId: 'addressId', + countryCode: 'CA', + lastName: 'Doofenschmirtz' + }), + ADDRESS + ], + // invalidate PLs? invalidate PL, update PLI + updateCustomerProductListItem: [ + createOptions<'updateCustomerProductListItem'>({priority: 0, public: false, quantity: 0}), + PRODUCT_LIST_ITEM + ] +} +// Type assertion is necessary because `Object.entries` is limited :\ +const allTestCases = Object.entries(testMap) as Array< + [ShopperCustomersMutation, NonNullable] +> + +// Not implemented checks are temporary to make sure we don't forget to add tests when adding +// implentations. When all mutations are added, the "not implemented" tests can be removed, +// and the `TestMap` type can be changed from optional keys to required keys. Doing so will +// leverage TypeScript to enforce having tests for all mutations. +const notImplTestCases: ShopperCustomersMutation[][] = [ + ['authorizeCustomer'], + ['authorizeTrustedSystem'], + ['deleteCustomerProductList'], + ['invalidateCustomerAuth'], + ['updateCustomerPassword'], + ['updateCustomerProductList'] +] + +describe('ShopperCustomers mutations', () => { + beforeEach(() => nock.cleanAll()) + test.each(allTestCases)( + '`%s` returns data on success', + async (mutationName, [options, data]) => { + mockMutationEndpoints(customersEndpoint, data ?? {}) // Fallback for `void` endpoints + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperCustomersMutation(mutationName) + }) + expect(result.current.data).toBeUndefined() + act(() => result.current.mutate(options)) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + } + ) + test.each(allTestCases)('`%s` returns error on error', async (mutationName, [options]) => { + mockMutationEndpoints(customersEndpoint, {error: true}, 400) + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperCustomersMutation(mutationName) + }) + expect(result.current.error).toBeNull() + act(() => result.current.mutate(options)) + await waitAndExpectError(wait, () => result.current) + // Validate that we get a `ResponseError` from commerce-sdk-isomorphic. Ideally, we could do + // `.toBeInstanceOf(ResponseError)`, but the class isn't exported. :\ + expect(result.current.error).toHaveProperty('response') + }) + test.each(notImplTestCases)('`%s` is not yet implemented', async (mutationName) => { + expect(() => useShopperCustomersMutation(mutationName)).toThrow(NotImplementedError) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx deleted file mode 100644 index 1fdca6a8db..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.tsx +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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 - */ -import React from 'react' -import {renderWithProviders, createQueryClient, DEFAULT_TEST_HOST} from '../../test-utils' -import {fireEvent, screen, waitFor} from '@testing-library/react' -import {ShopperCustomersMutation, useShopperCustomersMutation} from './mutation' -import nock from 'nock' -import {ApiClients, Argument} from '../types' - -jest.mock('../../auth/index.ts', () => { - return jest.fn().mockImplementation(() => ({ - ready: jest.fn().mockResolvedValue({access_token: '123'}) - })) -}) - -const CUSTOMER_ID = 'CUSTOMER_ID' -const ADDRESS_NAME = 'ADDRESS_NAME' -const LIST_ID = 'LIST_ID' -const ITEM_ID = 'ITEM_ID' -const PAYMENT_INSTRUMENT_ID = 'PAYMENT_INSTRUMENT_ID' -const PRODUCT_ID = 'PRODUCT_ID' - -type MutationPayloads = { - [Mutation in ShopperCustomersMutation]?: Argument -} - -const mutationPayloads: MutationPayloads = { - updateCustomer: { - body: {firstName: `Kobe`}, - parameters: {customerId: CUSTOMER_ID} - }, - - createCustomerAddress: { - body: {addressId: `TestNewAddress`, countryCode: 'CA', lastName: 'Murphy'}, - parameters: {customerId: CUSTOMER_ID} - }, - - updateCustomerAddress: { - body: {addressId: 'TestAddress', countryCode: 'US', lastName: `Murphy`}, - parameters: {customerId: CUSTOMER_ID, addressName: ADDRESS_NAME} - }, - - removeCustomerAddress: { - parameters: {customerId: CUSTOMER_ID, addressName: `TestNewAddress`} - }, - - createCustomerProductList: { - body: {type: 'wish_list'}, - parameters: {customerId: CUSTOMER_ID} - }, - - createCustomerProductListItem: { - body: {priority: 2, public: true, quantity: 3, type: 'product', productId: PRODUCT_ID}, - parameters: {customerId: CUSTOMER_ID, listId: LIST_ID} - }, - - updateCustomerProductListItem: { - body: {priority: 2, public: true, quantity: 13}, - parameters: {customerId: CUSTOMER_ID, listId: LIST_ID, itemId: ITEM_ID} - }, - - deleteCustomerProductListItem: { - parameters: {customerId: CUSTOMER_ID, listId: LIST_ID, itemId: ITEM_ID} - }, - - createCustomerPaymentInstrument: { - body: { - bankRoutingNumber: 'AB1234', - giftCertificateCode: 'gift-code', - paymentCard: { - number: '4454852652415965', - validFromMonth: 12, - expirationYear: 2030, - expirationMonth: 12, - cardType: 'Visa', - holder: 'John Smith', - issueNumber: '92743928', - validFromYear: 22 - }, - paymentMethodId: 'Credit Card' - }, - parameters: {customerId: CUSTOMER_ID} - }, - deleteCustomerPaymentInstrument: { - parameters: {customerId: CUSTOMER_ID, paymentInstrumentId: PAYMENT_INSTRUMENT_ID} - } -} - -interface CustomerMutationComponentParams { - action: ShopperCustomersMutation -} -const CustomerMutationComponent = ({action}: CustomerMutationComponentParams) => { - const mutationHook = useShopperCustomersMutation(action) - - return ( - <> -
- - {mutationHook.error?.message && ( -

Error: {mutationHook.error?.message}

- )} -
- {mutationHook.isSuccess && isSuccess} -
- - ) -} - -const tests = (Object.keys(mutationPayloads) as ShopperCustomersMutation[]).map((mutationName) => { - return { - hook: mutationName, - cases: [ - { - name: 'success', - assertions: async () => { - nock(DEFAULT_TEST_HOST) - .patch((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(200, {}) - .put((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(200, {}) - .post((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(200, {}) - .delete((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(204, {}) - - const queryClient = createQueryClient() - - renderWithProviders( - , - { - queryClient - } - ) - await waitFor(() => - screen.getByRole('button', { - name: mutationName - }) - ) - - const mutation = shopperCustomersCacheUpdateMatrix[mutationName] - - // Pre-populate cache with query keys we invalidate/update/remove onSuccess - const {invalidate, update, remove} = mutation( - mutationPayloads[mutationName], - {} - ) - - const queryKeys = [...(invalidate || []), ...(update || []), ...(remove || [])] - - queryKeys.forEach(({key: queryKey}) => { - queryClient.setQueryData(queryKey, () => ({test: true})) - }) - - const button = screen.getByRole('button', { - name: mutationName - }) - - fireEvent.click(button) - await waitFor(() => screen.getByText(/isSuccess/i)) - expect(screen.getByText(/isSuccess/i)).toBeInTheDocument() - - // Assert changes in cache - update?.forEach(({key: queryKey}) => { - expect(queryClient.getQueryData(queryKey)).not.toEqual({test: true}) - }) - invalidate?.forEach(({key: queryKey}) => { - expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBeTruthy() - }) - remove?.forEach(({key: queryKey}) => { - expect(queryClient.getQueryState(queryKey)).toBeFalsy() - }) - } - }, - { - name: 'error', - assertions: async () => { - nock(DEFAULT_TEST_HOST) - .patch((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - .put((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - .post((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - .delete((uri) => { - return uri.includes('/customer/shopper-customers/') - }) - .reply(500, {}) - - renderWithProviders( - - ) - await waitFor(() => - screen.getByRole('button', { - name: mutationName - }) - ) - - const button = screen.getByRole('button', { - name: mutationName - }) - - fireEvent.click(button) - await waitFor(() => screen.getByText(/error/i)) - expect(screen.getByText(/error/i)).toBeInTheDocument() - } - } - ] - } -}) - -tests.forEach(({hook, cases}) => { - describe(hook, () => { - beforeEach(() => { - jest.clearAllMocks() - }) - cases.forEach(({name, assertions}) => { - test(name, assertions) - }) - }) -}) - -// TODO: change to new format -test.each([] as ShopperCustomersMutation[])( - '%j - throws error when not implemented', - (methodName) => { - const action = methodName - expect(() => { - useShopperCustomersMutation(action) - }).toThrowError('This method is not implemented.') - } -) From 33d04f05e121caff985c7f23483bc98c829ed32f Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 24 Feb 2023 17:53:39 -0500 Subject: [PATCH 103/122] Export query key types. --- .../src/hooks/ShopperBaskets/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperContexts/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperCustomers/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperExperience/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperLogin/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperOrders/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperProducts/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperPromotions/queryKeyHelpers.ts | 2 +- .../src/hooks/ShopperSearch/queryKeyHelpers.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts index db44c25be0..8f75b0a606 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperBaskets<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getBasket: ['/organizations/', string, '/baskets/', string, Params<'getBasket'>] getPaymentMethodsForBasket: [ '/organizations/', diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts index b6e734731b..4ed126e213 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperContexts<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getShopperContext: [ '/organizations/', string, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts index 703c6dc67c..faa8df6222 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperCustomers<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getExternalProfile: [ '/organizations/', string, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts index d1392a8662..01a14f5090 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperExperience<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getPages: ['/organizations/', string, '/pages', Params<'getPages'>] getPage: ['/organizations/', string, '/pages/', string, Params<'getPage'>] } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts index 06e684ad5d..8c75dff6d7 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperGiftCertificates<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getGiftCertificate: [ '/organizations/', string, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts index 8d5cdc88ba..d4c8d92768 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperLogin<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { retrieveCredQualityUserInfo: [ '/organizations/', string, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts index dc1482ce11..00a64a413f 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperOrders<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getOrder: ['/organizations/', string, '/orders/', string, Params<'getOrder'>] getPaymentMethodsForOrder: [ '/organizations/', diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts index 53d6d1c434..e3a07e4134 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperProducts<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getProducts: ['/organizations/', string, '/products', Params<'getProducts'>] getProduct: ['/organizations/', string, '/products/', string, Params<'getProduct'>] getCategories: ['/organizations/', string, '/categories', Params<'getCategories'>] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts index 05a5b2d650..6ed70a9589 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperPromotions<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { getPromotions: ['/organizations/', string, '/promotions', Params<'getPromotions'>] getPromotionsForCampaign: [ '/organizations/', diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts index 1fff07af1a..43259012fd 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/queryKeyHelpers.ts @@ -11,7 +11,7 @@ import {pick} from '../utils' // We must use a client with no parameters in order to have required/optional match the API spec type Client = ShopperSearch<{shortCode: string}> type Params = NonNullable['parameters']> -type QueryKeys = { +export type QueryKeys = { productSearch: ['/organizations/', string, '/product-search', Params<'productSearch'>] getSearchSuggestions: [ '/organizations/', From 448a8265a615b10842f5307550ad46cde81bc7b5 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 24 Feb 2023 17:54:01 -0500 Subject: [PATCH 104/122] Remove old file. --- .../src/hooks/ShopperBaskets/queryKey.ts | 138 ------------------ 1 file changed, 138 deletions(-) delete mode 100644 packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts deleted file mode 100644 index b8c8475a4c..0000000000 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/queryKey.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2023, 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 type {ShopperBaskets} from 'commerce-sdk-isomorphic' -import {Argument, ExcludeTail} from '../types' -import {pick} from '../utils' - -// We must use a client with no parameters in order to have required/optional match the API spec -type Client = ShopperBaskets<{shortCode: string}> -type Params> = NonNullable< - Argument['parameters'] -> -export type QueryKeys = { - getBasket: readonly ['/organizations/', string, '/baskets/', string, Params<'getBasket'>] - getPaymentMethodsForBasket: readonly [ - '/organizations/', - string, - '/baskets/', - string, - '/payment-methods', - Params<'getPaymentMethodsForBasket'> - ] - getPriceBooksForBasket: readonly [ - '/organizations/', - string, - '/baskets/', - string, - '/price-books', - Params<'getPriceBooksForBasket'> - ] - getShippingMethodsForShipment: readonly [ - '/organizations/', - string, - '/baskets/', - string, - '/shipments/', - string, - '/shipping-methods', - Params<'getShippingMethodsForShipment'> - ] - getTaxesFromBasket: readonly [ - '/organizations/', - string, - '/baskets/', - string, - '/taxes', - Params<'getTaxesFromBasket'> - ] -} - -// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, -// and making those generic would add too much complexity. -type QueryKeyHelper = { - /** - * Reduces the given parameters (which may have additional, unknown properties) to an object - * containing *only* the properties required for an endpoint. - */ - parameters: (params: Params) => Params - /** Generates the path component of the query key for an endpoint. */ - path: (params: Params) => ExcludeTail - /** Generates the full query key for an endpoint. */ - queryKey: (params: Params) => QueryKeys[T] -} - -export const getBasket: QueryKeyHelper<'getBasket'> = { - parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId', 'locale']), - path: (params) => ['/organizations/', params.organizationId, '/baskets/', params.basketId], - queryKey: (params: Params<'getBasket'>) => [ - ...getBasket.path(params), - getBasket.parameters(params) - ] -} - -export const getPaymentMethodsForBasket: QueryKeyHelper<'getPaymentMethodsForBasket'> = { - parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId', 'locale']), - path: (params) => [ - '/organizations/', - params.organizationId, - '/baskets/', - params.basketId, - '/payment-methods' - ], - queryKey: (params: Params<'getPaymentMethodsForBasket'>) => [ - ...getPaymentMethodsForBasket.path(params), - getPaymentMethodsForBasket.parameters(params) - ] -} - -export const getPriceBooksForBasket: QueryKeyHelper<'getPriceBooksForBasket'> = { - parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId']), - path: (params) => [ - '/organizations/', - params.organizationId, - '/baskets/', - params.basketId, - '/price-books' - ], - queryKey: (params: Params<'getPriceBooksForBasket'>) => [ - ...getPriceBooksForBasket.path(params), - getPriceBooksForBasket.parameters(params) - ] -} - -export const getShippingMethodsForShipment: QueryKeyHelper<'getShippingMethodsForShipment'> = { - parameters: (params) => - pick(params, ['organizationId', 'basketId', 'shipmentId', 'siteId', 'locale']), - path: (params) => [ - '/organizations/', - params.organizationId, - '/baskets/', - params.basketId, - '/shipments/', - params.shipmentId, - '/shipping-methods' - ], - queryKey: (params: Params<'getShippingMethodsForShipment'>) => [ - ...getShippingMethodsForShipment.path(params), - getShippingMethodsForShipment.parameters(params) - ] -} - -export const getTaxesFromBasket: QueryKeyHelper<'getTaxesFromBasket'> = { - parameters: (params) => pick(params, ['organizationId', 'basketId', 'siteId']), - path: (params) => [ - '/organizations/', - params.organizationId, - '/baskets/', - params.basketId, - '/taxes' - ], - queryKey: (params: Params<'getTaxesFromBasket'>) => [ - ...getTaxesFromBasket.path(params), - getTaxesFromBasket.parameters(params) - ] -} From 87933ff3604fb4f79a96d915c800055490636892 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Fri, 24 Feb 2023 23:15:05 -0500 Subject: [PATCH 105/122] Update cache logic to use query keys instead of predicates. --- .../src/hooks/ShopperCustomers/cache.ts | 177 +++++------------- .../src/hooks/ShopperOrders/cache.ts | 22 +-- .../commerce-sdk-react/src/hooks/types.ts | 25 ++- .../commerce-sdk-react/src/hooks/utils.ts | 12 +- 4 files changed, 84 insertions(+), 152 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts index 515886a695..bfe453f359 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts @@ -4,8 +4,15 @@ * 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 {ApiClientConfigParams, ApiClients, CacheUpdateMatrix} from '../types' -import {and, matchesApiConfig, matchesPath, pathStartsWith} from '../utils' +import {getCustomerProductListItem, QueryKeys} from './queryKeyHelpers' +import {ApiClients, CacheUpdate, CacheUpdateMatrix, Tail} from '../types' +import { + getCustomer, + getCustomerAddress, + getCustomerPaymentInstrument, + getCustomerProductList, + getCustomerProductLists +} from './queryKeyHelpers' type Client = ApiClients['shopperCustomers'] @@ -15,139 +22,73 @@ const TODO = (method: keyof Client) => { console.warn(`Cache logic for '${method}' is not yet implemented.`) return undefined } -// Path helpers (avoid subtle typos!) -const getCustomerPath = (customerId: string, parameters: ApiClientConfigParams) => [ - '/organizations/', - parameters.organizationId, - '/customers/', - customerId -] -const getProductListPath = ( - customerId: string, - parameters: ApiClientConfigParams & {listId: string} -) => [...getCustomerPath(customerId, parameters), '/product-lists/', parameters.listId] -const getProductListItemPath = ( - customerId: string, - parameters: ApiClientConfigParams & {listId: string; itemId: string} -) => [...getProductListPath(customerId, parameters), '/items/', parameters.itemId] -const getPaymentInstrumentPath = ( - customerId: string, - parameters: ApiClientConfigParams & {paymentInstrumentId: string} -) => [ - ...getCustomerPath(customerId, parameters), - '/payment-instruments/', - parameters.paymentInstrumentId -] -const getCustomerAddressPath = ( - customerId: string, - parameters: ApiClientConfigParams, - address: string // Not a parameter because some endpoints use addressId and some use addressName -) => [...getCustomerPath(customerId, parameters), '/addresses/', address] /** Invalidates the customer endpoint, but not derivative endpoints. */ -const invalidateCustomer = (customerId: string, parameters: ApiClientConfigParams) => { - return { - invalidate: [ - and(matchesApiConfig(parameters), matchesPath(getCustomerPath(customerId, parameters))) - ] - } -} +const invalidateCustomer = (parameters: Tail): CacheUpdate => ({ + invalidate: [{queryKey: getCustomer.queryKey(parameters)}] +}) export const cacheUpdateMatrix: CacheUpdateMatrix = { authorizeCustomer: TODO('authorizeCustomer'), authorizeTrustedSystem: TODO('authorizeTrustedSystem'), createCustomerAddress(customerId, {parameters}, response) { - if (!customerId) return {} // getCustomerAddress uses `addressName` rather than `addressId` - const address = response.addressId - const newParams = {...parameters, addressName: address} + const newParams = {...parameters, addressName: response.addressId} return { - ...invalidateCustomer(customerId, parameters), - update: [ - {queryKey: [...getCustomerAddressPath(customerId, newParams, address), newParams]} - ] + // TODO: Rather than invalidate, can we selectively update? + ...invalidateCustomer(newParams), + update: [{queryKey: getCustomerAddress.queryKey(newParams)}] } }, createCustomerPaymentInstrument(customerId, {parameters}, response) { - if (!customerId) return {} const newParams = {...parameters, paymentInstrumentId: response.paymentInstrumentId} return { - ...invalidateCustomer(customerId, parameters), - update: [{queryKey: [...getPaymentInstrumentPath(customerId, newParams), newParams]}] + // TODO: Rather than invalidate, can we selectively update? + ...invalidateCustomer(newParams), + update: [{queryKey: getCustomerPaymentInstrument.queryKey(newParams)}] } }, createCustomerProductList(customerId, {parameters}, response) { - if (!customerId) return {} - const customerPath = getCustomerPath(customerId, parameters) // We always invalidate, because even without an ID we assume that something has changed - const invalidate = [ - and( - matchesApiConfig(parameters), - // NOTE: This is the aggregate endpoint, so there's no trailing / on /product-lists - matchesPath([...customerPath, '/product-lists']) - ) + // TODO: Rather than invalidate, can we selectively update? + const invalidate: CacheUpdate['invalidate'] = [ + {queryKey: getCustomerProductLists.queryKey(parameters)} ] + // We can only update cache for this product list if we have the ID const listId = response.id - // We can only update cache for this product list if we get the ID if (!listId) return {invalidate} - const newParams = {...parameters, listId} return { invalidate, - update: [{queryKey: [...getProductListPath(customerId, newParams), newParams]}] + update: [{queryKey: getCustomerProductList.queryKey({...parameters, listId})}] } }, createCustomerProductListItem(customerId, {parameters}, response) { - if (!customerId) return {} + // We always invalidate, because even without an ID we assume that something has changed + // TODO: Rather than invalidate, can we selectively update? + const invalidate: CacheUpdate['invalidate'] = [ + {queryKey: getCustomerProductList.queryKey(parameters)} + ] + // We can only update cache for this product list item if we have the ID const itemId = response.id + if (!itemId) return {invalidate} return { - // We can only update cache if the response comes with an ID - update: !itemId - ? [] - : [ - { - queryKey: [ - ...getProductListItemPath(customerId, {...parameters, itemId}), - {...parameters, itemId} - ] - } - ], - // We always invalidate, because even without an ID we assume that something has changed - invalidate: [ - and( - matchesApiConfig(parameters), - matchesPath(getProductListPath(customerId, parameters)) - ) - ] + invalidate, + update: [{queryKey: getCustomerProductListItem.queryKey({...parameters, itemId})}] } }, deleteCustomerPaymentInstrument(customerId, {parameters}) { - if (!customerId) return {} return { - ...invalidateCustomer(customerId, parameters), - remove: [ - and( - matchesApiConfig(parameters), - matchesPath(getPaymentInstrumentPath(customerId, parameters)) - ) - ] + // TODO: Rather than invalidate, can we selectively update? + ...invalidateCustomer(parameters), + remove: [{queryKey: getCustomerPaymentInstrument.queryKey(parameters)}] } }, deleteCustomerProductList: TODO('deleteCustomerProductList'), deleteCustomerProductListItem(customerId, {parameters}) { - if (!customerId) return {} return { - invalidate: [ - and( - matchesApiConfig(parameters), - matchesPath(getProductListPath(customerId, parameters)) - ) - ], - remove: [ - and( - matchesApiConfig(parameters), - matchesPath(getProductListItemPath(customerId, parameters)) - ) - ] + // TODO: Rather than invalidate, can we selectively update? + invalidate: [{queryKey: getCustomerProductList.queryKey(parameters)}], + remove: [{queryKey: getCustomerProductListItem.queryKey(parameters)}] } }, getResetPasswordToken: noop, @@ -155,53 +96,35 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { registerCustomer: noop, registerExternalProfile: TODO('registerExternalProfile'), removeCustomerAddress(customerId, {parameters}) { - if (!customerId) return {} return { - ...invalidateCustomer(customerId, parameters), - remove: [ - and( - matchesApiConfig(parameters), - matchesPath( - getCustomerAddressPath(customerId, parameters, parameters.addressName) - ) - ) - ] + // TODO: Rather than invalidate, can we selectively update? + ...invalidateCustomer(parameters), + remove: [{queryKey: getCustomerAddress.queryKey(parameters)}] } }, resetPassword: noop, updateCustomer(customerId, {parameters}) { - if (!customerId) return {} - const customerPath = getCustomerPath(customerId, parameters) return { - update: [{queryKey: [...customerPath, parameters]}], + update: [{queryKey: getCustomer.queryKey(parameters)}], // This is NOT invalidateCustomer(), as we want to invalidate *all* customer endpoints, // not just getCustomer - invalidate: [and(matchesApiConfig(parameters), pathStartsWith(customerPath))] + invalidate: [{queryKey: getCustomer.path(parameters)}] } }, updateCustomerAddress(customerId, {parameters}) { - if (!customerId) return {} return { - // TODO: Rather than invalidating customer, can we selectively update its `addresses`? - ...invalidateCustomer(customerId, parameters), - update: [ - { - queryKey: [ - ...getCustomerAddressPath(customerId, parameters, parameters.addressName), - parameters - ] - } - ] + // TODO: Rather than invalidate, can we selectively update? + ...invalidateCustomer(parameters), + update: [{queryKey: getCustomerAddress.queryKey(parameters)}] } }, updateCustomerPassword: TODO('updateCustomerPassword'), updateCustomerProductList: TODO('updateCustomerProductList'), updateCustomerProductListItem(customerId, {parameters}) { - if (!customerId) return {} - const productListPath = getProductListPath(customerId, parameters) return { - update: [{queryKey: [...getProductListItemPath(customerId, parameters), parameters]}], - invalidate: [and(matchesApiConfig(parameters), matchesPath(productListPath))] + update: [{queryKey: getCustomerProductList.queryKey(parameters)}], + // TODO: Rather than invalidate, can we selectively update? + invalidate: [{queryKey: getCustomerProductListItem.queryKey(parameters)}] } } } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts index 708c40ee9a..9589d5d61c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts @@ -4,8 +4,9 @@ * 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 {getCustomerBaskets} from '../ShopperCustomers/queryKeyHelpers' import {ApiClients, CacheUpdateMatrix, CacheUpdateUpdate, CacheUpdateInvalidate} from '../types' -import {and, matchesApiConfig, matchesPath} from '../utils' +import {getOrder} from './queryKeyHelpers' type Client = ApiClients['shopperOrders'] @@ -22,28 +23,17 @@ const TODO = (method: keyof Client) => { export const cacheUpdateMatrix: CacheUpdateMatrix = { createOrder(customerId, {parameters}, response) { - const update: CacheUpdateUpdate[] = !response.orderNo + const {orderNo} = response + const update: CacheUpdateUpdate[] = !orderNo ? [] : [ { - queryKey: [ - ...basePath(parameters), - '/orders/', - response.orderNo, - {...parameters, orderNo: response.orderNo} - ] + queryKey: getOrder.queryKey({...parameters, orderNo}) } ] - const invalidate: CacheUpdateInvalidate[] = !customerId ? [] - : [ - and( - matchesApiConfig(parameters), - matchesPath([...basePath(parameters), '/customers/', customerId, '/baskets']) - ) - ] - + : [{queryKey: getCustomerBaskets.queryKey({...parameters, customerId})}] return {update, invalidate} }, createPaymentInstrumentForOrder: TODO('createPaymentInstrumentForOrder'), diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index addc260931..b68618106d 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -4,7 +4,7 @@ * 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 {Query, Updater, UseQueryOptions} from '@tanstack/react-query' +import {InvalidateQueryFilters, QueryFilters, Updater, UseQueryOptions} from '@tanstack/react-query' import { ShopperBaskets, ShopperContexts, @@ -42,11 +42,17 @@ type StringIndexNever = { /** Removes a string index type. */ export type RemoveStringIndex = RemoveNeverValues> -/** Remove the last entry from a readonly tuple type. */ +/** Gets the last element of an array. */ +export type Tail = T extends [...head: unknown[], tail: infer Tail] + ? Tail + : T + +/** Remove the last entry from a tuple type. */ export type ExcludeTail = T extends readonly [...infer Head, unknown] - ? Readonly - : // If it's a plain array, rather than a tuple, then removing the last element has no effect - T + ? T extends unknown[] // Preserve mutable or readonly from the original array + ? Head + : Readonly + : T // If it's a plain array, rather than a tuple, then removing the last element has no effect // --- API CLIENTS --- // export type ApiParameter = string | number | boolean | string[] | number[] @@ -149,10 +155,15 @@ export type CacheUpdateUpdate = { } /** Query predicate for queries to invalidate */ -export type CacheUpdateInvalidate = (query: Query) => boolean +export type CacheUpdateInvalidate = + | InvalidateQueryFilters + // TODO: Change Shopper Baskets cache logic from using predicates to creating query filters + // using the query key helpers, then this 'predicate' type can be removed, as can the one in + // `CacheUpdateRemove`. The predicate helpers in `utils.ts` should also then be removed. + | NonNullable /** Query predicate for queries to remove */ -export type CacheUpdateRemove = (query: Query) => boolean +export type CacheUpdateRemove = QueryFilters | NonNullable /** Collection of updates to make to the cache when a request completes. */ export type CacheUpdate = { diff --git a/packages/commerce-sdk-react/src/hooks/utils.ts b/packages/commerce-sdk-react/src/hooks/utils.ts index 7d6ab63a80..f65d339dd7 100644 --- a/packages/commerce-sdk-react/src/hooks/utils.ts +++ b/packages/commerce-sdk-react/src/hooks/utils.ts @@ -9,8 +9,16 @@ import {ApiClient, ApiOptions, ApiParameter, CacheUpdate, MergedOptions} from '. /** Applies the set of cache updates to the query client. */ export const updateCache = (queryClient: QueryClient, cacheUpdates: CacheUpdate, data: unknown) => { - cacheUpdates.invalidate?.forEach((predicate) => queryClient.invalidateQueries({predicate})) - cacheUpdates.remove?.forEach((predicate) => queryClient.removeQueries({predicate})) + cacheUpdates.invalidate?.forEach((invalidate) => { + // TODO: Update Shopper Baskets cache logic to not use predicate functions, and then this + // check will no longer be needed. (Same for the remove block.) + const filters = typeof invalidate === 'function' ? {predicate: invalidate} : invalidate + queryClient.invalidateQueries(filters) + }) + cacheUpdates.remove?.forEach((remove) => { + const filters = typeof remove === 'function' ? {predicate: remove} : remove + queryClient.removeQueries(filters) + }) cacheUpdates.update?.forEach(({queryKey, updater}) => // If an updater isn't given, fall back to just setting the data queryClient.setQueryData(queryKey, updater ?? data) From 09d7899d2660c0f502d7dc1669e34b3bf9d1cd66 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Sat, 25 Feb 2023 14:31:07 -0500 Subject: [PATCH 106/122] Update parser for proper TypeScript support. babel-eslint is deprecated and superseded by @babel/eslint-parser. --- packages/pwa-kit-dev/package-lock.json | 43 +++++++++++++------ packages/pwa-kit-dev/package.json | 2 +- .../src/configs/eslint/eslint-config.js | 2 +- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/pwa-kit-dev/package-lock.json b/packages/pwa-kit-dev/package-lock.json index 36a39326b8..c4e86da729 100644 --- a/packages/pwa-kit-dev/package-lock.json +++ b/packages/pwa-kit-dev/package-lock.json @@ -78,6 +78,28 @@ } } }, + "@babel/eslint-parser": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", + "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "requires": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "@babel/generator": { "version": "7.17.10", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", @@ -1698,6 +1720,14 @@ "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", "optional": true }, + "@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "requires": { + "eslint-scope": "5.1.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2688,19 +2718,6 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, "babel-jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", diff --git a/packages/pwa-kit-dev/package.json b/packages/pwa-kit-dev/package.json index e29d7367c2..f8bc598ebb 100644 --- a/packages/pwa-kit-dev/package.json +++ b/packages/pwa-kit-dev/package.json @@ -37,6 +37,7 @@ "dependencies": { "@babel/cli": "^7.4.4", "@babel/core": "^7.4.5", + "@babel/eslint-parser": "^7.19.1", "@babel/parser": "^7.5.5", "@babel/plugin-proposal-object-rest-spread": "^7.4.4", "@babel/plugin-transform-async-to-generator": "^7.7.0", @@ -55,7 +56,6 @@ "@loadable/webpack-plugin": "^5.15.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.6", "archiver": "1.3.0", - "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-loader": "^8.0.6", "babel-plugin-dynamic-import-node-babel-7": "^2.0.7", diff --git a/packages/pwa-kit-dev/src/configs/eslint/eslint-config.js b/packages/pwa-kit-dev/src/configs/eslint/eslint-config.js index 818c691ead..8f8cb09ad5 100644 --- a/packages/pwa-kit-dev/src/configs/eslint/eslint-config.js +++ b/packages/pwa-kit-dev/src/configs/eslint/eslint-config.js @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ module.exports = { - parser: 'babel-eslint', + parser: '@babel/eslint-parser', parserOptions: { ecmaVersion: 2017, sourceType: 'module', From b162e037c63fb0648f0e33bf7d5814c19682ed19 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Sat, 25 Feb 2023 14:34:01 -0500 Subject: [PATCH 107/122] Remove unused babel-eslint. --- packages/internal-lib-build/package-lock.json | 13 ------------- packages/internal-lib-build/package.json | 1 - 2 files changed, 14 deletions(-) diff --git a/packages/internal-lib-build/package-lock.json b/packages/internal-lib-build/package-lock.json index 79c67e7d50..8473d2e45e 100644 --- a/packages/internal-lib-build/package-lock.json +++ b/packages/internal-lib-build/package-lock.json @@ -2567,19 +2567,6 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, "babel-jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", diff --git a/packages/internal-lib-build/package.json b/packages/internal-lib-build/package.json index 84567f3f8e..1e9d244265 100644 --- a/packages/internal-lib-build/package.json +++ b/packages/internal-lib-build/package.json @@ -41,7 +41,6 @@ "@babel/traverse": "^7.5.5", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", - "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-loader": "^8.0.6", "babel-plugin-dynamic-import-node-babel-7": "^2.0.7", From 1ec20eedd8638db18e6f5b5374868fc95661c577 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Sun, 26 Feb 2023 11:54:48 -0500 Subject: [PATCH 108/122] Temporarily(?) use internal-lib-build so TypeScript files are properly linted. --- packages/test-commerce-sdk-react/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/test-commerce-sdk-react/package.json b/packages/test-commerce-sdk-react/package.json index bbbd58c018..380c9ab153 100644 --- a/packages/test-commerce-sdk-react/package.json +++ b/packages/test-commerce-sdk-react/package.json @@ -14,6 +14,7 @@ "@types/react-dom": "^17.0.2", "@types/react-router-dom": "^5.1.2", "commerce-sdk-react-preview": "^2.7.0-dev", + "internal-lib-build": "^2.7.0-dev", "pwa-kit-dev": "^2.7.0-dev", "pwa-kit-react-sdk": "^2.7.0-dev", "pwa-kit-runtime": "^2.7.0-dev", @@ -23,8 +24,8 @@ "react-router-dom": "^5.1.2" }, "scripts": { - "format": "pwa-kit-dev format \"**/*.{js,jsx,tsx}\"", - "lint": "pwa-kit-dev lint \"**/*.{js,jsx,tsx}\"", + "format": "internal-lib-build format \"**/*.{js,jsx,tsx}\"", + "lint": "internal-lib-build lint \"**/*.{js,jsx,tsx}\"", "lint:fix": "npm run lint -- --fix", "save-credentials": "pwa-kit-dev save-credentials", "start": "pwa-kit-dev start", From 8732c1fb2401709b02e8850c9faedc4488b0e26f Mon Sep 17 00:00:00 2001 From: Will Harney Date: Sun, 26 Feb 2023 11:55:21 -0500 Subject: [PATCH 109/122] Fix eslint and tsc errors. --- .../app/components/_app-config/index.tsx | 5 +--- .../app/pages/query-errors.tsx | 30 +++++++++++++------ .../app/pages/use-shopper-login-helper.tsx | 11 +++---- packages/test-commerce-sdk-react/pwa-kit.d.ts | 11 +++++++ 4 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 packages/test-commerce-sdk-react/pwa-kit.d.ts diff --git a/packages/test-commerce-sdk-react/app/components/_app-config/index.tsx b/packages/test-commerce-sdk-react/app/components/_app-config/index.tsx index a42deedce5..be010bf31b 100644 --- a/packages/test-commerce-sdk-react/app/components/_app-config/index.tsx +++ b/packages/test-commerce-sdk-react/app/components/_app-config/index.tsx @@ -1,15 +1,12 @@ /* - * Copyright (c) 2022, salesforce.com, inc. + * Copyright (c) 2023, 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 React, {useState, ReactElement} from 'react' -// @ts-ignore import {CommerceApiProvider} from 'commerce-sdk-react-preview' -// @ts-ignore import {withReactQuery} from 'pwa-kit-react-sdk/ssr/universal/components/with-react-query' -// @ts-ignore import {useCorrelationId} from 'pwa-kit-react-sdk/ssr/universal/hooks' interface AppConfigProps { diff --git a/packages/test-commerce-sdk-react/app/pages/query-errors.tsx b/packages/test-commerce-sdk-react/app/pages/query-errors.tsx index 37291ab2e0..a0c20a5c36 100644 --- a/packages/test-commerce-sdk-react/app/pages/query-errors.tsx +++ b/packages/test-commerce-sdk-react/app/pages/query-errors.tsx @@ -10,10 +10,22 @@ import {useProducts, useProduct} from 'commerce-sdk-react-preview' import Json from '../components/Json' const QueryErrors = () => { - // TODO: `products` no longer has an error, because the query is not enabled unless all required - // parameters are passed - const products = useProducts({parameters: {FOO: ''} as Record}) + // TODO: `products` is a contrived situation, as the query hook is disabled by default until all + // required parameters have been set. Also, unused parameters (like `FOO`) are simply ignored. + // Do we still want to highlight this error? + + // Type assertion because we're explicitly violating the expected type + const products = useProducts({parameters: {FOO: ''}} as any, {enabled: true}) const product = useProduct({parameters: {id: '25502228Mxxx'}}) + /** Errors don't nicely serialize to JSON, so we have to do it ourselves. */ + const toLoggable = (err: unknown) => { + if (err instanceof Error) { + // Clone all keys onto a plain object + const keys = Object.getOwnPropertyNames(err) as Array + return keys.reduce((acc, key) => ({...acc, [key]: err[key]}), {}) + } + return err + } return ( <> @@ -36,14 +48,14 @@ const QueryErrors = () => {

Validation Error

{products.isLoading &&

Loading...

} - {products.error &&

Something is wrong

} + {products.error &&

Something is wrong.

}
-
Returning data
+
Returning data:
@@ -51,14 +63,14 @@ const QueryErrors = () => {

Response Error

{product.isLoading &&

Loading...

} - {product.error &&

Something is wrong

} + {product.error &&

Something is wrong.

}
-
Returning data
+
Returning data:
diff --git a/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx b/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx index e2fa1f429a..4047cf8154 100644 --- a/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx +++ b/packages/test-commerce-sdk-react/app/pages/use-shopper-login-helper.tsx @@ -13,6 +13,7 @@ const UseShopperLoginHelper = () => { const loginGuestUser = useAuthHelper(AuthHelpers.LoginGuestUser) const loginRegisteredUser = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C) const logout = useAuthHelper(AuthHelpers.Logout) + const isError = (err: unknown): err is Error => err instanceof Error //logout before logging guest user in const loginGuestUserFlow = async () => { @@ -25,8 +26,8 @@ const UseShopperLoginHelper = () => {

LoginGuestUser

- {loginGuestUser.error?.message && ( -

Error: {loginGuestUser.error?.message}

+ {isError(loginGuestUser.error) && ( +

Error: {loginGuestUser.error.message}

)}

LoginRegisteredUserB2C

@@ -38,14 +39,14 @@ const UseShopperLoginHelper = () => { > loginRegisteredUser - {loginRegisteredUser.error?.message && ( -

Error: {loginRegisteredUser.error?.message}

+ {isError(loginRegisteredUser.error) && ( +

Error: {loginRegisteredUser.error.message}

)}

Logout

- {logout.error?.message &&

Error: {logout.error?.message}

} + {isError(logout.error) &&

Error: {logout.error.message}

} ) } diff --git a/packages/test-commerce-sdk-react/pwa-kit.d.ts b/packages/test-commerce-sdk-react/pwa-kit.d.ts new file mode 100644 index 0000000000..3699ded717 --- /dev/null +++ b/packages/test-commerce-sdk-react/pwa-kit.d.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2023, 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 + */ +// pwa-kit-* packages currently do not export any type definitions, which causes the TypeScript +// compiler to report errors when the packages are used. Until the packages provide their own +// definitions, this file serves to mitigate the errors by typing the packages' exports as `any`. +declare module 'pwa-kit-react-sdk/ssr/universal/components/with-react-query' +declare module 'pwa-kit-react-sdk/ssr/universal/hooks' From f0a119f32e094c5a63129cba38378f54b4f5e1d1 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 27 Feb 2023 11:32:19 -0500 Subject: [PATCH 110/122] Temporarily suppress linter errors. --- .../app/components/hello-typescript.tsx | 3 +++ packages/template-typescript-minimal/app/pages/home.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/template-typescript-minimal/app/components/hello-typescript.tsx b/packages/template-typescript-minimal/app/components/hello-typescript.tsx index 6539d40808..90df53efb2 100644 --- a/packages/template-typescript-minimal/app/components/hello-typescript.tsx +++ b/packages/template-typescript-minimal/app/components/hello-typescript.tsx @@ -4,6 +4,9 @@ * 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 */ +// The eslint config used from pwa-kit-dev does not properly parse TypeScript. :\ +// TODO: Remove this directive when the eslint config is updated. +/* eslint-disable no-undef */ import React from 'react' interface Props { diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index cbdeffdba7..eb58482909 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -4,6 +4,9 @@ * 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 */ +// The eslint config used from pwa-kit-dev does not properly parse TypeScript. :\ +// TODO: Remove this directive when the eslint config is updated. +/* eslint-disable no-undef */ import React, {useEffect, useState} from 'react' import {useQuery} from '@tanstack/react-query' From 96347e3b4e703f5b69c6980a39e58d6edcc34b1d Mon Sep 17 00:00:00 2001 From: Will Harney Date: Mon, 27 Feb 2023 18:17:23 -0500 Subject: [PATCH 111/122] Implement tests for shopper customers mutations that modify a customer. --- .../src/hooks/ShopperCustomers/cache.ts | 17 +- .../hooks/ShopperCustomers/mutation.test.ts | 324 +++++++++++++----- 2 files changed, 261 insertions(+), 80 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts index bfe453f359..b30e275ec7 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts @@ -4,6 +4,7 @@ * 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 {Query} from '@tanstack/react-query' import {getCustomerProductListItem, QueryKeys} from './queryKeyHelpers' import {ApiClients, CacheUpdate, CacheUpdateMatrix, Tail} from '../types' import { @@ -13,6 +14,7 @@ import { getCustomerProductList, getCustomerProductLists } from './queryKeyHelpers' +import {and, pathStartsWith} from '../utils' type Client = ApiClients['shopperCustomers'] @@ -93,6 +95,7 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { }, getResetPasswordToken: noop, invalidateCustomerAuth: TODO('invalidateCustomerAuth'), + // TODO: Should this update the `getCustomer` cache? registerCustomer: noop, registerExternalProfile: TODO('registerExternalProfile'), removeCustomerAddress(customerId, {parameters}) { @@ -104,11 +107,19 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { }, resetPassword: noop, updateCustomer(customerId, {parameters}) { + // When we update a customer, we don't know what data has changed, so we must invalidate all + // derivative endpoints. They conveniently all start with the same path as `getCustomer`, + // but we do NOT want to invalidate `getCustomer` itself, we want to _update_ it. (Ideally, + // we could invalidate *then* update, but React Query can't handle that.) To do so, we + // examine the path of each cached query. If it starts with the `getCustomer` path, we + // invalidate, UNLESS the first item afer the path is an object, because that means that it + // is the `getCustomer` query itself. + const path = getCustomer.path(parameters) + const isNotGetCustomer = ({queryKey}: Query) => typeof queryKey[path.length] !== 'object' + const predicate = and(pathStartsWith(path), isNotGetCustomer) return { update: [{queryKey: getCustomer.queryKey(parameters)}], - // This is NOT invalidateCustomer(), as we want to invalidate *all* customer endpoints, - // not just getCustomer - invalidate: [{queryKey: getCustomer.path(parameters)}] + invalidate: [{predicate}] } }, updateCustomerAddress(customerId, {parameters}) { diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts index e39028e3ef..79f90545f7 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts @@ -8,13 +8,16 @@ import {act} from '@testing-library/react' import {ShopperCustomersTypes} from 'commerce-sdk-isomorphic' import nock from 'nock' import { + assertInvalidateQuery, + assertRemoveQuery, + assertUpdateQuery, mockMutationEndpoints, + mockQueryEndpoint, renderHookWithProviders, - waitAndExpectError, waitAndExpectSuccess } from '../../test-utils' import {ShopperCustomersMutation, useShopperCustomersMutation} from '../ShopperCustomers' -import {ApiClients, Argument, DataType} from '../types' +import {ApiClients, Argument, DataType, RequireKeys} from '../types' import {NotImplementedError} from '../utils' import * as queries from './query' @@ -26,53 +29,72 @@ jest.mock('../../auth/index.ts', () => { type Client = ApiClients['shopperCustomers'] const customersEndpoint = '/customer/shopper-customers/' -const ADDRESS: ShopperCustomersTypes.CustomerAddress = { +/** All Shopper Customers parameters. Can be used for all endpoints, as unused params are ignored. */ +const PARAMETERS = { + addressName: 'address', // Not 'addressName' because sometimes it's used as `addressId` + customerId: 'customerId', + itemId: 'itemId', + listId: 'listId', + paymentInstrumentId: 'paymentInstrumentId' +} +/** Options object that can be used for all query endpoints. */ +const queryOptions = {parameters: PARAMETERS} +const oldAddress: ShopperCustomersTypes.CustomerAddress = { addressId: 'address', countryCode: 'CA', lastName: 'Doofenschmirtz' } -const PRODUCT_LIST_ITEM: ShopperCustomersTypes.CustomerProductListItem = { +const newAddress: ShopperCustomersTypes.CustomerAddress = { + addressId: 'address', + countryCode: 'US', + lastName: 'Doofenschmirtz' +} +const oldProductListItem: ShopperCustomersTypes.CustomerProductListItem = { id: 'productListItemId', priority: 0, public: false, quantity: 0 } -const PAYMENT_INSTRUMENT: ShopperCustomersTypes.CustomerPaymentInstrument = { +const newProductListItem: ShopperCustomersTypes.CustomerProductListItem = { + id: 'productListItemId', + priority: 1, + public: false, + quantity: 1 +} +const oldPaymentInstrument: ShopperCustomersTypes.CustomerPaymentInstrument = { paymentBankAccount: {}, paymentCard: {cardType: 'fake'}, paymentInstrumentId: 'paymentInstrumentId', paymentMethodId: 'paymentMethodId' } -/** All Shopper Customers parameters. Can be used for all endpoints, as unused params are ignored. */ -const PARAMETERS = { - addressName: 'addressName', +const newPaymentInstrument: ShopperCustomersTypes.CustomerPaymentInstrument = { + paymentBankAccount: {}, + paymentCard: {cardType: 'different'}, + paymentInstrumentId: 'paymentInstrumentId', + paymentMethodId: 'paymentMethodId' +} +const baseCustomer: RequireKeys< + ShopperCustomersTypes.Customer, + 'addresses' | 'paymentInstruments' +> = { customerId: 'customerId', - itemId: 'itemId', - listId: 'listId', - paymentInstrumentId: 'paymentInstrumentId' + addresses: [oldAddress], + paymentInstruments: [oldPaymentInstrument] +} +const baseProductList: RequireKeys< + ShopperCustomersTypes.CustomerProductList, + 'customerProductListItems' +> = { + id: 'productListId', + customerProductListItems: [] } + const createOptions = ( body: Argument extends {body: infer B} ? B : undefined -): Argument => ({body, parameters: PARAMETERS}) +): Argument => ({...queryOptions, body}) // --- TEST CASES --- // -// This is an object rather than an array to more easily ensure we cover all mutations -// TODO: Remove optional flag when all mutations are implemented -type TestMap = {[Mut in ShopperCustomersMutation]?: [Argument, DataType]} -const testMap: TestMap = { - // Invalidate customer, update created item endpoint - createCustomerAddress: [createOptions<'createCustomerAddress'>(ADDRESS), ADDRESS], - // Invalidate customer, update created item endpoint - createCustomerPaymentInstrument: [ - createOptions<'createCustomerPaymentInstrument'>({ - bankRoutingNumber: 'bank', - giftCertificateCode: 'code', - // Type assertion so we don't need the full type - paymentCard: {} as ShopperCustomersTypes.CustomerPaymentCardRequest, - paymentMethodId: 'paymentMethodId' - }), - PAYMENT_INSTRUMENT - ], +const testMap = { // Invalidate product listS, update created item endpoint createCustomerProductList: [ createOptions<'createCustomerProductList'>({id: 'productListId'}), @@ -80,13 +102,8 @@ const testMap: TestMap = { ], // Invalidate product listS, update PL, update created item endpoint createCustomerProductListItem: [ - createOptions<'createCustomerProductListItem'>(PRODUCT_LIST_ITEM), - PRODUCT_LIST_ITEM - ], - // Invalidate customer, remove - deleteCustomerPaymentInstrument: [ - createOptions<'deleteCustomerPaymentInstrument'>(undefined), - undefined + createOptions<'createCustomerProductListItem'>(oldProductListItem), + oldProductListItem ], // Invalidate product listS? invalidate PL, remove deleteCustomerProductListItem: [ @@ -103,8 +120,6 @@ const testMap: TestMap = { createOptions<'registerCustomer'>({customer: {}, password: 'hunter2'}), {customerId: 'customerId'} ], - // Invalidate customer, remove - removeCustomerAddress: [createOptions<'removeCustomerAddress'>(undefined), undefined], // noop resetPassword: [ createOptions<'resetPassword'>({ @@ -114,26 +129,12 @@ const testMap: TestMap = { }), undefined ], - updateCustomer: [createOptions<'updateCustomer'>({}), {customerId: 'customerId'}], - // Update customer, invalidate * - updateCustomerAddress: [ - createOptions<'updateCustomerAddress'>({ - addressId: 'addressId', - countryCode: 'CA', - lastName: 'Doofenschmirtz' - }), - ADDRESS - ], // invalidate PLs? invalidate PL, update PLI updateCustomerProductListItem: [ createOptions<'updateCustomerProductListItem'>({priority: 0, public: false, quantity: 0}), - PRODUCT_LIST_ITEM + oldProductListItem ] } -// Type assertion is necessary because `Object.entries` is limited :\ -const allTestCases = Object.entries(testMap) as Array< - [ShopperCustomersMutation, NonNullable] -> // Not implemented checks are temporary to make sure we don't forget to add tests when adding // implentations. When all mutations are added, the "not implemented" tests can be removed, @@ -150,32 +151,201 @@ const notImplTestCases: ShopperCustomersMutation[][] = [ describe('ShopperCustomers mutations', () => { beforeEach(() => nock.cleanAll()) - test.each(allTestCases)( - '`%s` returns data on success', - async (mutationName, [options, data]) => { - mockMutationEndpoints(customersEndpoint, data ?? {}) // Fallback for `void` endpoints - const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { - return useShopperCustomersMutation(mutationName) + test.each(notImplTestCases)('`%s` is not yet implemented', async (mutationName) => { + expect(() => useShopperCustomersMutation(mutationName)).toThrow(NotImplementedError) + }) + describe('modify a customer', () => { + test('`createCustomerAddress` updates cache on success', async () => { + const customer: ShopperCustomersTypes.Customer = {...baseCustomer, addresses: []} + const options = createOptions<'createCustomerAddress'>(oldAddress) + const data: DataType = oldAddress + mockQueryEndpoint(customersEndpoint, customer) // getCustomer + mockMutationEndpoints(customersEndpoint, data) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch + const {result, rerender, waitForValueToChange} = renderHookWithProviders( + (props: {enabled: boolean} = {enabled: false}) => ({ + customer: queries.useCustomer(queryOptions), + mutation: useShopperCustomersMutation('createCustomerAddress'), + // Initially disabled; not needed until after the creation mutation + query: queries.useCustomerAddress(queryOptions, props) + }) + ) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.customer) + expect(result.current.customer.data).toEqual(customer) + expect(result.current.query.data).toBeUndefined() + + // 2. Do creation mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toEqual(data) + assertInvalidateQuery(result.current.customer, customer) + + // 3. Re-render to validate the created resource was added to cache + await rerender({enabled: true}) + await waitForValueToChange(() => result.current.query) + assertUpdateQuery(result.current.query, data) + }) + test('`createCustomerPaymentInstrument` updates cache on success', async () => { + // 0. Setup + const customer: ShopperCustomersTypes.Customer = { + ...baseCustomer, + paymentInstruments: [] + } + const options = createOptions<'createCustomerPaymentInstrument'>({ + bankRoutingNumber: 'bank', + giftCertificateCode: 'code', + // Type assertion so we don't need the full type + paymentCard: {} as ShopperCustomersTypes.CustomerPaymentCardRequest, + paymentMethodId: 'paymentMethodId' }) - expect(result.current.data).toBeUndefined() - act(() => result.current.mutate(options)) - await waitAndExpectSuccess(wait, () => result.current) - expect(result.current.data).toEqual(data) - } - ) - test.each(allTestCases)('`%s` returns error on error', async (mutationName, [options]) => { - mockMutationEndpoints(customersEndpoint, {error: true}, 400) - const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { - return useShopperCustomersMutation(mutationName) + const data: DataType = oldPaymentInstrument + mockQueryEndpoint(customersEndpoint, customer) // getCustomer + mockMutationEndpoints(customersEndpoint, data) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch + const {result, rerender, waitForValueToChange} = renderHookWithProviders( + (props: {enabled: boolean} = {enabled: false}) => ({ + customer: queries.useCustomer(queryOptions), + mutation: useShopperCustomersMutation('createCustomerPaymentInstrument'), + // Initially disabled; we don't want to trigger it until after the creation mutation + query: queries.useCustomerPaymentInstrument(queryOptions, props) + }) + ) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.customer) + expect(result.current.customer.data).toEqual(customer) + expect(result.current.query.data).toBeUndefined() + + // 2. Do creation mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toEqual(data) + assertInvalidateQuery(result.current.customer, customer) + + // 3. Re-render to validate the created resource was added to cache + await rerender({enabled: true}) + await waitForValueToChange(() => result.current.query) + assertUpdateQuery(result.current.query, data) + }) + test('`deleteCustomerPaymentInstrument` updates cache on success', async () => { + // 0. Setup + const customer = baseCustomer + const options = queryOptions // can be used for this mutation as it has no body + const queryData = oldPaymentInstrument + mockQueryEndpoint(customersEndpoint, customer) // getCustomer + mockQueryEndpoint(customersEndpoint, queryData) // query + mockMutationEndpoints(customersEndpoint, {}) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + customer: queries.useCustomer(queryOptions), + mutation: useShopperCustomersMutation('deleteCustomerPaymentInstrument'), + query: queries.useCustomerPaymentInstrument(queryOptions) + })) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.customer) + expect(result.current.customer.data).toEqual(customer) + expect(result.current.query.data).toEqual(queryData) + + // 2. Do deletion mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toBeUndefined() + assertInvalidateQuery(result.current.customer, customer) + assertRemoveQuery(result.current.query) + }) + test('`removeCustomerAddress` updates cache on success', async () => { + // 0. Setup + const customer = baseCustomer + const options = queryOptions // can be used for this mutation as it has no body + const queryData = oldAddress + mockQueryEndpoint(customersEndpoint, customer) // getCustomer + mockQueryEndpoint(customersEndpoint, queryData) // query + mockMutationEndpoints(customersEndpoint, {}) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + customer: queries.useCustomer(queryOptions), + mutation: useShopperCustomersMutation('removeCustomerAddress'), + query: queries.useCustomerAddress(queryOptions) + })) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.customer) + expect(result.current.customer.data).toEqual(customer) + expect(result.current.query.data).toEqual(queryData) + + // 2. Do deletion mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toBeUndefined() + assertInvalidateQuery(result.current.customer, customer) + assertRemoveQuery(result.current.query) + }) + test('`updateCustomer` updates cache on success', async () => { + // 0. Setup + const oldCustomer = baseCustomer + const body: ShopperCustomersTypes.Customer = {email: 'new@email'} + const newCustomer = {...oldCustomer, ...body} + const options = {body, parameters: PARAMETERS} + mockQueryEndpoint(customersEndpoint, oldCustomer) // getCustomer + mockMutationEndpoints(customersEndpoint, newCustomer) // this mutation + mockQueryEndpoint(customersEndpoint, oldAddress) // query + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // query refetch + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + customer: queries.useCustomer(queryOptions), + mutation: useShopperCustomersMutation('updateCustomer'), + query: queries.useCustomerAddress(queryOptions) + })) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.customer) + expect(result.current.customer.data).toEqual(oldCustomer) + + // 2. Do update mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toEqual(newCustomer) + // `updateCustomer` invalidates all customer endpoints, which one we check isn't important + // assertInvalidateQuery(result.current.query, oldAddress) + // expect(result.current.customer.data).toEqual(newCustomer) + assertUpdateQuery(result.current.customer, newCustomer) + }) + test('`updateCustomerAddress` updates cache on success', async () => { + const customer = baseCustomer + const options = createOptions<'updateCustomerAddress'>(newAddress) + const oldData = oldAddress + const newData = newAddress + mockQueryEndpoint(customersEndpoint, customer) // getCustomer + mockQueryEndpoint(customersEndpoint, oldData) // getCustomerAddress + mockMutationEndpoints(customersEndpoint, newData) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomerAddress refetch + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + customer: queries.useCustomer(queryOptions), + mutation: useShopperCustomersMutation('updateCustomerAddress'), + query: queries.useCustomerAddress(queryOptions) + })) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.customer) + expect(result.current.customer.data).toEqual(customer) + expect(result.current.query.data).toEqual(oldData) + + // 2. Do mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toEqual(newData) + assertInvalidateQuery(result.current.customer, customer) + assertUpdateQuery(result.current.query, newData) }) - expect(result.current.error).toBeNull() - act(() => result.current.mutate(options)) - await waitAndExpectError(wait, () => result.current) - // Validate that we get a `ResponseError` from commerce-sdk-isomorphic. Ideally, we could do - // `.toBeInstanceOf(ResponseError)`, but the class isn't exported. :\ - expect(result.current.error).toHaveProperty('response') }) - test.each(notImplTestCases)('`%s` is not yet implemented', async (mutationName) => { - expect(() => useShopperCustomersMutation(mutationName)).toThrow(NotImplementedError) + describe('modify a customer product list', () => { + // TODO + }) + describe('noops', () => { + // TODO }) }) From 504773b4a42b3a6e49e88f6d8c2d954e20d30b1a Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 28 Feb 2023 12:52:49 -0500 Subject: [PATCH 112/122] Implement full Shopper Customers mutation tests. --- .../src/hooks/ShopperCustomers/cache.ts | 15 +- .../hooks/ShopperCustomers/mutation.test.ts | 296 +++++++++++++----- 2 files changed, 227 insertions(+), 84 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts index b30e275ec7..069812339b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts @@ -68,7 +68,8 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { // We always invalidate, because even without an ID we assume that something has changed // TODO: Rather than invalidate, can we selectively update? const invalidate: CacheUpdate['invalidate'] = [ - {queryKey: getCustomerProductList.queryKey(parameters)} + {queryKey: getCustomerProductList.queryKey(parameters)}, + {queryKey: getCustomerProductLists.queryKey(parameters)} ] // We can only update cache for this product list item if we have the ID const itemId = response.id @@ -89,7 +90,10 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { deleteCustomerProductListItem(customerId, {parameters}) { return { // TODO: Rather than invalidate, can we selectively update? - invalidate: [{queryKey: getCustomerProductList.queryKey(parameters)}], + invalidate: [ + {queryKey: getCustomerProductList.queryKey(parameters)}, + {queryKey: getCustomerProductLists.queryKey(parameters)} + ], remove: [{queryKey: getCustomerProductListItem.queryKey(parameters)}] } }, @@ -133,9 +137,12 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { updateCustomerProductList: TODO('updateCustomerProductList'), updateCustomerProductListItem(customerId, {parameters}) { return { - update: [{queryKey: getCustomerProductList.queryKey(parameters)}], + update: [{queryKey: getCustomerProductListItem.queryKey(parameters)}], // TODO: Rather than invalidate, can we selectively update? - invalidate: [{queryKey: getCustomerProductListItem.queryKey(parameters)}] + invalidate: [ + {queryKey: getCustomerProductList.queryKey(parameters)}, + {queryKey: getCustomerProductLists.queryKey(parameters)} + ] } } } diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts index 79f90545f7..a4d77bc384 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts @@ -36,7 +36,7 @@ const PARAMETERS = { itemId: 'itemId', listId: 'listId', paymentInstrumentId: 'paymentInstrumentId' -} +} as const /** Options object that can be used for all query endpoints. */ const queryOptions = {parameters: PARAMETERS} const oldAddress: ShopperCustomersTypes.CustomerAddress = { @@ -44,98 +44,45 @@ const oldAddress: ShopperCustomersTypes.CustomerAddress = { countryCode: 'CA', lastName: 'Doofenschmirtz' } -const newAddress: ShopperCustomersTypes.CustomerAddress = { - addressId: 'address', - countryCode: 'US', - lastName: 'Doofenschmirtz' -} const oldProductListItem: ShopperCustomersTypes.CustomerProductListItem = { - id: 'productListItemId', + id: 'itemId', // MUST match parameters priority: 0, public: false, quantity: 0 } -const newProductListItem: ShopperCustomersTypes.CustomerProductListItem = { - id: 'productListItemId', - priority: 1, - public: false, - quantity: 1 -} -const oldPaymentInstrument: ShopperCustomersTypes.CustomerPaymentInstrument = { +const basePaymentInstrument: ShopperCustomersTypes.CustomerPaymentInstrument = { paymentBankAccount: {}, paymentCard: {cardType: 'fake'}, paymentInstrumentId: 'paymentInstrumentId', paymentMethodId: 'paymentMethodId' } -const newPaymentInstrument: ShopperCustomersTypes.CustomerPaymentInstrument = { - paymentBankAccount: {}, - paymentCard: {cardType: 'different'}, - paymentInstrumentId: 'paymentInstrumentId', - paymentMethodId: 'paymentMethodId' -} const baseCustomer: RequireKeys< ShopperCustomersTypes.Customer, 'addresses' | 'paymentInstruments' > = { customerId: 'customerId', addresses: [oldAddress], - paymentInstruments: [oldPaymentInstrument] + paymentInstruments: [basePaymentInstrument] } -const baseProductList: RequireKeys< - ShopperCustomersTypes.CustomerProductList, - 'customerProductListItems' -> = { - id: 'productListId', - customerProductListItems: [] +const baseProductList = { + id: 'listId', // MUST match parameters used + customerProductListItems: [oldProductListItem] +} +const emptyListResult: ShopperCustomersTypes.CustomerProductListResult = { + data: [], + limit: 0, + total: 0 +} +const baseListResult: ShopperCustomersTypes.CustomerProductListResult = { + data: [baseProductList], + limit: 1, + total: 1 } const createOptions = ( body: Argument extends {body: infer B} ? B : undefined ): Argument => ({...queryOptions, body}) -// --- TEST CASES --- // -const testMap = { - // Invalidate product listS, update created item endpoint - createCustomerProductList: [ - createOptions<'createCustomerProductList'>({id: 'productListId'}), - {id: 'productListId'} - ], - // Invalidate product listS, update PL, update created item endpoint - createCustomerProductListItem: [ - createOptions<'createCustomerProductListItem'>(oldProductListItem), - oldProductListItem - ], - // Invalidate product listS? invalidate PL, remove - deleteCustomerProductListItem: [ - createOptions<'deleteCustomerProductListItem'>(undefined), - undefined - ], - // noop - getResetPasswordToken: [ - createOptions<'getResetPasswordToken'>({login: 'login'}), - {email: 'customer@email', expiresInMinutes: 10, login: 'login', resetToken: 'token'} - ], - // noop - registerCustomer: [ - createOptions<'registerCustomer'>({customer: {}, password: 'hunter2'}), - {customerId: 'customerId'} - ], - // noop - resetPassword: [ - createOptions<'resetPassword'>({ - resetToken: 'token', - newPassword: 'hunter3', - login: 'login' - }), - undefined - ], - // invalidate PLs? invalidate PL, update PLI - updateCustomerProductListItem: [ - createOptions<'updateCustomerProductListItem'>({priority: 0, public: false, quantity: 0}), - oldProductListItem - ] -} - // Not implemented checks are temporary to make sure we don't forget to add tests when adding // implentations. When all mutations are added, the "not implemented" tests can be removed, // and the `TestMap` type can be changed from optional keys to required keys. Doing so will @@ -200,7 +147,7 @@ describe('ShopperCustomers mutations', () => { paymentCard: {} as ShopperCustomersTypes.CustomerPaymentCardRequest, paymentMethodId: 'paymentMethodId' }) - const data: DataType = oldPaymentInstrument + const data: DataType = basePaymentInstrument mockQueryEndpoint(customersEndpoint, customer) // getCustomer mockMutationEndpoints(customersEndpoint, data) // this mutation mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch @@ -232,10 +179,10 @@ describe('ShopperCustomers mutations', () => { test('`deleteCustomerPaymentInstrument` updates cache on success', async () => { // 0. Setup const customer = baseCustomer - const options = queryOptions // can be used for this mutation as it has no body - const queryData = oldPaymentInstrument + const options = queryOptions // Can be used for this mutation as it has no body + const data = basePaymentInstrument mockQueryEndpoint(customersEndpoint, customer) // getCustomer - mockQueryEndpoint(customersEndpoint, queryData) // query + mockQueryEndpoint(customersEndpoint, data) // query mockMutationEndpoints(customersEndpoint, {}) // this mutation mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomer refetch const {result, waitForValueToChange} = renderHookWithProviders(() => ({ @@ -247,7 +194,7 @@ describe('ShopperCustomers mutations', () => { // 1. Populate cache with initial data await waitAndExpectSuccess(waitForValueToChange, () => result.current.customer) expect(result.current.customer.data).toEqual(customer) - expect(result.current.query.data).toEqual(queryData) + expect(result.current.query.data).toEqual(data) // 2. Do deletion mutation act(() => result.current.mutation.mutate(options)) @@ -315,9 +262,13 @@ describe('ShopperCustomers mutations', () => { }) test('`updateCustomerAddress` updates cache on success', async () => { const customer = baseCustomer - const options = createOptions<'updateCustomerAddress'>(newAddress) const oldData = oldAddress - const newData = newAddress + const newData: ShopperCustomersTypes.CustomerAddress = { + addressId: 'address', + countryCode: 'US', + lastName: 'Doofenschmirtz' + } + const options = createOptions<'updateCustomerAddress'>(newData) mockQueryEndpoint(customersEndpoint, customer) // getCustomer mockQueryEndpoint(customersEndpoint, oldData) // getCustomerAddress mockMutationEndpoints(customersEndpoint, newData) // this mutation @@ -343,9 +294,194 @@ describe('ShopperCustomers mutations', () => { }) }) describe('modify a customer product list', () => { - // TODO + test('`createCustomerProductList` updates cache on success', async () => { + const listResult = emptyListResult + const data: ShopperCustomersTypes.CustomerProductList = { + id: 'listId', // MUST match parameters used + customerProductListItems: [] + } + const options = createOptions<'createCustomerProductList'>(data) + mockQueryEndpoint(customersEndpoint, listResult) // getCustomerProductLists + mockMutationEndpoints(customersEndpoint, data) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomerProductList refetch + const {result, rerender, waitForValueToChange} = renderHookWithProviders( + (props: {enabled: boolean} = {enabled: false}) => ({ + lists: queries.useCustomerProductLists(queryOptions), + mutation: useShopperCustomersMutation('createCustomerProductList'), + // Initially disabled; not needed until after the creation mutation + query: queries.useCustomerProductList(queryOptions, props) + }) + ) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.lists) + expect(result.current.lists.data).toEqual(listResult) + expect(result.current.query.data).toBeUndefined() + + // 2. Do creation mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toEqual(data) + assertInvalidateQuery(result.current.lists, listResult) + + // 3. Re-render to validate the created resource was added to cache + await rerender({enabled: true}) + await waitForValueToChange(() => result.current.query) + assertUpdateQuery(result.current.query, data) + }) + test('`createCustomerProductListItem` updates cache on success', async () => { + const data = oldProductListItem + const list = emptyListResult + const listResult = emptyListResult + const options = createOptions<'createCustomerProductListItem'>(data) + mockQueryEndpoint(customersEndpoint, list) // getCustomerProductList + mockQueryEndpoint(customersEndpoint, listResult) // getCustomerProductLists + mockMutationEndpoints(customersEndpoint, data) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomerProductList refetch + mockQueryEndpoint(customersEndpoint, {test: 'use this? should not be!'}) // getCustomerProductLists refetch + const {result, rerender, waitForValueToChange} = renderHookWithProviders( + (props: {enabled: boolean} = {enabled: false}) => ({ + list: queries.useCustomerProductList(queryOptions), + lists: queries.useCustomerProductLists(queryOptions), + mutation: useShopperCustomersMutation('createCustomerProductListItem'), + // Initially disabled; not needed until after the creation mutation + query: queries.useCustomerProductListItem(queryOptions, props) + }) + ) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.lists) + expect(result.current.list.data).toEqual(list) + expect(result.current.lists.data).toEqual(listResult) + expect(result.current.query.data).toBeUndefined() + + // 2. Do creation mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toEqual(data) + assertInvalidateQuery(result.current.list, list) + assertInvalidateQuery(result.current.lists, listResult) + + // 3. Re-render to validate the created resource was added to cache + await rerender({enabled: true}) + await waitForValueToChange(() => result.current.query) + assertUpdateQuery(result.current.query, data) + }) + test.only('`deleteCustomerProductListItem` updates cache on success', async () => { + const data = oldProductListItem + const list = baseProductList + const listResult = baseListResult + const options = queryOptions // Can be used for this mutation as it has no body + mockQueryEndpoint(customersEndpoint, list) // getCustomerProductList + mockQueryEndpoint(customersEndpoint, listResult) // getCustomerProductLists + mockQueryEndpoint(customersEndpoint, data) // getCustomerProductListItem + mockMutationEndpoints(customersEndpoint, {}) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomerProductList refetch + mockQueryEndpoint(customersEndpoint, {test: 'use this? should not be!'}) // getCustomerProductLists refetch + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + list: queries.useCustomerProductList(queryOptions), + lists: queries.useCustomerProductLists(queryOptions), + mutation: useShopperCustomersMutation('deleteCustomerProductListItem'), + query: queries.useCustomerProductListItem(queryOptions) + })) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.lists) + expect(result.current.list.data).toEqual(list) + expect(result.current.lists.data).toEqual(listResult) + expect(result.current.query.data).toEqual(data) + + // 2. Do deletion mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toBeUndefined() + assertInvalidateQuery(result.current.list, list) + assertInvalidateQuery(result.current.lists, listResult) + assertRemoveQuery(result.current.query) + }) + test('`updateCustomerProductListItem` updates cache on success', async () => { + const oldData = oldProductListItem + const newData: ShopperCustomersTypes.CustomerProductListItem = { + id: 'itemId', // MUST match parameters + priority: 1, + public: false, + quantity: 1 + } + const list = baseProductList + const listResult = baseListResult + const options = createOptions<'updateCustomerProductListItem'>(newData) + mockQueryEndpoint(customersEndpoint, list) // getCustomerProductList + mockQueryEndpoint(customersEndpoint, listResult) // getCustomerProductLists + mockQueryEndpoint(customersEndpoint, oldData) // getCustomerProductListItem + mockMutationEndpoints(customersEndpoint, newData) // this mutation + mockQueryEndpoint(customersEndpoint, {test: 'this should not get used'}) // getCustomerProductList refetch + mockQueryEndpoint(customersEndpoint, {test: 'use this? should not be!'}) // getCustomerProductLists refetch + const {result, waitForValueToChange} = renderHookWithProviders(() => ({ + list: queries.useCustomerProductList(queryOptions), + lists: queries.useCustomerProductLists(queryOptions), + mutation: useShopperCustomersMutation('updateCustomerProductListItem'), + query: queries.useCustomerProductListItem(queryOptions) + })) + + // 1. Populate cache with initial data + await waitAndExpectSuccess(waitForValueToChange, () => result.current.lists) + expect(result.current.list.data).toEqual(list) + expect(result.current.lists.data).toEqual(listResult) + expect(result.current.query.data).toEqual(oldData) + + // 2. Do update mutation + act(() => result.current.mutation.mutate(options)) + await waitAndExpectSuccess(waitForValueToChange, () => result.current.mutation) + expect(result.current.mutation.data).toEqual(newData) + assertInvalidateQuery(result.current.list, list) + assertInvalidateQuery(result.current.lists, listResult) + assertUpdateQuery(result.current.query, newData) + }) }) - describe('noops', () => { - // TODO + describe('simple mutations (no cache updates)', () => { + /** `void` doesn't play nice as a value, so just replace it with `undefined`. */ + type FixVoid = void extends T ? undefined : T + type TestMap = { + [Mut in ShopperCustomersMutation]?: [ + Argument, + FixVoid> + ] + } + // Using an object, rather than array, to more easily manage data values + const testMap: TestMap = { + getResetPasswordToken: [ + createOptions<'getResetPasswordToken'>({login: 'login'}), + {email: 'customer@email', expiresInMinutes: 10, login: 'login', resetToken: 'token'} + ], + registerCustomer: [ + createOptions<'registerCustomer'>({customer: {}, password: 'hunter2'}), + {customerId: 'customerId'} + ], + resetPassword: [ + createOptions<'resetPassword'>({ + resetToken: 'token', + newPassword: 'hunter3', + login: 'login' + }), + undefined + ] + } + // Type assertion because `Object.entries` is limited :\ + const testCases = Object.entries(testMap) as [ + ShopperCustomersMutation, + NonNullable + ][] + test.each(testCases)( + '`%s` returns data on success', + async (mutationName, [options, data]) => { + mockMutationEndpoints(customersEndpoint, data ?? {}) // Fallback for `void` endpoints + const {result, waitForValueToChange: wait} = renderHookWithProviders(() => { + return useShopperCustomersMutation(mutationName) + }) + act(() => result.current.mutate(options)) + await waitAndExpectSuccess(wait, () => result.current) + expect(result.current.data).toEqual(data) + } + ) }) }) From 63f35c8ffb230e02006ab5635ae861ecfa8ff773 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 28 Feb 2023 13:08:03 -0500 Subject: [PATCH 113/122] Rename `StringIndexNever` to `StringIndexToNever` --- packages/commerce-sdk-react/src/hooks/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index b68618106d..9a470d9b26 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -35,12 +35,12 @@ type RemoveNeverValues = { } /** Change string index type to `never`. */ -type StringIndexNever = { +type StringIndexToNever = { [K in keyof T]: string extends K ? never : T[K] } /** Removes a string index type. */ -export type RemoveStringIndex = RemoveNeverValues> +export type RemoveStringIndex = RemoveNeverValues> /** Gets the last element of an array. */ export type Tail = T extends [...head: unknown[], tail: infer Tail] From 254b43ee96dd91a5dfb351e4130eecdfda948490 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 28 Feb 2023 15:27:53 -0500 Subject: [PATCH 114/122] Remove unnecessary invalidation. The query is already updated, so doesn't need to be invalidated. --- .../commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts index aeddf9165c..beaaf91334 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/cache.ts @@ -124,8 +124,7 @@ const updateBasket = ( response: Basket ): CacheUpdate => ({ // TODO: We only update the basket from the matching locale; we should also invalidate other locales - ...updateBasketQuery(customerId, parameters, response), - ...invalidateCustomerBasketsQuery(customerId, parameters) + ...updateBasketQuery(customerId, parameters, response) }) const updateBasketWithResponseBasketId = ( @@ -136,8 +135,7 @@ const updateBasketWithResponseBasketId = ( const {basketId} = response return { // TODO: We only update the basket from the matching locale; we should also invalidate other locales - ...(basketId && updateBasketQuery(customerId, {...parameters, basketId}, response)), - ...invalidateCustomerBasketsQuery(customerId, parameters) + ...(basketId && updateBasketQuery(customerId, {...parameters, basketId}, response)) } } From 081ea67c875ed185cdb93a3ac7b0300eff23308c Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 28 Feb 2023 15:43:57 -0500 Subject: [PATCH 115/122] Update generated comments for hooks. --- .../src/hooks/ShopperBaskets/mutation.ts | 346 ++++++++++++++++++ .../src/hooks/ShopperBaskets/query.ts | 40 +- .../src/hooks/ShopperContexts/mutation.ts | 23 ++ .../src/hooks/ShopperContexts/query.ts | 8 +- .../src/hooks/ShopperCustomers/mutation.ts | 229 ++++++++++++ .../src/hooks/ShopperCustomers/query.ts | 96 ++--- .../src/hooks/ShopperExperience/query.ts | 16 +- .../hooks/ShopperGiftCertificates/query.ts | 8 +- .../src/hooks/ShopperLogin/mutation.ts | 127 +++++++ .../src/hooks/ShopperLogin/query.ts | 32 +- .../src/hooks/ShopperOrders/mutation.ts | 41 +++ .../src/hooks/ShopperOrders/query.ts | 24 +- .../src/hooks/ShopperProducts/query.ts | 32 +- .../src/hooks/ShopperPromotions/query.ts | 16 +- .../src/hooks/ShopperSearch/query.ts | 16 +- 15 files changed, 910 insertions(+), 144 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts index 0e44a4e7e1..45b897497f 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/mutation.ts @@ -13,35 +13,381 @@ import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperBaskets'] +/** Mutations available for Shopper Baskets. */ export const ShopperBasketsMutations = { + /** + * Creates a new basket. + +The created basket is initialized with default values. Data provided in the body document is populated into the created basket. It can be updated with API endpoints listed below. + +The taxMode query parameter can be used to choose the basket tax mode. The default is internal, in which case the tax calculation is done automatically based on internal tax tables. Alternatively, external taxation mode can be set which allows manual modification of the tax rates and values. External tax data is mandatory for product line items, option line items, shipping line items, coupon line items, and bonus discount line item. Gift certificate line items are optional and use zero tax rate per default, which can be overwritten. Price adjustments cannot be set because they are either calculated or inherited (depending on the type, the tax rate is either obtained from the related line item or computed as prorate of the basket). + +API endpoints allowing further basket modification: + +- customer information: PUT /baskets/\{basketId\}/customer + +- billing address: PUT /baskets/\{basketId\}/billing-address + +- shipments including shipping address and shipping method: POST /baskets/\{basketId\}/shipments + +- product items: POST /baskets/\{basketId\}/items + +- coupon items: POST /baskets/\{basketId\}/coupons + +- gift certificate items: POST /baskets/\{basketId\}/gift-certificates + +- basket taxation: PUT /baskets/\{basketId\}/taxes + +- basket item taxation: PUT /baskets/\{basketId\}/items/\{itemId\}/taxes + +- payment method and card type: POST /baskets/\{basketId\}/payment-instruments + +- custom properties: PATCH /baskets/\{basketId\} + +Related resource means with which resource you can specify the same data after the basket creation. +Identify the basket using the basketId property, which +should be integrated into the path of an update request (for example a POST to +/baskets/\{basketId\}/items). + +A customer must provide a JSON Web Token (JWT), which specifies exactly one customer (it can be a guest or a registered +customer). In this case, the resource creates a basket for this customer. + +The number of baskets which can be created per customer is limited. When a +basket is created, it is said to be open. It remains open until either an order is created from it +using a POST to resource /orders, or it is deleted using a DELETE to resource +/baskets/\{basketId\}. Each customer can have just one open basket. + +Custom properties in the form c_\ are supported. A custom property must correspond to a custom +attribute (\) defined for the basket system object, and its value must be valid for that custom +attribute. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `createBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=createBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#createbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateBasket: 'createBasket', + /** + * Transfer the previous shopper's basket to the current shopper by updating the basket's owner. No other values change. You must obtain the shopper authorization token via SLAS and you must provide the ‘guest usid‘ in both the ‘/oauth2/login‘ and ‘/oauth2/token‘ calls while fetching the registered user JWT token. + +A success response contains the transferred basket. + +If the current shopper has an active basket, and the `overrideExisting` request parameter is `false`, then the transfer request returns a BasketTransferException (HTTP status 409). You can proceed with one of these options: +- Keep the current shopper's active basket. +- Merge the previous and current shoppers' baskets by calling the `baskets/merge` endpoint. +- Force the transfer by calling the `baskets/transfer` endpoint again, with the parameter `overrideExisting=true`. Forcing the transfer deletes the current shopper's active basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `transferBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=transferBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#transferbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ TransferBasket: 'transferBasket', + /** + * Merge data from the previous shopper's basket into the current shopper's active basket and delete the previous shopper's basket. This endpoint doesn't merge Personally Identifiable Information (PII). You must obtain the shopper authorization token via SLAS and you must provide the ‘guest usid‘ in both the ‘/oauth2/login‘ and ‘/oauth2/token‘ calls while fetching the registered user JWT token. After the merge, all basket amounts are recalculated and totaled, including lookups for prices, taxes, shipping, and promotions. + +The following information is merged: +- custom attributes on the basket and on all copied records +- product items +- gift certificate items +- coupon items +- shipments +- ad-hoc price adjustments + +To control the merging of products that exist in both baskets, use the `productItemMergeMode` parameter. By default, the higher of the two basket quantities is used for each product. Products in both baskets are considered to be the same when all of the following values match (if one product doesn't have a value, the other product is a match only if it also doesn't have that value): +- shipment +- productId +- option values +- wishlist reference +- inventory list id +- gift flag & message +- ad-hoc price adjustments + +If any of the listed values don't match, then the item in the previous shopper's basket is copied to a new line item in the current shopper's basket. If the listed values all match, but the matching products have different values for any custom attribute, the merged line item keeps the custom attribute value from the current shopper's basket. + +A success response contains the current shopper's active basket. The previous guest shopper's active basket is deleted. + +If the current shopper doesn't have an active basket, and the createDestinationBasket request parameter is false, then the merge request returns a BasketMergeException (HTTP status 409). You can proceed with one of these options: +- Transfer the previous shopper's active basket to the current logged-in shopper by calling the `baskets/transfer` endpoint. +- Force the merge by calling the `baskets/merge` endpoint again, with the parameter `createDestinationBasket=true`. Forcing the merge creates a new basket for the current shopper and copies information from the previous shopper's basket into it. Because the merge doesn't copy all basket data, a forced merge is not the same as a transfer. For example, the new basket doesn't contain any Personally Identifiable Information (PII) from the previous basket. + +### before merge +| Previous Shopper's Basket, SKU: Quantity, Custom Attributes | Current Shopper's Basket, SKU: Quantity, Custom Attributes | +|-------------------------------------------------------------|-------------------------------------------------------------| +| SKU_A: 5\ SKU_B: 3\ SKU_C: 4\ c_customAttr_1: 'ABC' \ c_customAttr_2: 'DEF' | SKU_A: 2\ SKU_D: 6\ SKU_E: 7\ c_customAttr_1: 'UVW' \ c_customAttr_3: 'XYZ' | + +### after merge - (previous shopper's basket is deleted) +| productItemMergeMode | Current Shopper's Basket - SKU: Quantity, Custom Attributes | +|----------------------|--------------------------------------------------------------| +| sum_quantities | SKU_A: 7\ SKU_B: 3\ SKU_C: 4\ SKU_D: 6\ SKU_E: 7\ c_customAttr_1: 'UVW' \ c_customAttr_2: 'DEF' \ c_customAttr_3: 'XYZ' | +| higher_quantity | SKU_A: 5\ SKU_B: 3\ SKU_C: 4\ SKU_D: 6\ SKU_E: 7\ c_customAttr_1: 'UVW' \ c_customAttr_2: 'DEF' \ c_customAttr_3: 'XYZ' | +| saved_quantity | SKU_A: 2\ SKU_B: 3\ SKU_C: 4\ SKU_D: 6\ SKU_E: 7\ c_customAttr_1: 'UVW' \ c_customAttr_2: 'DEF' \ c_customAttr_3: 'XYZ' | +| separate_item | SKU_A: 5\ SKU_B: 3\ SKU_C: 4\ SKU_A: 2\ SKU_D: 6\ SKU_E: 7\ c_customAttr_1: 'UVW' \ c_customAttr_2: 'DEF' \ c_customAttr_3: 'XYZ' | + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `mergeBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=mergeBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#mergebasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ MergeBasket: 'mergeBasket', + /** + * Removes a basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `deleteBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=deleteBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#deletebasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ DeleteBasket: 'deleteBasket', + /** + * Updates a basket. Only the currency of the basket, source code, the custom +properties of the basket, and the shipping items will be considered. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatebasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateBasket: 'updateBasket', + /** + * Sets the billing address of a basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateBillingAddressForBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateBillingAddressForBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatebillingaddressforbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateBillingAddressForBasket: 'updateBillingAddressForBasket', + /** + * Adds a coupon to an existing basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `addCouponToBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addCouponToBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addcoupontobasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AddCouponToBasket: 'addCouponToBasket', + /** + * Removes a coupon from the basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `removeCouponFromBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeCouponFromBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removecouponfrombasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RemoveCouponFromBasket: 'removeCouponFromBasket', + /** + * Sets customer information for an existing basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateCustomerForBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateCustomerForBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatecustomerforbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateCustomerForBasket: 'updateCustomerForBasket', + /** + * Adds a gift certificate item to an existing basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `addGiftCertificateItemToBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addGiftCertificateItemToBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addgiftcertificateitemtobasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AddGiftCertificateItemToBasket: 'addGiftCertificateItemToBasket', + /** + * Deletes a gift certificate item from an existing basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `removeGiftCertificateItemFromBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeGiftCertificateItemFromBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removegiftcertificateitemfrombasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RemoveGiftCertificateItemFromBasket: 'removeGiftCertificateItemFromBasket', + /** + * Updates a gift certificate item of an existing basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateGiftCertificateItemInBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateGiftCertificateItemInBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updategiftcertificateiteminbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateGiftCertificateItemInBasket: 'updateGiftCertificateItemInBasket', + /** + * Adds new items to a basket. The added items are associated with the +specified shipment. If no shipment id is specified, the added items are associated with the default shipment. +Considered values from the request body, for each item are: + +- productId: a valid product ID. This is the ID of the product to be added to the basket. If the +product is already in the basket, the API either increments the quantity of the existing product line item or +creates a new product line item, based on the site preference 'Add Product Behavior'. For option products and +product bundles containing variation masters, the API creates a new product line item regardless of the site +preference. +- shipmentId: a valid shipment ID (optional). This is the ID of the shipment in which the product item +is created. +- quantity: a number between 0.01 and 999. This is the quantity of the product to order. +- inventoryId: a valid inventory ID (optional). This is the ID of the inventory from which the item is +allocated. +- bonusDiscountLineItemId: a valid bonus discount line item ID (optional). This is the ID of the +bonus discount line item for which the added product is a selected bonus product. +- optionItems/optionValueId: a valid option value ID. This is an option value for an option item of +an option product. This is only possible if the product item is an option +product. To set option values, you must specify a collection of option items in the optionItems +property. These option items must contain optionId and optionValueId. Also, +the values you specify must be valid for the option product that this product item represents. Otherwise, the +server throws an InvalidProductOptionItemException or an +InvalidProductOptionValueItemException. +- custom properties in the form c_\: the custom property must correspond to a custom +attribute (\) defined for ProductLineItem. The value of this property must be valid for the +type of custom attribute defined for ProductLineItem. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `addItemToBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addItemToBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#additemtobasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AddItemToBasket: 'addItemToBasket', + /** + * Removes a product item from the basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `removeItemFromBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeItemFromBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removeitemfrombasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RemoveItemFromBasket: 'removeItemFromBasket', + /** + * Updates an item in a basket. The +following values in the request body are considered by the server: + +- productId: a valid product ID. The purpose of this +value is to exchange a variation of a variation product. +- shipmentId: a valid shipment ID. The purpose of +this value is to move a product item to another shipment. +- quantity: a number between 0 and 999. The purpose of +this value is to change quantity of the product item. If quantity is 0, +the product item is removed. +- optionItems/optionValueId: a valid option value +ID. The purpose of this value is to exchange an option value for an +option item of an option product. +This is only possible if the product item is an option product. To change +option values a collection of option items to be changed need to be +provided in property optionItems. Those +optionItems need to contain optionId +and optionValueId. The provided values must be valid +for the option product that this product item represents. Otherwise +InvalidProductOptionItemException or +InvalidProductOptionValueItemException will be thrown. +custom properties c_\: a +value corresponding to the type defined for custom attribute +\ of ProductLineItem. The purpose of this value is to +add or change the value of a custom attribute defined for +ProductLineItem. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateItemInBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateItemInBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateiteminbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateItemInBasket: 'updateItemInBasket', + /** + * This method allows you to apply external taxation data to an existing basket to be able to pass tax rates and optional values for a specific taxable line item. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `addTaxesForBasketItem` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addTaxesForBasketItem| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addtaxesforbasketitem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AddTaxesForBasketItem: 'addTaxesForBasketItem', + /** + * Adds a payment instrument to a basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `addPaymentInstrumentToBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addPaymentInstrumentToBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addpaymentinstrumenttobasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AddPaymentInstrumentToBasket: 'addPaymentInstrumentToBasket', + /** + * Removes a payment instrument of a basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `removePaymentInstrumentFromBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removePaymentInstrumentFromBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removepaymentinstrumentfrombasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RemovePaymentInstrumentFromBasket: 'removePaymentInstrumentFromBasket', + /** + * Updates payment instrument of an existing basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updatePaymentInstrumentInBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updatePaymentInstrumentInBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updatepaymentinstrumentinbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdatePaymentInstrumentInBasket: 'updatePaymentInstrumentInBasket', + /** + * This method allows you to put an array of priceBookIds to an existing basket, which will be used for basket calculation. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `addPriceBooksToBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addPriceBooksToBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addpricebookstobasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AddPriceBooksToBasket: 'addPriceBooksToBasket', + /** + * Creates a new shipment for a basket. + +The created shipment is initialized with values provided in the body +document and can be updated with further data API calls. Considered from +the body are the following properties if specified: + +- the ID +- the shipping address +- the shipping method +- gift boolean flag +- gift message +- custom properties + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `createShipmentForBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=createShipmentForBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#createshipmentforbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateShipmentForBasket: 'createShipmentForBasket', + /** + * Removes a specified shipment and all associated product, gift certificate, +shipping, and price adjustment line items from a basket. +It is not allowed to remove the default shipment. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `removeShipmentFromBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=removeShipmentFromBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#removeshipmentfrombasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RemoveShipmentFromBasket: 'removeShipmentFromBasket', + /** + * Updates a shipment for a basket. + +The shipment is initialized with values provided in the body +document and can be updated with further data API calls. Considered from +the body are the following properties if specified: +- the ID +- the shipping address +- the shipping method +- gift boolean flag +- gift message +- custom properties + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateShipmentForBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateShipmentForBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateshipmentforbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateShipmentForBasket: 'updateShipmentForBasket', + /** + * Sets a shipping address of a specific shipment of a basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateShippingAddressForShipment` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateShippingAddressForShipment| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateshippingaddressforshipment | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateShippingAddressForShipment: 'updateShippingAddressForShipment', + /** + * Sets a shipping method to a specific shipment of a basket. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `updateShippingMethodForShipment` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateShippingMethodForShipment| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#updateshippingmethodforshipment | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateShippingMethodForShipment: 'updateShippingMethodForShipment', + /** + * This method allows you to apply external taxation data to an existing basket to be able to pass tax rates and optional values for all taxable line items. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. + * @returns A TanStack Query mutation hook for interacting with the Shopper Baskets `addTaxesForBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=addTaxesForBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#addtaxesforbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AddTaxesForBasket: 'addTaxesForBasket' } as const +/** Mutation for Shopper Baskets. */ export type ShopperBasketsMutation = (typeof ShopperBasketsMutations)[keyof typeof ShopperBasketsMutations] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts index 710a002c4f..5ad254d6cd 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperBaskets'] /** - * A hook for `ShopperBaskets#getBasket`. * Gets a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getbasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Baskets `getBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useBasket = ( apiOptions: Argument, @@ -45,11 +45,11 @@ export const useBasket = ( }) } /** - * A hook for `ShopperBaskets#getPaymentMethodsForBasket`. * Gets applicable payment methods for an existing basket considering the open payment amount only. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPaymentMethodsForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpaymentmethodsforbasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Baskets `getPaymentMethodsForBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPaymentMethodsForBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpaymentmethodsforbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePaymentMethodsForBasket = ( apiOptions: Argument, @@ -76,11 +76,11 @@ export const usePaymentMethodsForBasket = ( }) } /** - * A hook for `ShopperBaskets#getPriceBooksForBasket`. * Gets applicable price books for an existing basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPriceBooksForBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpricebooksforbasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Baskets `getPriceBooksForBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getPriceBooksForBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getpricebooksforbasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePriceBooksForBasket = ( apiOptions: Argument, @@ -107,11 +107,11 @@ export const usePriceBooksForBasket = ( }) } /** - * A hook for `ShopperBaskets#getShippingMethodsForShipment`. * Gets the applicable shipping methods for a certain shipment of a basket. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getShippingMethodsForShipment} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getshippingmethodsforshipment} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Baskets `getShippingMethodsForShipment` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getShippingMethodsForShipment| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#getshippingmethodsforshipment | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useShippingMethodsForShipment = ( apiOptions: Argument, @@ -138,11 +138,11 @@ export const useShippingMethodsForShipment = ( }) } /** - * A hook for `ShopperBaskets#getTaxesFromBasket`. * This method gives you the external taxation data set by the PUT taxes API. This endpoint can be called only if external taxation mode was used for basket creation. See POST /baskets for more information. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getTaxesFromBasket} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#gettaxesfrombasket} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Baskets `getTaxesFromBasket` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=getTaxesFromBasket| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperbaskets.shopperbaskets-1.html#gettaxesfrombasket | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useTaxesFromBasket = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts index fefbff71d4..0c5a30522b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/mutation.ts @@ -13,12 +13,35 @@ import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperContexts'] +/** Mutations available for Shopper Contexts. */ export const ShopperContextsMutations = { + /** + * Creates the shopper's context based on shopperJWT. + * @returns A TanStack Query mutation hook for interacting with the Shopper Contexts `createShopperContext` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=createShopperContext| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#createshoppercontext | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateShopperContext: 'createShopperContext', + /** + * Gets the shopper's context based on the shopperJWT. + * @returns A TanStack Query mutation hook for interacting with the Shopper Contexts `deleteShopperContext` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=deleteShopperContext| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#deleteshoppercontext | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ DeleteShopperContext: 'deleteShopperContext', + /** + * Updates the shopper's context based on the Shopper JWT. If the shopper context exists, it's updated with the patch body. If a customer qualifier or an `effectiveDateTime` is already present in the existing shopper context, its value is replaced by the corresponding value from the patch body. If a customer qualifers' value is set to `null` it's deleted from existing shopper context. If `effectiveDateTime` value is set to set to an empty string (\"\"), it's deleted from existing shopper context. If `effectiveDateTime` value is set to `null` it's ignored. If an `effectiveDateTime` or customer qualifiiers' value is new, it's added to the existing Shopper context. + * @returns A TanStack Query mutation hook for interacting with the Shopper Contexts `updateShopperContext` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=updateShopperContext| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#updateshoppercontext | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateShopperContext: 'updateShopperContext' } as const +/** Mutation for Shopper Contexts. */ export type ShopperContextsMutation = (typeof ShopperContextsMutations)[keyof typeof ShopperContextsMutations] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts index 35bc3eab95..a125569d79 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperContexts'] /** - * A hook for `ShopperContexts#getShopperContext`. * Gets the shopper's context based on the shopperJWT. ******** This API is currently a work in progress, and not available to use yet. ******** - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=getShopperContext} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#getshoppercontext} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Contexts `getShopperContext` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-contexts?meta=getShopperContext| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercontexts.shoppercontexts-1.html#getshoppercontext | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useShopperContext = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts index 699cfe35d5..392028a5b2 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts @@ -13,29 +13,258 @@ import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperCustomers'] +/** Mutations available for Shopper Customers. */ export const ShopperCustomersMutations = { + /** + * Registers a new customer. The mandatory data are the credentials, profile last name, and email. This requires a JSON Web Token (JWT) which needs to be obtained using the POST /customers/auth API with type \"guest\". + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `registerCustomer` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerCustomer| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registercustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RegisterCustomer: 'registerCustomer', + /** + * **DEPRECATION NOTICE** + +To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. + +--- + +Log the user out. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `invalidateCustomerAuth` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=invalidateCustomerAuth| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#invalidatecustomerauth | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ InvalidateCustomerAuth: 'invalidateCustomerAuth', + /** + * **DEPRECATION NOTICE** + +To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. + +--- + +Obtains a new JSON Web Token (JWT)for a guest or registered +customer. Tokens are returned as an HTTP Authorization:Bearer response +header entry. These kinds of request are supported, as specified by the +type: + +Type guest - creates a guest (non-authenticated) customer +and returns a token for the customer. +Request Body for guest : \{\"type\": \"guest\"\} +Type credentials - authenticates credentials passed in the +HTTP Authorization:Basic request header, returning a token for a +successfully authenticated customer, otherwise it throws an +AuthenticationFailedException. +Request Body for guest : \{\"type\": \"credentials\"\} +Type refresh - examines the token passed in the HTTP +Authorization:Bearer request header and when valid returns a new token +with an updated expiry time. +Request Body for guest : \{\"type\": \"refresh\"\} + +For a request of type credentials: + +Updates profile attributes for the customer (for example, +\"last-visited\"). +Handles the maximum number of failed login attempts. + +About JWT The token contains 3 sections: + +The header section (specifies token type and algorithm used), +the payload section (contains customer information, client ID, +issue, and expiration time), +finally the signature section records the token signature. + +A token is created and returned to the client whenever a registered +customer logs in (type \"credentials\") or a guest customer requests it (type +\"guest\"). The token is returned in the response header as +Authorization: Bearer --token-- + +The client has to include the token in the request header as +Authorization: Bearer --token-- +in any follow-up request. The server declines any follow-up requests +without a token or which cannot be verified based on the token signature +or expiration time. A token nearing its expiration time should be +exchanged for a new one (type \"refresh\"). + +See \"API Usage \> JWT\" for more details on using JWT as an authentication +mechanism. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `authorizeCustomer` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeCustomer| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizecustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AuthorizeCustomer: 'authorizeCustomer', + /** + * **DEPRECATION NOTICE** + +To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. + +--- + +Obtain the JSON Web Token (JWT) for registered customers whose credentials are stored using a third party system. Accepts loginId and +clientId, returns a customer object in the response body and the JWT generated against the clientId in the response header. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `authorizeTrustedSystem` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeTrustedSystem| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizetrustedsystem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AuthorizeTrustedSystem: 'authorizeTrustedSystem', + /** + * Reset customer password, after obtaining a reset token. This is the second step in the reset customer password flow, where a customer password is reset by providing the new credentials along with a reset token. This call should be preceded by a call to the /create-reset-token endpoint. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `resetPassword` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=resetPassword| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#resetpassword | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ ResetPassword: 'resetPassword', + /** + * Get reset password token. This is the first step in the reset customer password flow, where a password reset token is requested for future use to reset a customer password. This call should be followed by a call to the /reset endpoint. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `getResetPasswordToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getResetPasswordToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getresetpasswordtoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ GetResetPasswordToken: 'getResetPasswordToken', + /** + * Registers a new external profile for a customer. This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `registerExternalProfile` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerExternalProfile| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registerexternalprofile | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RegisterExternalProfile: 'registerExternalProfile', + /** + * Updates a customer. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `updateCustomer` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomer| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateCustomer: 'updateCustomer', + /** + * Creates a new address with the given name for the given customer. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `createCustomerAddress` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerAddress| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomeraddress | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateCustomerAddress: 'createCustomerAddress', + /** + * Deletes a customer's address by address name. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `removeCustomerAddress` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=removeCustomerAddress| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#removecustomeraddress | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RemoveCustomerAddress: 'removeCustomerAddress', + /** + * Updates a customer's address by address name. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `updateCustomerAddress` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerAddress| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomeraddress | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateCustomerAddress: 'updateCustomerAddress', + /** + * Updates the customer's password. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `updateCustomerPassword` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerPassword| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerpassword | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateCustomerPassword: 'updateCustomerPassword', + /** + * Adds a payment instrument to the customer information. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `createCustomerPaymentInstrument` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerPaymentInstrument| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerpaymentinstrument | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateCustomerPaymentInstrument: 'createCustomerPaymentInstrument', + /** + * Deletes a customer's payment instrument. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `deleteCustomerPaymentInstrument` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerPaymentInstrument| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerpaymentinstrument | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ DeleteCustomerPaymentInstrument: 'deleteCustomerPaymentInstrument', + /** + * Creates a customer product list. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `createCustomerProductList` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductList| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlist | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateCustomerProductList: 'createCustomerProductList', + /** + * Deletes a customer product list. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `deleteCustomerProductList` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductList| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlist | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ DeleteCustomerProductList: 'deleteCustomerProductList', + /** + * Changes a product list. Changeable properties are the name, description, and if the list is public. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `updateCustomerProductList` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductList| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlist | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateCustomerProductList: 'updateCustomerProductList', + /** + * Adds an item to the customer's product list. Considered values from the request body are: + +type: A valid type, mandatory. This is the type of the item to be added to the customer's product. +list. +priority: This is the priority of the item to be added to the customer's product list. +public: This is the flag whether the item to be added to the customer's product list is public. +product_id: A valid product ID, used for product item type only. This is the ID (SKU) +of the product related to the item to be added to the customer's product list. It is mandatory for +product item type, and it must be a valid product id, otherwise +ProductListProductIdMissingException or ProductListProductNotFoundException +will be thrown. +quantity: Used for product item type only. This is the quantity of the item to be +added to the customer's product list. +custom properties in the form c_\: The custom property must correspond to a custom +attribute (\) defined for ProductListItem. The value of this property must be valid for the +type of custom attribute defined for ProductListItem. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `createCustomerProductListItem` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=createCustomerProductListItem| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#createcustomerproductlistitem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateCustomerProductListItem: 'createCustomerProductListItem', + /** + * Removes an item from a customer product list. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `deleteCustomerProductListItem` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=deleteCustomerProductListItem| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#deletecustomerproductlistitem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ DeleteCustomerProductListItem: 'deleteCustomerProductListItem', + /** + * Updates an item of a customer's product list. +Considered values from the request body are: + +priority: This is the priority of the customer's product list item. +public: This is the flag whether the customer's product list item is public. +quantity: This is the quantity of +the customer's product list item. Used for product item type only. +custom properties in the form c_\: The custom property +must correspond to a custom attribute (\) defined for ProductListItem. +The value of this property must be valid for the type of custom attribute defined for ProductListItem. + * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `updateCustomerProductListItem` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=updateCustomerProductListItem| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#updatecustomerproductlistitem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdateCustomerProductListItem: 'updateCustomerProductListItem' } as const +/** Mutation for Shopper Customers. */ export type ShopperCustomersMutation = (typeof ShopperCustomersMutations)[keyof typeof ShopperCustomersMutations] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts index cdabc87ba8..3408de97ec 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperCustomers'] /** - * A hook for `ShopperCustomers#getExternalProfile`. * Gets the new external profile for a customer.This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getExternalProfile} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getexternalprofile} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getExternalProfile` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getExternalProfile| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getexternalprofile | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useExternalProfile = ( apiOptions: Argument, @@ -50,11 +50,11 @@ export const useExternalProfile = ( }) } /** - * A hook for `ShopperCustomers#getCustomer`. * Gets a customer with all existing addresses and payment instruments associated with the requested customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomer} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomer} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomer` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomer| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomer = ( apiOptions: Argument, @@ -81,11 +81,11 @@ export const useCustomer = ( }) } /** - * A hook for `ShopperCustomers#getCustomerAddress`. * Retrieves a customer's address by address name. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerAddress} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomeraddress} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomerAddress` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerAddress| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomeraddress | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomerAddress = ( apiOptions: Argument, @@ -112,11 +112,11 @@ export const useCustomerAddress = ( }) } /** - * A hook for `ShopperCustomers#getCustomerBaskets`. * Gets the baskets of a customer. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerBaskets} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerbaskets} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomerBaskets` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerBaskets| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerbaskets | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomerBaskets = ( apiOptions: Argument, @@ -143,11 +143,11 @@ export const useCustomerBaskets = ( }) } /** - * A hook for `ShopperCustomers#getCustomerOrders`. * Returns a pageable list of all customer's orders. The default page size is 10. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerOrders} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerorders} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomerOrders` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerOrders| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerorders | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomerOrders = ( apiOptions: Argument, @@ -174,11 +174,11 @@ export const useCustomerOrders = ( }) } /** - * A hook for `ShopperCustomers#getCustomerPaymentInstrument`. * Retrieves a customer's payment instrument by its ID. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerPaymentInstrument} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerpaymentinstrument} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomerPaymentInstrument` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerPaymentInstrument| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerpaymentinstrument | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomerPaymentInstrument = ( apiOptions: Argument, @@ -210,11 +210,11 @@ export const useCustomerPaymentInstrument = ( }) } /** - * A hook for `ShopperCustomers#getCustomerProductLists`. * Returns all customer product lists. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerProductLists} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlists} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomerProductLists` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerProductLists| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlists | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomerProductLists = ( apiOptions: Argument, @@ -241,11 +241,11 @@ export const useCustomerProductLists = ( }) } /** - * A hook for `ShopperCustomers#getCustomerProductList`. * Returns a customer product list of the given customer and the items in the list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlist} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomerProductList` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerProductList| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlist | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomerProductList = ( apiOptions: Argument, @@ -272,11 +272,11 @@ export const useCustomerProductList = ( }) } /** - * A hook for `ShopperCustomers#getCustomerProductListItem`. * Returns an item of a customer product list and the actual product details like image, availability and price. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlistitem} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomerProductListItem` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getCustomerProductListItem| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getcustomerproductlistitem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCustomerProductListItem = ( apiOptions: Argument, @@ -309,11 +309,11 @@ export const useCustomerProductListItem = ( }) } /** - * A hook for `ShopperCustomers#getPublicProductListsBySearchTerm`. * Retrieves all public product lists as defined by the given search term (for example, email OR first name and last name). - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getPublicProductListsBySearchTerm} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getpublicproductlistsbysearchterm} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getPublicProductListsBySearchTerm` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getPublicProductListsBySearchTerm| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getpublicproductlistsbysearchterm | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePublicProductListsBySearchTerm = ( apiOptions: Argument, @@ -340,11 +340,11 @@ export const usePublicProductListsBySearchTerm = ( }) } /** - * A hook for `ShopperCustomers#getPublicProductList`. * Retrieves a public product list by ID and the items under that product list. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getPublicProductList} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getpublicproductlist} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getPublicProductList` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getPublicProductList| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getpublicproductlist | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePublicProductList = ( apiOptions: Argument, @@ -371,11 +371,11 @@ export const usePublicProductList = ( }) } /** - * A hook for `ShopperCustomers#getProductListItem`. * Retrieves an item from a public product list and the actual product details like product, image, availability and price. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getProductListItem} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getproductlistitem} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Customers `getProductListItem` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getProductListItem| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getproductlistitem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useProductListItem = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts index a46fce4596..81ff1a903f 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperExperience/query.ts @@ -14,15 +14,15 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperExperience'] /** - * A hook for `ShopperExperience#getPages`. * Get Page Designer pages. The results will apply the visibility rules for each page's components, such as personalization or scheduled visibility. Either `categoryId` or `productId` must be given in addition to `aspectTypeId`. Because only a single page-to-product and page-to-category assignment per aspect type can be authored today, the returned results contains one element at most. **Important**: Because this resource uses the GET method, you must not pass sensitive data (payment card information, for example) and must not perform any transactional processes within the server-side scripts that are run for the page and components. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-experience?meta=getPages} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperexperience.shopperexperience-1.html#getpages} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Experience `getPages` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-experience?meta=getPages| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperexperience.shopperexperience-1.html#getpages | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePages = ( apiOptions: Argument, @@ -49,13 +49,13 @@ export const usePages = ( }) } /** - * A hook for `ShopperExperience#getPage`. * Get a Page Designer page based on a single page ID. The results will apply the visibility rules for the page's components, such as personalization or scheduled visibility. **Important**: Because this resource uses the GET method, you must not pass sensitive data (payment card information, for example) and must not perform any transactional processes within the server-side scripts that are run for the page and components. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-experience?meta=getPage} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperexperience.shopperexperience-1.html#getpage} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Experience `getPage` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-experience?meta=getPage| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperexperience.shopperexperience-1.html#getpage | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePage = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts index 3f630af32e..dafd3db2c6 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperGiftCertificates/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperGiftCertificates'] /** - * A hook for `ShopperGiftCertificates#getGiftCertificate`. * Action to retrieve an existing gift certificate. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-gift-certificates?meta=getGiftCertificate} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppergiftcertificates.shoppergiftcertificates-1.html#getgiftcertificate} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Gift Certificates `getGiftCertificate` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-gift-certificates?meta=getGiftCertificate| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppergiftcertificates.shoppergiftcertificates-1.html#getgiftcertificate | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useGiftCertificate = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts index 98d9dcd350..67a5eea5c8 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts @@ -13,21 +13,148 @@ import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperLogin'] +/** Mutations available for Shopper Login. */ export const ShopperLoginMutations = { + /** + * Logs in a shopper with credentials that are managed by a B2C Commerce instance (ECOM). It follows the authorization code grant flow as defined by the OAuth 2.1 standard. It also uses a proof key for code exchange (PKCE). + +For PKCE values: +- The `code_verifier` string is a random string used for the `/token` endpoint request. +- The `code_challenge` is an encoded version of the `code_verifier` string using an SHA-256 hash. + +The request must include a basic authorization header that contains a Base64 encoded version of the following string: `\:\`. + +Required parameters: `code_challenge`, `channel_id`, `client_id`, and `redirect_uri`. + +Optional parameters: `usid``. + +The SLAS `/login`` endpoint redirects back to the redirect URI and returns an authorization code. + + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `authenticateCustomer` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=authenticateCustomer| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#authenticatecustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AuthenticateCustomer: 'authenticateCustomer', + /** + * Allows the customer to authenticate when their identity provider is down. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `authorizePasswordlessCustomer` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=authorizePasswordlessCustomer| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#authorizepasswordlesscustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ AuthorizePasswordlessCustomer: 'authorizePasswordlessCustomer', + /** + * Log out a shopper. The shopper's access token and refresh token are revoked. If the shopper authenticated with a B2C Commerce (ECOM) instance, the OCAPI JWT is also revoked. This should be called for Registered users that have logged in using SLAS. his should be called for registered users that have logged in using SLAS. This endpoint is not for use with guest users. + +Required header: Authorization header bearer token of the Shopper access token to logout. + +Required parameters: `refresh token`, `channel_id`, and `client`. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `logoutCustomer` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=logoutCustomer| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#logoutcustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ LogoutCustomer: 'logoutCustomer', + /** + * Get the shopper or guest JWT access token and a refresh token. This is the second step of the OAuth 2.1 authorization code flow. + +For a private client, an application is able to get an access token for the shopper through the back channel (a trusted server) by passing in the client credentials and the authorization code retrieved from the `authorize` endpoint. + +For a guest user, get the shopper JWT access token and a refresh token. This is where a client appplication is able to get an access token for the guest user through the back channel (a trusted server) by passing in the client credentials. + +For a public client using PKCE, an application will pass a PKCE `code_verifier`` that matches the `code_challenge`` that was used to `authorize` the customer along with the authorization code. + +When refreshing the access token with a private client ID and client secret, the refresh token is _not_ regenerated. However, when refreshing the access token with a public client ID, the refresh token is _always_ regenerated. The old refresh token is voided with every refresh call, so the refresh token on the client needs to be replaced to always store the new refresh token. + +See the Body section for required parameters, including `grant_type` and others, depending on the value of `grant_type`. + +**Important**: We strongly recommended using the `channel_id` query parameter because **it will be required in the future**. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `getAccessToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getAccessToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getaccesstoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ GetAccessToken: 'getAccessToken', + /** + * Get a shopper JWT access token for a registered customer using session bridge. + +For public client id requests the grant_type must be set to `session_bridge`. + +For private client_id and secret the grant_type must be set to `client_credentials` along with a basic authorization header. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `getSessionBridgeAccessToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getSessionBridgeAccessToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getsessionbridgeaccesstoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ GetSessionBridgeAccessToken: 'getSessionBridgeAccessToken', + /** + * Get a shopper JWT access token for a registered customer whose credentials are stored using a third party system. + +For external trusted-system requests, a basic authorization header that includes a SLAS client ID and SLAS client secret can be used in place of the bearer token. + +For internal trusted-system requests, the bearer token must be a C2C JWT. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `getTrustedSystemAccessToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getTrustedSystemAccessToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#gettrustedsystemaccesstoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ GetTrustedSystemAccessToken: 'getTrustedSystemAccessToken', + /** + * Get a shopper JWT access token for a registered customer using a trusted agent (merchant). + +If using a SLAS private client ID, you must also use an `_sfdc_client_auth` header. + +The value of the `_sfdc_client_auth` header must be a Base64-encoded string. The string is composed of a SLAS private client ID and client secret, separated by a colon (`:`). For example, `privateClientId:privateClientsecret` becomes `cHJpdmF0ZUNsaWVudElkOnByaXZhdGVDbGllbnRzZWNyZXQ=` after Base64 encoding. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `getTrustedAgentAccessToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getTrustedAgentAccessToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#gettrustedagentaccesstoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ GetTrustedAgentAccessToken: 'getTrustedAgentAccessToken', + /** + * Request a reset password token + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `getPasswordResetToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getPasswordResetToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getpasswordresettoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ GetPasswordResetToken: 'getPasswordResetToken', + /** + * Creates a new password + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `resetPassword` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=resetPassword| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#resetpassword | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ ResetPassword: 'resetPassword', + /** + * Issue a shopper token (JWT). + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `getPasswordLessAccessToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getPasswordLessAccessToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getpasswordlessaccesstoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ GetPasswordLessAccessToken: 'getPasswordLessAccessToken', + /** + * Invalidate the refresh token. A basic auth header with Base64-encoded `clientId:secret` is required in the Authorization header, and the refresh token to be revoked is required in the body. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `revokeToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=revokeToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#revoketoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RevokeToken: 'revokeToken', + /** + * Returns the token properties. A basic auth header with Base64-encoded `clientId:secret` is required in the Authorization header, as well as an access token or refresh token. Use `token_type_hint` to help identify the token. + * @returns A TanStack Query mutation hook for interacting with the Shopper Login `introspectToken` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=introspectToken| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#introspecttoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ IntrospectToken: 'introspectToken' } as const +/** Mutation for Shopper Login. */ export type ShopperLoginMutation = (typeof ShopperLoginMutations)[keyof typeof ShopperLoginMutations] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts index ef3c828744..83df255c0b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperLogin'] /** - * A hook for `ShopperLogin#retrieveCredQualityUserInfo`. * Get credential quality statistics for a user. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=retrieveCredQualityUserInfo} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#retrievecredqualityuserinfo} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Login `retrieveCredQualityUserInfo` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=retrieveCredQualityUserInfo| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#retrievecredqualityuserinfo | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCredQualityUserInfo = ( apiOptions: Argument, @@ -45,11 +45,11 @@ export const useCredQualityUserInfo = ( }) } /** - * A hook for `ShopperLogin#getUserInfo`. * Returns a JSON listing of claims about the currently authenticated user. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getUserInfo} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getuserinfo} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Login `getUserInfo` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getUserInfo| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getuserinfo | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useUserInfo = ( apiOptions: Argument, @@ -76,11 +76,11 @@ export const useUserInfo = ( }) } /** - * A hook for `ShopperLogin#getWellknownOpenidConfiguration`. * Returns a JSON listing of the OpenID/OAuth endpoints, supported scopes and claims, public keys used to sign the tokens, and other details. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getWellknownOpenidConfiguration} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getwellknownopenidconfiguration} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Login `getWellknownOpenidConfiguration` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getWellknownOpenidConfiguration| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getwellknownopenidconfiguration | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useWellknownOpenidConfiguration = ( apiOptions: Argument, @@ -107,11 +107,11 @@ export const useWellknownOpenidConfiguration = ( }) } /** - * A hook for `ShopperLogin#getJwksUri`. * Returns a JSON Web Key Set (JWKS) containing the current, past, and future public keys. The key set enables clients to validate the Shopper JSON Web Token (JWT) issued by SLAS. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getJwksUri} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getjwksuri} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Login `getJwksUri` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getJwksUri| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getjwksuri | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useJwksUri = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts index e190914a89..7bc277df97 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/mutation.ts @@ -13,13 +13,54 @@ import {cacheUpdateMatrix} from './cache' type Client = ApiClients['shopperOrders'] +/** Mutations available for Shopper Orders. */ export const ShopperOrdersMutations = { + /** + * Submits an order based on a prepared basket. The only considered value from the request body is basketId. + * @returns A TanStack Query mutation hook for interacting with the Shopper Orders `createOrder` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createOrder| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createorder | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreateOrder: 'createOrder', + /** + * Adds a payment instrument to an order. + +Details: + +The payment instrument is added with the provided details. The payment method must be applicable for the order see GET +/baskets/\{basketId\}/payment-methods, if the payment method is 'CREDIT_CARD' a paymentCard must be specified in the request. + * @returns A TanStack Query mutation hook for interacting with the Shopper Orders `createPaymentInstrumentForOrder` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=createPaymentInstrumentForOrder| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#createpaymentinstrumentfororder | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ CreatePaymentInstrumentForOrder: 'createPaymentInstrumentForOrder', + /** + * Removes a payment instrument of an order. + * @returns A TanStack Query mutation hook for interacting with the Shopper Orders `removePaymentInstrumentFromOrder` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=removePaymentInstrumentFromOrder| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#removepaymentinstrumentfromorder | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ RemovePaymentInstrumentFromOrder: 'removePaymentInstrumentFromOrder', + /** + * Updates a payment instrument of an order. + +Details: + +The payment instrument is updated with the provided details. The payment method must be applicable for the +order see GET /baskets/\{basketId\}/payment-methods, if the payment method is 'CREDIT_CARD' a +paymentCard must be specified in the request. + * @returns A TanStack Query mutation hook for interacting with the Shopper Orders `updatePaymentInstrumentForOrder` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=updatePaymentInstrumentForOrder| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#updatepaymentinstrumentfororder | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + */ UpdatePaymentInstrumentForOrder: 'updatePaymentInstrumentForOrder' } as const +/** Mutation for Shopper Orders. */ export type ShopperOrdersMutation = (typeof ShopperOrdersMutations)[keyof typeof ShopperOrdersMutations] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts index 34f75313a2..6a5a8e30b0 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperOrders'] /** - * A hook for `ShopperOrders#getOrder`. * Gets information for an order. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#getorder} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Orders `getOrder` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getOrder| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#getorder | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useOrder = ( apiOptions: Argument, @@ -45,11 +45,11 @@ export const useOrder = ( }) } /** - * A hook for `ShopperOrders#getPaymentMethodsForOrder`. * Gets the applicable payment methods for an existing order considering the open payment amount only. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getPaymentMethodsForOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#getpaymentmethodsfororder} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Orders `getPaymentMethodsForOrder` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getPaymentMethodsForOrder| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#getpaymentmethodsfororder | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePaymentMethodsForOrder = ( apiOptions: Argument, @@ -76,13 +76,13 @@ export const usePaymentMethodsForOrder = ( }) } /** - * A hook for `ShopperOrders#getTaxesFromOrder`. * This method gives you the external taxation data of the order transferred from the basket during order creation. This endpoint can be called only if external taxation was used. See POST /baskets for more information. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getTaxesFromOrder} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#gettaxesfromorder} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Orders `getTaxesFromOrder` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=getTaxesFromOrder| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperorders.shopperorders-1.html#gettaxesfromorder | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useTaxesFromOrder = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts index fedbaa302b..32500f7819 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperProducts'] /** - * A hook for `ShopperProducts#getProducts`. * Allows access to multiple products by a single request. Only products that are online and assigned to a site catalog are returned. The maximum number of productIDs that can be requested are 24. Along with product details, the availability, product options, images, price, promotions, and variations for the valid products will be included, as appropriate. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getProducts} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getproducts} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Products `getProducts` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getProducts| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getproducts | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useProducts = ( apiOptions: Argument, @@ -45,11 +45,11 @@ export const useProducts = ( }) } /** - * A hook for `ShopperProducts#getProduct`. * Allows access to product details for a single product ID. Only products that are online and assigned to a site catalog are returned. Along with product details, the availability, images, price, bundled_products, set_products, recommedations, product options, variations, and promotions for the products will be included, as appropriate. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getProduct} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getproduct} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Products `getProduct` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getProduct| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getproduct | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useProduct = ( apiOptions: Argument, @@ -76,11 +76,11 @@ export const useProduct = ( }) } /** - * A hook for `ShopperProducts#getCategories`. * When you use the URL template, the server returns multiple categories (a result object of category documents). You can use this template as a convenient way of obtaining multiple categories in a single request, instead of issuing separate requests for each category. You can specify up to 50 multiple IDs. You must enclose the list of IDs in parentheses. If a category identifier contains parenthesis or the separator sign, you must URL encode the character. The server only returns online categories. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getCategories} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getcategories} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Products `getCategories` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getCategories| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getcategories | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCategories = ( apiOptions: Argument, @@ -107,13 +107,13 @@ export const useCategories = ( }) } /** - * A hook for `ShopperProducts#getCategory`. * When you use the URL template below, the server returns a category identified by its ID; by default, the server also returns the first level of subcategories, but you can specify another level by setting the levels parameter. The server only returns online categories. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getCategory} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getcategory} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Products `getCategory` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getCategory| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperproducts.shopperproducts-1.html#getcategory | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useCategory = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts index e3a6886400..42c63913e8 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperPromotions/query.ts @@ -14,11 +14,11 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperPromotions'] /** - * A hook for `ShopperPromotions#getPromotions`. * Returns an array of enabled promotions for a list of specified IDs. In the request URL, you can specify up to 50 IDs. If you specify an ID that contains either parentheses or the separator characters, you must URL encode these characters. Each request returns only enabled promotions as the server does not consider promotion qualifiers or schedules. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-promotions?meta=getPromotions} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperpromotions.shopperpromotions-1.html#getpromotions} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Promotions `getPromotions` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-promotions?meta=getPromotions| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperpromotions.shopperpromotions-1.html#getpromotions | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePromotions = ( apiOptions: Argument, @@ -45,15 +45,15 @@ export const usePromotions = ( }) } /** - * A hook for `ShopperPromotions#getPromotionsForCampaign`. * Handles get promotion by filter criteria. Returns an array of enabled promotions matching the specified filter criteria. In the request URL, you must provide a campaign_id parameter, and you can optionally specify a date range by providing start_date and end_date parameters. Both parameters are required to specify a date range, as omitting one causes the server to return a MissingParameterException fault. Each request returns only enabled promotions, since the server does not consider promotion qualifiers or schedules. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-promotions?meta=getPromotionsForCampaign} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperpromotions.shopperpromotions-1.html#getpromotionsforcampaign} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Promotions `getPromotionsForCampaign` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-promotions?meta=getPromotionsForCampaign| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperpromotions.shopperpromotions-1.html#getpromotionsforcampaign | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const usePromotionsForCampaign = ( apiOptions: Argument, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts index 3420f3737c..78ac9e3f67 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperSearch/query.ts @@ -14,12 +14,12 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperSearch'] /** - * A hook for `ShopperSearch#productSearch`. * Provides keyword and refinement search functionality for products. Only returns the product ID, link, and name in the product search hit. The search result contains only products that are online and assigned to site catalog. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-search?meta=productSearch} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppersearch.shoppersearch-1.html#productsearch} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Search `productSearch` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-search?meta=productSearch| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppersearch.shoppersearch-1.html#productsearch | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useProductSearch = ( apiOptions: Argument, @@ -46,11 +46,11 @@ export const useProductSearch = ( }) } /** - * A hook for `ShopperSearch#getSearchSuggestions`. * Provides keyword search functionality for products, categories, and brands suggestions. Returns suggested products, suggested categories, and suggested brands for the given search phrase. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-search?meta=getSearchSuggestions} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppersearch.shoppersearch-1.html#getsearchsuggestions} for more information on the parameters and returned data type. - * @returns An object describing the state of the request. + * @returns A TanStack Query query hook with data from the Shopper Search `getSearchSuggestions` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-search?meta=getSearchSuggestions| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppersearch.shoppersearch-1.html#getsearchsuggestions | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. */ export const useSearchSuggestions = ( apiOptions: Argument, From 7d52245c67ae6f7ff875f398bc656d6569fee952 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Tue, 28 Feb 2023 16:55:10 -0500 Subject: [PATCH 116/122] Remove unused variable. --- packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts index 9589d5d61c..094ec8740e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/cache.ts @@ -10,11 +10,6 @@ import {getOrder} from './queryKeyHelpers' type Client = ApiClients['shopperOrders'] -const basePath = (parameters: Client['clientConfig']['parameters']) => [ - '/organizations/', - parameters.organizationId -] - /** Logs a warning to console (on startup) and returns nothing (method is unimplemented). */ const TODO = (method: keyof Client) => { console.warn(`Cache logic for '${method}' is not yet implemented.`) From 89181789d7adeaec12ccc04d302c6372d7afe3f5 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 1 Mar 2023 12:32:06 -0500 Subject: [PATCH 117/122] Remove SLAS `authenticateCustomer` and `getPasswordResetToken` hooks. They modify state and return headers, rather than body, so they are not suitable for the current state of mutation hooks. --- .../src/hooks/ShopperLogin/mutation.test.ts | 2 -- .../src/hooks/ShopperLogin/mutation.ts | 29 ------------------- 2 files changed, 31 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts index d806fdeec7..9c1835acba 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.test.ts @@ -64,11 +64,9 @@ type Implemented = Exclude // This is an object rather than an array to more easily ensure we cover all mutations type TestMap = {[Mut in Implemented]: [Argument, DataType]} const testMap: TestMap = { - authenticateCustomer: [OPTIONS, undefined], authorizePasswordlessCustomer: [OPTIONS, {}], getAccessToken: [OPTIONS, TOKEN_RESPONSE], getPasswordLessAccessToken: [OPTIONS, TOKEN_RESPONSE], - getPasswordResetToken: [OPTIONS, undefined], getSessionBridgeAccessToken: [OPTIONS, TOKEN_RESPONSE], getTrustedAgentAccessToken: [OPTIONS, TOKEN_RESPONSE], getTrustedSystemAccessToken: [OPTIONS, TOKEN_RESPONSE], diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts index 67a5eea5c8..6e9b60a2d7 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/mutation.ts @@ -15,27 +15,6 @@ type Client = ApiClients['shopperLogin'] /** Mutations available for Shopper Login. */ export const ShopperLoginMutations = { - /** - * Logs in a shopper with credentials that are managed by a B2C Commerce instance (ECOM). It follows the authorization code grant flow as defined by the OAuth 2.1 standard. It also uses a proof key for code exchange (PKCE). - -For PKCE values: -- The `code_verifier` string is a random string used for the `/token` endpoint request. -- The `code_challenge` is an encoded version of the `code_verifier` string using an SHA-256 hash. - -The request must include a basic authorization header that contains a Base64 encoded version of the following string: `\:\`. - -Required parameters: `code_challenge`, `channel_id`, `client_id`, and `redirect_uri`. - -Optional parameters: `usid``. - -The SLAS `/login`` endpoint redirects back to the redirect URI and returns an authorization code. - - * @returns A TanStack Query mutation hook for interacting with the Shopper Login `authenticateCustomer` endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=authenticateCustomer| Salesforce Developer Center} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#authenticatecustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. - * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. - */ - AuthenticateCustomer: 'authenticateCustomer', /** * Allows the customer to authenticate when their identity provider is down. * @returns A TanStack Query mutation hook for interacting with the Shopper Login `authorizePasswordlessCustomer` endpoint. @@ -112,14 +91,6 @@ The value of the `_sfdc_client_auth` header must be a Base64-encoded string. The * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. */ GetTrustedAgentAccessToken: 'getTrustedAgentAccessToken', - /** - * Request a reset password token - * @returns A TanStack Query mutation hook for interacting with the Shopper Login `getPasswordResetToken` endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getPasswordResetToken| Salesforce Developer Center} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperlogin.shopperlogin-1.html#getpasswordresettoken | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. - * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. - */ - GetPasswordResetToken: 'getPasswordResetToken', /** * Creates a new password * @returns A TanStack Query mutation hook for interacting with the Shopper Login `resetPassword` endpoint. From bff51744ae888761b0debaf96a98f5d3edf34f56 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 1 Mar 2023 12:37:24 -0500 Subject: [PATCH 118/122] Remove deprecated Shopper Customers mutations. --- .../hooks/ShopperCustomers/mutation.test.ts | 3 - .../src/hooks/ShopperCustomers/mutation.ts | 87 ------------------- 2 files changed, 90 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts index a4d77bc384..29dcd16584 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.test.ts @@ -88,10 +88,7 @@ const createOptions = ( // and the `TestMap` type can be changed from optional keys to required keys. Doing so will // leverage TypeScript to enforce having tests for all mutations. const notImplTestCases: ShopperCustomersMutation[][] = [ - ['authorizeCustomer'], - ['authorizeTrustedSystem'], ['deleteCustomerProductList'], - ['invalidateCustomerAuth'], ['updateCustomerPassword'], ['updateCustomerProductList'] ] diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts index 392028a5b2..63284871d1 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts @@ -23,93 +23,6 @@ export const ShopperCustomersMutations = { * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. */ RegisterCustomer: 'registerCustomer', - /** - * **DEPRECATION NOTICE** - -To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - ---- - -Log the user out. - * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `invalidateCustomerAuth` endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=invalidateCustomerAuth| Salesforce Developer Center} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#invalidatecustomerauth | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. - * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. - */ - InvalidateCustomerAuth: 'invalidateCustomerAuth', - /** - * **DEPRECATION NOTICE** - -To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - ---- - -Obtains a new JSON Web Token (JWT)for a guest or registered -customer. Tokens are returned as an HTTP Authorization:Bearer response -header entry. These kinds of request are supported, as specified by the -type: - -Type guest - creates a guest (non-authenticated) customer -and returns a token for the customer. -Request Body for guest : \{\"type\": \"guest\"\} -Type credentials - authenticates credentials passed in the -HTTP Authorization:Basic request header, returning a token for a -successfully authenticated customer, otherwise it throws an -AuthenticationFailedException. -Request Body for guest : \{\"type\": \"credentials\"\} -Type refresh - examines the token passed in the HTTP -Authorization:Bearer request header and when valid returns a new token -with an updated expiry time. -Request Body for guest : \{\"type\": \"refresh\"\} - -For a request of type credentials: - -Updates profile attributes for the customer (for example, -\"last-visited\"). -Handles the maximum number of failed login attempts. - -About JWT The token contains 3 sections: - -The header section (specifies token type and algorithm used), -the payload section (contains customer information, client ID, -issue, and expiration time), -finally the signature section records the token signature. - -A token is created and returned to the client whenever a registered -customer logs in (type \"credentials\") or a guest customer requests it (type -\"guest\"). The token is returned in the response header as -Authorization: Bearer --token-- - -The client has to include the token in the request header as -Authorization: Bearer --token-- -in any follow-up request. The server declines any follow-up requests -without a token or which cannot be verified based on the token signature -or expiration time. A token nearing its expiration time should be -exchanged for a new one (type \"refresh\"). - -See \"API Usage \> JWT\" for more details on using JWT as an authentication -mechanism. - * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `authorizeCustomer` endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeCustomer| Salesforce Developer Center} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizecustomer | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. - * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. - */ - AuthorizeCustomer: 'authorizeCustomer', - /** - * **DEPRECATION NOTICE** - -To enhance the security and availability of Salesforce services, this endpoint is now _**deprecated**_, and _**we plan to remove it in mid-2022**_. This endpoint is not available to new customers, and we discourage existing customers from using it. Instead, we strongly recommend using the endpoints of the [Shopper Login and API Access Service](https://developer.commercecloud.com/s/api-details/a003k00000VWfNDAA1/commerce-cloud-developer-centershopperloginandapiaccessservice) (SLAS) because they meet our rigorous standards for security and availability. - ---- - -Obtain the JSON Web Token (JWT) for registered customers whose credentials are stored using a third party system. Accepts loginId and -clientId, returns a customer object in the response body and the JWT generated against the clientId in the response header. - * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `authorizeTrustedSystem` endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=authorizeTrustedSystem| Salesforce Developer Center} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#authorizetrustedsystem | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. - * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. - */ - AuthorizeTrustedSystem: 'authorizeTrustedSystem', /** * Reset customer password, after obtaining a reset token. This is the second step in the reset customer password flow, where a customer password is reset by providing the new credentials along with a reset token. This call should be preceded by a call to the /create-reset-token endpoint. * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `resetPassword` endpoint. From a5cc03c405e3ed7289dbadbe4b0b72851569b890 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 1 Mar 2023 12:47:32 -0500 Subject: [PATCH 119/122] Comment out Shopper Customers endpoints in closed beta. --- .../src/hooks/ShopperCustomers/cache.ts | 5 +- .../src/hooks/ShopperCustomers/mutation.ts | 17 ++--- .../src/hooks/ShopperCustomers/query.ts | 69 ++++++++++--------- 3 files changed, 46 insertions(+), 45 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts index 069812339b..d309a5db63 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts @@ -31,8 +31,6 @@ const invalidateCustomer = (parameters: Tail): CacheUp }) export const cacheUpdateMatrix: CacheUpdateMatrix = { - authorizeCustomer: TODO('authorizeCustomer'), - authorizeTrustedSystem: TODO('authorizeTrustedSystem'), createCustomerAddress(customerId, {parameters}, response) { // getCustomerAddress uses `addressName` rather than `addressId` const newParams = {...parameters, addressName: response.addressId} @@ -101,7 +99,8 @@ export const cacheUpdateMatrix: CacheUpdateMatrix = { invalidateCustomerAuth: TODO('invalidateCustomerAuth'), // TODO: Should this update the `getCustomer` cache? registerCustomer: noop, - registerExternalProfile: TODO('registerExternalProfile'), + // TODO: Implement when the endpoint exits closed beta. + // registerExternalProfile: TODO('registerExternalProfile'), removeCustomerAddress(customerId, {parameters}) { return { // TODO: Rather than invalidate, can we selectively update? diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts index 63284871d1..18e456877b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/mutation.ts @@ -39,14 +39,15 @@ export const ShopperCustomersMutations = { * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. */ GetResetPasswordToken: 'getResetPasswordToken', - /** - * Registers a new external profile for a customer. This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program - * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `registerExternalProfile` endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerExternalProfile| Salesforce Developer Center} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registerexternalprofile | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. - * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. - */ - RegisterExternalProfile: 'registerExternalProfile', + // TODO: Re-implement (and update description from RAML spec) when the endpoint exits closed beta. + // /** + // * Registers a new external profile for a customer. This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program + // * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `registerExternalProfile` endpoint. + // * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=registerExternalProfile| Salesforce Developer Center} for more information about the API endpoint. + // * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#registerexternalprofile | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + // * @see {@link https://tanstack.com/query/latest/docs/react/reference/useMutation | TanStack Query `useMutation` reference} for more information about the return value. + // */ + // RegisterExternalProfile: 'registerExternalProfile', /** * Updates a customer. * @returns A TanStack Query mutation hook for interacting with the Shopper Customers `updateCustomer` endpoint. diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts index 3408de97ec..8e69565b39 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.ts @@ -13,42 +13,43 @@ import * as queryKeyHelpers from './queryKeyHelpers' type Client = ApiClients['shopperCustomers'] -/** - * Gets the new external profile for a customer.This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program - * @returns A TanStack Query query hook with data from the Shopper Customers `getExternalProfile` endpoint. - * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getExternalProfile| Salesforce Developer Center} for more information about the API endpoint. - * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getexternalprofile | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. - * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. - */ -export const useExternalProfile = ( - apiOptions: Argument, - queryOptions: ApiQueryOptions = {} -): UseQueryResult> => { - type Options = Argument - type Data = DataType - const {shopperCustomers: client} = useCommerceApi() - const methodName = 'getExternalProfile' - const requiredParameters = [ - 'organizationId', - 'externalId', - 'authenticationProviderId', - 'siteId' - ] as const +// TODO: Re-implement (and update description from RAML spec) when the endpoint exits closed beta. +// /** +// * Gets the new external profile for a customer.This endpoint is in closed beta, available to select few customers. Please get in touch with your Account Team if you'd like to participate in the beta program +// * @returns A TanStack Query query hook with data from the Shopper Customers `getExternalProfile` endpoint. +// * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-customers?meta=getExternalProfile| Salesforce Developer Center} for more information about the API endpoint. +// * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shoppercustomers.shoppercustomers-1.html#getexternalprofile | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. +// * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. +// */ +// export const useExternalProfile = ( +// apiOptions: Argument, +// queryOptions: ApiQueryOptions = {} +// ): UseQueryResult> => { +// type Options = Argument +// type Data = DataType +// const {shopperCustomers: client} = useCommerceApi() +// const methodName = 'getExternalProfile' +// const requiredParameters = [ +// 'organizationId', +// 'externalId', +// 'authenticationProviderId', +// 'siteId' +// ] as const - // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order - // to generate the correct query key. - const netOptions = mergeOptions(client, apiOptions) - const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) - const method = async (options: Options) => await client[methodName](options) +// // Parameters can be set in `apiOptions` or `client.clientConfig`, we must merge them in order +// // to generate the correct query key. +// const netOptions = mergeOptions(client, apiOptions) +// const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) +// const method = async (options: Options) => await client[methodName](options) - // For some reason, if we don't explicitly set these generic parameters, the inferred type for - // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. - return useQuery(netOptions, queryOptions, { - method, - queryKey, - requiredParameters - }) -} +// // For some reason, if we don't explicitly set these generic parameters, the inferred type for +// // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. +// return useQuery(netOptions, queryOptions, { +// method, +// queryKey, +// requiredParameters +// }) +// } /** * Gets a customer with all existing addresses and payment instruments associated with the requested customer. * @returns A TanStack Query query hook with data from the Shopper Customers `getCustomer` endpoint. From 8a9fdcc47c12482770c1f85408cdb36fef1a3473 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 1 Mar 2023 12:47:41 -0500 Subject: [PATCH 120/122] Fix Shopper Customers tests. --- .../src/hooks/ShopperCustomers/index.test.ts | 10 ++++++---- .../src/hooks/ShopperCustomers/query.test.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts index b3a592c119..e6df648f6c 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts @@ -17,10 +17,12 @@ describe('Shopper Customers hooks', () => { cacheUpdateMatrix ) expect(unimplemented).toEqual([ - 'invalidateCustomerAuth', - 'authorizeCustomer', - 'authorizeTrustedSystem', - 'registerExternalProfile', + 'invalidateCustomerAuth', // DEPRECATED, not included + 'authorizeCustomer', // DEPRECATED, not included + 'authorizeTrustedSystem', // DEPRECATED, not included + 'registerExternalProfile', // TODO: Implement when the endpoint exits closed beta + 'getExternalProfile', // TODO: Implement when the endpoint exits closed beta + // The rest, we just haven't gotten to yet 'updateCustomerPassword', 'deleteCustomerProductList', 'updateCustomerProductList' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts index 6815fdfd24..ac5c55fb16 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/query.test.ts @@ -51,11 +51,12 @@ const testMap: TestMap = { useCustomerProductList: {}, useCustomerProductListItem: {priority: 9000, public: false, quantity: 0}, useCustomerProductLists: {data: [], limit: 0, total: 0}, - useExternalProfile: { - authenticationProviderId: 'squirrel', - customerId: 'customerId', - externalId: 'external' - }, + // TODO: Re-implement test when the endpoint exits closed beta. + // useExternalProfile: { + // authenticationProviderId: 'squirrel', + // customerId: 'customerId', + // externalId: 'external' + // }, useProductListItem: {id: 'id', priority: 0, type: 'thing'}, usePublicProductList: {id: 'id', public: true, type: 'other'}, usePublicProductListsBySearchTerm: {data: [], limit: 0, total: 0} From 4b645c7b2d285d57a39c1f9f3a2c0d88eb2876c2 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 1 Mar 2023 12:57:56 -0500 Subject: [PATCH 121/122] Fix Shopper Login tests. --- .../commerce-sdk-react/src/hooks/ShopperLogin/cache.ts | 2 -- .../src/hooks/ShopperLogin/index.test.ts | 9 ++++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts index bfdb5d5bfe..f8024e5ce3 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/cache.ts @@ -18,14 +18,12 @@ const TODO = (method: string): undefined => { } export const cacheUpdateMatrix: CacheUpdateMatrix = { - authenticateCustomer: noop, authorizePasswordlessCustomer: noop, logoutCustomer: TODO('logoutCustomer'), getAccessToken: noop, getSessionBridgeAccessToken: noop, getTrustedSystemAccessToken: noop, getTrustedAgentAccessToken: noop, - getPasswordResetToken: noop, resetPassword: noop, getPasswordLessAccessToken: noop, revokeToken: noop, diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts index 17b2796f7d..44eb3e7704 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts @@ -13,12 +13,11 @@ describe('Shopper Login hooks', () => { test('all endpoints have hooks', () => { const unimplemented = getUnimplementedEndpoints(ShopperLogin, queries, cacheUpdateMatrix) expect(unimplemented).toEqual([ - // TODO: implement - 'logoutCustomer', - // These methods generate headers - they don't mutate or return any data, so they don't make - // sense as query/mutation hooks (as currently implemented). + 'authenticateCustomer', + 'logoutCustomer', // TODO: Implement 'authorizeCustomer', - 'getTrustedAgentAuthorizationToken' + 'getTrustedAgentAuthorizationToken', + 'getPasswordResetToken' ]) }) }) From a5b0a405197a5025e78388e981dfc78459bc0862 Mon Sep 17 00:00:00 2001 From: Will Harney Date: Wed, 1 Mar 2023 14:15:21 -0500 Subject: [PATCH 122/122] Update "hook exists" tests to distinguish between "mutation in enum" and "mutation has cache logic" --- .../src/hooks/ShopperBaskets/index.test.ts | 18 ++++++++++++-- .../src/hooks/ShopperContexts/index.test.ts | 18 ++++++++++++-- .../src/hooks/ShopperCustomers/index.test.ts | 24 +++++++++++++------ .../src/hooks/ShopperLogin/index.test.ts | 19 +++++++++++++-- .../src/hooks/ShopperOrders/index.test.ts | 18 ++++++++++++-- .../commerce-sdk-react/src/test-utils.tsx | 7 ++---- 6 files changed, 84 insertions(+), 20 deletions(-) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts index 4e8617737a..2dc8bf7a67 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperBaskets/index.test.ts @@ -7,12 +7,26 @@ import {ShopperBaskets} from 'commerce-sdk-isomorphic' import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' +import {ShopperBasketsMutations as mutations} from './mutation' import * as queries from './query' describe('Shopper Baskets hooks', () => { test('all endpoints have hooks', () => { - const unimplemented = getUnimplementedEndpoints(ShopperBaskets, queries, cacheUpdateMatrix) - expect(unimplemented).toEqual([ + // unimplemented = SDK method exists, but no query hook or value in mutations enum + const unimplemented = getUnimplementedEndpoints(ShopperBaskets, 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([]) + }) + test('all mutations have cache update logic', () => { + // unimplemented = value in mutations enum, but no method in cache update matrix + const unimplemented = new Set(Object.values(mutations)) + Object.entries(cacheUpdateMatrix).forEach(([method, implementation]) => { + if (implementation) unimplemented.delete(method) + }) + // If this test fails: add cache update logic, remove the endpoint from the mutations enum, + // or add it to the `expected` array to indicate that it is still a TODO. + expect([...unimplemented]).toEqual([ 'transferBasket', 'addGiftCertificateItemToBasket', 'removeGiftCertificateItemFromBasket', diff --git a/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts index 91558de24a..64b187ab4b 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperContexts/index.test.ts @@ -7,12 +7,26 @@ import {ShopperContexts} from 'commerce-sdk-isomorphic' import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' +import {ShopperContextsMutations as mutations} from './mutation' import * as queries from './query' describe('Shopper Contexts hooks', () => { test('all endpoints have hooks', () => { - const unimplemented = getUnimplementedEndpoints(ShopperContexts, queries, cacheUpdateMatrix) - expect(unimplemented).toEqual([ + // unimplemented = SDK method exists, but no query hook or value in mutations enum + const unimplemented = getUnimplementedEndpoints(ShopperContexts, 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([]) + }) + test('all mutations have cache update logic', () => { + // unimplemented = value in mutations enum, but no method in cache update matrix + const unimplemented = new Set(Object.values(mutations)) + Object.entries(cacheUpdateMatrix).forEach(([method, implementation]) => { + if (implementation) unimplemented.delete(method) + }) + // If this test fails: add cache update logic, remove the endpoint from the mutations enum, + // or add it to the `expected` array to indicate that it is still a TODO. + expect([...unimplemented]).toEqual([ 'createShopperContext', 'deleteShopperContext', 'updateShopperContext' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts index e6df648f6c..16e968bca2 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperCustomers/index.test.ts @@ -7,22 +7,32 @@ import {ShopperCustomers} from 'commerce-sdk-isomorphic' import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' +import {ShopperCustomersMutations as mutations} from './mutation' import * as queries from './query' describe('Shopper Customers hooks', () => { test('all endpoints have hooks', () => { - const unimplemented = getUnimplementedEndpoints( - ShopperCustomers, - queries, - cacheUpdateMatrix - ) + // unimplemented = SDK method exists, but no query hook or value in mutations enum + const unimplemented = getUnimplementedEndpoints(ShopperCustomers, 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([ 'invalidateCustomerAuth', // DEPRECATED, not included 'authorizeCustomer', // DEPRECATED, not included 'authorizeTrustedSystem', // DEPRECATED, not included 'registerExternalProfile', // TODO: Implement when the endpoint exits closed beta - 'getExternalProfile', // TODO: Implement when the endpoint exits closed beta - // The rest, we just haven't gotten to yet + 'getExternalProfile' // TODO: Implement when the endpoint exits closed beta + ]) + }) + test('all mutations have cache update logic', () => { + // unimplemented = value in mutations enum, but no method in cache update matrix + const unimplemented = new Set(Object.values(mutations)) + Object.entries(cacheUpdateMatrix).forEach(([method, implementation]) => { + if (implementation) unimplemented.delete(method) + }) + // If this test fails: add cache update logic, remove the endpoint from the mutations enum, + // or add it to the `expected` array to indicate that it is still a TODO. + expect([...unimplemented]).toEqual([ 'updateCustomerPassword', 'deleteCustomerProductList', 'updateCustomerProductList' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts index 44eb3e7704..397d2202ca 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperLogin/index.test.ts @@ -7,17 +7,32 @@ import {ShopperLogin} from 'commerce-sdk-isomorphic' import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' +import {ShopperLoginMutations as mutations} from './mutation' import * as queries from './query' describe('Shopper Login hooks', () => { test('all endpoints have hooks', () => { - const unimplemented = getUnimplementedEndpoints(ShopperLogin, queries, cacheUpdateMatrix) + // unimplemented = SDK method exists, but no query hook or value in mutations enum + const unimplemented = getUnimplementedEndpoints(ShopperLogin, 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([ + // These endpoints all return data in the response headers, rather than body, so they + // don't work well with the current implementation of mutation hooks. 'authenticateCustomer', - 'logoutCustomer', // TODO: Implement 'authorizeCustomer', 'getTrustedAgentAuthorizationToken', 'getPasswordResetToken' ]) }) + test('all mutations have cache update logic', () => { + // unimplemented = value in mutations enum, but no method in cache update matrix + const unimplemented = new Set(Object.values(mutations)) + Object.entries(cacheUpdateMatrix).forEach(([method, implementation]) => { + if (implementation) unimplemented.delete(method) + }) + // If this test fails: add cache update logic, remove the endpoint from the mutations enum, + // or add it to the `expected` array to indicate that it is still a TODO. + expect([...unimplemented]).toEqual(['logoutCustomer']) + }) }) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts index f57795fd15..583f33dd8e 100644 --- a/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts +++ b/packages/commerce-sdk-react/src/hooks/ShopperOrders/index.test.ts @@ -7,12 +7,26 @@ import {ShopperOrders} from 'commerce-sdk-isomorphic' import {getUnimplementedEndpoints} from '../../test-utils' import {cacheUpdateMatrix} from './cache' +import {ShopperOrdersMutations as mutations} from './mutation' import * as queries from './query' describe('Shopper Orders hooks', () => { test('all endpoints have hooks', () => { - const unimplemented = getUnimplementedEndpoints(ShopperOrders, queries, cacheUpdateMatrix) - expect(unimplemented).toEqual([ + // unimplemented = SDK method exists, but no query hook or value in mutations enum + 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([]) + }) + test('all mutations have cache update logic', () => { + // unimplemented = value in mutations enum, but no method in cache update matrix + const unimplemented = new Set(Object.values(mutations)) + Object.entries(cacheUpdateMatrix).forEach(([method, implementation]) => { + if (implementation) unimplemented.delete(method) + }) + // If this test fails: add cache update logic, remove the endpoint from the mutations enum, + // or add it to the `expected` array to indicate that it is still a TODO. + expect([...unimplemented]).toEqual([ 'createPaymentInstrumentForOrder', 'removePaymentInstrumentFromOrder', 'updatePaymentInstrumentForOrder' diff --git a/packages/commerce-sdk-react/src/test-utils.tsx b/packages/commerce-sdk-react/src/test-utils.tsx index cc623800a2..a14edce6f2 100644 --- a/packages/commerce-sdk-react/src/test-utils.tsx +++ b/packages/commerce-sdk-react/src/test-utils.tsx @@ -166,15 +166,12 @@ const getQueryName = (method: string): string => { export const getUnimplementedEndpoints = ( SdkClass: {prototype: object}, queryHooks: object, - mutationCacheUpdates: object = {} + mutations: object = {} ) => { const unimplemented = new Set(Object.getOwnPropertyNames(SdkClass.prototype)) // Always present on a class; we can ignore unimplemented.delete('constructor') - // Mutations endpoints are implemented if they have a defined cache update function - Object.entries(mutationCacheUpdates).forEach(([method, implementation]) => { - if (implementation) unimplemented.delete(method) - }) + Object.values(mutations).forEach((method) => unimplemented.delete(method)) // Names of implemented query endpoints have been mangled when converted into hooks unimplemented.forEach((method) => { const queryName = getQueryName(method)