Skip to content

Commit

Permalink
Merge simplified hook signatures (#1024)
Browse files Browse the repository at this point in the history
* Simplify hooks logic (#959)

* Merge hooks changes from wjh/simplify-hooks

* lint fixes

* Use correct type.

* Remove unnecessary file.

* Add support for updater in cache updates.

* Sort imports.

* Change `updater` generic from being the same for all to being per function.

* Clean up cache update matrix.

* Rekerjigger cache logic to see what we can do with it.

* Create options merge util.

* Use merged options in query hooks and simplify query keys.

* Simplify endMatches logic.

* Add predicate helper to match API config values.

* Fix temp test file.

* Use consistent order of generic parameters.

* Remove pointless line.

* Use consistent generic parameter names.

* Add comments.

* More comments.

* Remove test directory.

* Generate hooks for all APIs

* Update Shopper Baskets config from Test directory.

* Update Shopper Experience query hooks.

* Fix missed merged options type.

* Add Shopper Login hooks.

* Convert getGiftCertificate from mutation hook to query hook.

* Update Shopper Login hooks.

Change logoutCustomer from query to mutation.
Change useRetrieveCredQualityUserInfo to getCredQualityUserInfo.

* First pass at converting to new cache update matrix.

* Remove canceled Shopper Discovery Search API.

* Update login helpers.

* Fix auth types.

* Hopefully avoid caching getGiftCertificate.

* Rename shopper login helper to auth helper.

* Move auth helper to hooks root.

* Mark useAuthorizationHeader as internal.

* Rename useAuth to useAuthContext for clarity w/ useAuthHelper.

* Convert useAuthHelper to default export for consistency.

* Remove siteId from AuthData, as it is passed via config.

* Update comment with generated code.

* Move headers to last parameter because we rarely use it.

* Update cache update matrices to new interface.

* Refactore test project after api changes.

* Clean up cache update logic.

Hopefully makes it much easier to maintain.

* Update names for clarity.

* Revert test file to version from develop.

* Add reminder to revert.

* Remove unnecessary default export.

* Cache update TODOs should return functions, not throw.

* Allow user to override authorization header.

* Remove old TODO.

* 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.

* Replace getQueryKey callback with just declaring query key.

* Clean up types.

* Rename config.ts to cache.ts

* Restore type checking test files.

* Suppress react query error logs during tests.

* Strip unused parameters before generating query key.

* Fix tpe errors.

* Partially fix type errors.

* Update Shopper Baskets cache logic to use narrower parameters.

* Test that all endpoints have hooks.

* Create new mutation tests for refactored code.

* Add test to validate all mutations have cache update logic.

* Alphabetize cache updates.

* Simplify mutation tests a bit.

* Fix nock adding duplicate request mocks.

* Make failing tests have more helpful failures.

Show the hook's error instead of "hook timed out" or nock mismatch.

* Fix tests failing due to missing parameters.

* Replace old mutation test file with new one.

* Remove unused helper type.

* Create hook success/error helpers.

* Improve parameter matching.

* Add `deleteBasket` tests.

* Add request mock to fix test.

* Add TODO cache update logic for missing mutations.

* 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.

* Add test for not implemented mutations.

* Pass not implemented tests.

* Remove unused import.

* Implement `resetPassword` caching as a no-op.

* Add Shopper Baskets query tests.

* Add "all endpoints have hooks" tests for all APIs.

* Fix failing index tests.

* Implement tests for all query hooks.

* 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.)

* Update "not implemented" tests to check if cache update logic exists.

* Extract reused type into type def.

* Update comment to reflect changed tests.

* Remove unnecessary `async`.

* Implement Shopper Contexts mutation tests.

* Rename `makeOptions` to `createOptions`.

* Implement Shopper Login mutation tests.

* Add extra ResponseError assertion.

* Update test names.

* Implement Shopper Orders mutation tests.

* Convert TODO from throwing to just logging.

* Fix failing "all endpoints have hooks" tests.

* Remove unused imports.

* Introduce query key helpers.

* Remove no longer needed query test file.

All query hooks are now ttested in their respective folders.

* Update hook usage.

* Implement basic Shopper Customers mutation tests.

* Export query key types.

* Remove old file.

* Update cache logic to use query keys instead of predicates.

* Update parser for proper TypeScript support.

babel-eslint is deprecated and superseded by @babel/eslint-parser.

* Remove unused babel-eslint.

* Temporarily(?) use internal-lib-build so TypeScript files are properly linted.

* Fix eslint and tsc errors.

* Temporarily suppress linter errors.

* Implement tests for shopper customers mutations that modify a customer.

* Implement full Shopper Customers mutation tests.

* Rename `StringIndexNever` to `StringIndexToNever`

* Remove unnecessary invalidation.

The query is already updated, so doesn't need to be invalidated.

* Update generated comments for hooks.

* Remove unused variable.

* 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.

* Remove deprecated Shopper Customers mutations.

* Comment out Shopper Customers endpoints in closed beta.

* Fix Shopper Customers tests.

* Fix Shopper Login tests.

* Update "hook exists" tests to distinguish between "mutation in enum" and "mutation has cache logic"

---------

Co-authored-by: Ben Chypak <bchypak@salesforce.com>

* Fix package-lock.json

* Rename Shopper Login helpers to Auth helpers.

* Update to new hooks signature.

* Restore missing component.

How the heck did it get deleted?

* `enabled` must be a boolean.

* Restore missing imports.

* Implement `updateCustomerPassword` as no-op.

* Restore missing export.

* Restore missing prop types.

* Restore missing template names.

* Temporarily bump bundle size limit.

Re-visit after hooks work is completed.

* Update merged changes to use new format.

---------

Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com>
Co-authored-by: Ben Chypak <bchypak@salesforce.com>
Co-authored-by: Will Harney <wharney@salesforce.com>
  • Loading branch information
4 people authored Mar 3, 2023
1 parent a963c96 commit fb5eded
Show file tree
Hide file tree
Showing 136 changed files with 30,558 additions and 27,901 deletions.
23 changes: 16 additions & 7 deletions packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<AuthData, 'refresh_token'> & {refresh_token_guest?: string}

const config = {
clientId: 'clientId',
organizationId: 'organizationId',
Expand Down Expand Up @@ -85,7 +88,7 @@ describe('Auth', () => {
test('this.data returns the storage value', () => {
const auth = new Auth(config)

const sample = {
const sample: StoredAuthData = {
refresh_token_guest: 'refresh_token_guest',
access_token: 'access_token',
customer_id: 'customer_id',
Expand All @@ -97,7 +100,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
Expand Down Expand Up @@ -161,7 +166,7 @@ describe('Auth', () => {
test('ready - re-use valid access token', () => {
const auth = new Auth(config)

const data = {
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',
Expand All @@ -173,7 +178,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
Expand All @@ -192,7 +199,7 @@ describe('Auth', () => {
test('ready - use refresh token when access token is expired', async () => {
const auth = new Auth(config)

const data = {
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',
Expand All @@ -204,7 +211,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
Expand Down
77 changes: 40 additions & 37 deletions packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -30,24 +31,25 @@ 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
*/
export type AuthData = Prettify<
RemoveStringIndex<TokenResponse> & {
customer_type: CustomerType
idp_access_token: string
}
>

/** 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<keyof AuthData, 'refresh_token'>
| 'refresh_token_guest'
| 'refresh_token_registered'
| 'token_type'
| 'usid'
| 'site_id'
| 'customer_type'

type AuthDataMap = Record<
AuthDataKeys,
{
Expand All @@ -57,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"
Expand Down Expand Up @@ -119,10 +111,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'
Expand All @@ -141,7 +129,7 @@ class Auth {
private client: ShopperLogin<ApiClientConfigParams>
private shopperCustomersClient: ShopperCustomers<ApiClientConfigParams>
private redirectURI: string
private pendingToken: Promise<ShopperLoginTypes.TokenResponse> | undefined
private pendingToken: Promise<TokenResponse> | undefined
private REFRESH_TOKEN_EXPIRATION_DAYS = 90
private stores: Record<StorageType, BaseStorage>
private fetchedToken: string
Expand Down Expand Up @@ -208,9 +196,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)
})
Expand Down Expand Up @@ -248,7 +237,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)
Expand All @@ -272,7 +261,7 @@ class Auth {
*
* @Internal
*/
async queueRequest(fn: () => Promise<ShopperLoginTypes.TokenResponse>, isGuest: boolean) {
async queueRequest(fn: () => Promise<TokenResponse>, isGuest: boolean) {
const queue = this.pendingToken ?? Promise.resolve()
this.pendingToken = queue.then(async () => {
const token = await fn()
Expand Down Expand Up @@ -331,6 +320,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<Args extends unknown[], Data>(
fn: (...args: Args) => Promise<Data>
): (...args: Args) => Promise<Data> {
return async (...args) => {
await this.ready()
return await fn(...args)
}
}

/**
* A wrapper method for commerce-sdk-isomorphic helper: loginGuestUser.
*
Expand Down
Loading

0 comments on commit fb5eded

Please sign in to comment.