From f5bcc3a082e5609b094c1508c434973165245472 Mon Sep 17 00:00:00 2001 From: ManojNB Date: Tue, 10 Oct 2023 10:54:10 -0700 Subject: [PATCH 01/22] feat(inApp): functional dispatchEvent & setConflictHandler APIs (#12231) * chore: truncate comments and typo --------- Co-authored-by: Aaron S <94858815+stocaaro@users.noreply.github.com> Co-authored-by: Jim Blanchard --- .../aws-amplify/__tests__/exports.test.ts | 4 + packages/core/src/Platform/types.ts | 1 + packages/notifications/__mocks__/data.ts | 10 +- .../pinpoint/apis/dispatchEvent.test.ts | 76 ++++ .../pinpoint/apis/setConflictHandler.test.ts | 54 +++ .../utils/processInAppMessages.test.ts | 59 +++ .../notifications/src/inAppMessaging/index.ts | 7 +- .../src/inAppMessaging/providers/index.ts | 7 +- .../providers/pinpoint/apis/dispatchEvent.ts | 59 +++ .../providers/pinpoint/apis/index.ts | 2 + .../pinpoint/apis/setConflictHandler.ts | 66 ++++ .../providers/pinpoint/index.ts | 7 +- .../providers/pinpoint/types/index.ts | 14 +- .../providers/pinpoint/types/inputs.ts | 17 +- .../providers/pinpoint/types/types.ts | 8 +- .../providers/pinpoint/utils/helpers.ts | 340 ++++++++++++++++++ .../providers/pinpoint/utils/index.ts | 2 + .../pinpoint/utils/processInAppMessages.ts | 144 ++++++++ .../src/inAppMessaging/types/index.ts | 8 +- 19 files changed, 874 insertions(+), 11 deletions(-) create mode 100644 packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts create mode 100644 packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts create mode 100644 packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/utils/processInAppMessages.ts diff --git a/packages/aws-amplify/__tests__/exports.test.ts b/packages/aws-amplify/__tests__/exports.test.ts index 982ac31b131..28740921bc9 100644 --- a/packages/aws-amplify/__tests__/exports.test.ts +++ b/packages/aws-amplify/__tests__/exports.test.ts @@ -102,6 +102,8 @@ describe('aws-amplify Exports', () => { Array [ "identifyUser", "syncMessages", + "dispatchEvent", + "setConflictHandler", ] `); }); @@ -112,6 +114,8 @@ describe('aws-amplify Exports', () => { Array [ "identifyUser", "syncMessages", + "dispatchEvent", + "setConflictHandler", ] `); }); diff --git a/packages/core/src/Platform/types.ts b/packages/core/src/Platform/types.ts index 003c9018838..8536db6c01e 100644 --- a/packages/core/src/Platform/types.ts +++ b/packages/core/src/Platform/types.ts @@ -88,6 +88,7 @@ export enum GeoAction { export enum InAppMessagingAction { SyncMessages = '1', IdentifyUser = '2', + DispatchEvent = '3', } export enum InteractionsAction { None = '0', diff --git a/packages/notifications/__mocks__/data.ts b/packages/notifications/__mocks__/data.ts index a4afbef9c62..51077ce0a53 100644 --- a/packages/notifications/__mocks__/data.ts +++ b/packages/notifications/__mocks__/data.ts @@ -172,16 +172,20 @@ export const pinpointInAppMessage: PinpointInAppMessage = { }, Priority: 3, Schedule: { - EndDate: '2021-01-01T00:00:00Z', + EndDate: '2024-01-01T00:00:00Z', EventFilter: { FilterType: 'SYSTEM', Dimensions: { - Attributes: {}, + Attributes: { + interests: { Values: ['test-interest'] }, + }, EventType: { DimensionType: 'INCLUSIVE', Values: ['clicked', 'swiped'], }, - Metrics: {}, + Metrics: { + clicks: { ComparisonOperator: 'EQUAL', Value: 5 }, + }, }, }, QuietTime: { diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts new file mode 100644 index 00000000000..d7f7c3a5e4f --- /dev/null +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts @@ -0,0 +1,76 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defaultStorage } from '@aws-amplify/core'; +import { dispatchEvent } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import { processInAppMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; +import { + inAppMessages, + simpleInAppMessages, + simpleInAppMessagingEvent, +} from '../../../../../__mocks__/data'; +import { InAppMessagingError } from '../../../../../src/inAppMessaging/errors'; +import { notifyEventListeners } from '../../../../../src/common/eventListeners'; + +jest.mock('@aws-amplify/core'); +jest.mock('@aws-amplify/core/internals/utils'); +jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); +jest.mock('../../../../../src/common/eventListeners'); + +const mockDefaultStorage = defaultStorage as jest.Mocked; +const mockNotifyEventListeners = notifyEventListeners as jest.Mock; +const mockProcessInAppMessages = processInAppMessages as jest.Mock; + +describe('dispatchEvent', () => { + beforeEach(() => { + mockDefaultStorage.setItem.mockClear(); + mockNotifyEventListeners.mockClear(); + }); + test('gets in-app messages from store and notifies listeners', async () => { + const [message] = inAppMessages; + mockDefaultStorage.getItem.mockResolvedValueOnce( + JSON.stringify(simpleInAppMessages) + ); + mockProcessInAppMessages.mockReturnValueOnce([message]); + await dispatchEvent(simpleInAppMessagingEvent); + expect(mockProcessInAppMessages).toBeCalledWith( + simpleInAppMessages, + simpleInAppMessagingEvent + ); + expect(mockNotifyEventListeners).toBeCalledWith('messageReceived', message); + }); + + test('handles conflicts through default conflict handler', async () => { + mockDefaultStorage.getItem.mockResolvedValueOnce( + JSON.stringify(simpleInAppMessages) + ); + mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); + await dispatchEvent(simpleInAppMessagingEvent); + expect(mockProcessInAppMessages).toBeCalledWith( + simpleInAppMessages, + simpleInAppMessagingEvent + ); + expect(mockNotifyEventListeners).toBeCalledWith( + 'messageReceived', + inAppMessages[4] + ); + }); + + test('does not notify listeners if no messages are returned', async () => { + mockProcessInAppMessages.mockReturnValueOnce([]); + mockDefaultStorage.getItem.mockResolvedValueOnce( + JSON.stringify(simpleInAppMessages) + ); + + await dispatchEvent(simpleInAppMessagingEvent); + + expect(mockNotifyEventListeners).not.toBeCalled(); + }); + + test('logs error if storage retrieval fails', async () => { + mockDefaultStorage.getItem.mockRejectedValueOnce(Error); + await expect( + dispatchEvent(simpleInAppMessagingEvent) + ).rejects.toStrictEqual(expect.any(InAppMessagingError)); + }); +}); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts new file mode 100644 index 00000000000..b8bae764502 --- /dev/null +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defaultStorage } from '@aws-amplify/core'; +import { + dispatchEvent, + setConflictHandler, +} from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import { processInAppMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; +import { + closestExpiryMessage, + customHandledMessage, + inAppMessages, + simpleInAppMessagingEvent, +} from '../../../../../__mocks__/data'; +import { notifyEventListeners } from '../../../../../src/common/eventListeners'; + +jest.mock('@aws-amplify/core'); +jest.mock('@aws-amplify/core/internals/utils'); +jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); +jest.mock('../../../../../src/common/eventListeners'); + +const mockDefaultStorage = defaultStorage as jest.Mocked; +const mockNotifyEventListeners = notifyEventListeners as jest.Mock; +const mockProcessInAppMessages = processInAppMessages as jest.Mock; + +describe('Conflict handling', () => { + beforeEach(() => { + mockDefaultStorage.setItem.mockClear(); + mockNotifyEventListeners.mockClear(); + }); + test('has a default implementation', async () => { + mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); + await dispatchEvent(simpleInAppMessagingEvent); + expect(mockNotifyEventListeners).toBeCalledWith( + 'messageReceived', + closestExpiryMessage + ); + }); + + test('can be customized through setConflictHandler', async () => { + const customConflictHandler = messages => + messages.find(message => message.id === 'custom-handled'); + mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); + + setConflictHandler(customConflictHandler); + await dispatchEvent(simpleInAppMessagingEvent); + + expect(mockNotifyEventListeners).toBeCalledWith( + 'messageReceived', + customHandledMessage + ); + }); +}); diff --git a/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts b/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts new file mode 100644 index 00000000000..7d43a650050 --- /dev/null +++ b/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + pinpointInAppMessage, + simpleInAppMessagingEvent, +} from '../../../__mocks__/data'; +import { processInAppMessages } from '../../../src/inAppMessaging/providers/pinpoint/utils/processInAppMessages'; +import { cloneDeep } from 'lodash'; +import { + isBeforeEndDate, + matchesAttributes, + matchesEventType, + matchesMetrics, +} from '../../../src/inAppMessaging/providers/pinpoint/utils/helpers'; + +jest.mock('@aws-amplify/core'); +jest.mock('@aws-amplify/core/internals/utils'); +jest.mock('../../../src/inAppMessaging/providers/pinpoint/utils/helpers'); + +const mockIsBeforeEndDate = isBeforeEndDate as jest.Mock; +const mockMatchesAttributes = matchesAttributes as jest.Mock; +const mockMatchesEventType = matchesEventType as jest.Mock; +const mockMatchesMetrics = matchesMetrics as jest.Mock; + +// TODO(V6): Add tests for session cap etc +describe('processInAppMessages', () => { + const messages = [ + cloneDeep(pinpointInAppMessage), + { ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-2', Priority: 3 }, + { ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-3', Priority: 1 }, + { ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-4', Priority: 2 }, + ]; + beforeEach(() => { + mockMatchesEventType.mockReturnValue(true); + mockMatchesAttributes.mockReturnValue(true); + mockMatchesMetrics.mockReturnValue(true); + mockIsBeforeEndDate.mockReturnValue(true); + }); + + test('filters in-app messages from Pinpoint by criteria', async () => { + mockMatchesEventType.mockReturnValueOnce(false); + mockMatchesAttributes.mockReturnValueOnce(false); + mockMatchesMetrics.mockReturnValueOnce(false); + const [result] = await processInAppMessages( + messages, + simpleInAppMessagingEvent + ); + expect(result.id).toBe('uuid-4'); + }); + + test('filters in-app messages from Pinpoint by criteria', async () => { + const [result] = await processInAppMessages( + messages, + simpleInAppMessagingEvent + ); + expect(result.id).toBe('uuid-3'); + }); +}); diff --git a/packages/notifications/src/inAppMessaging/index.ts b/packages/notifications/src/inAppMessaging/index.ts index 9152b0d2973..5bfbae5da51 100644 --- a/packages/notifications/src/inAppMessaging/index.ts +++ b/packages/notifications/src/inAppMessaging/index.ts @@ -1,4 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { identifyUser, syncMessages } from './providers/pinpoint'; +export { + identifyUser, + syncMessages, + dispatchEvent, + setConflictHandler, +} from './providers/pinpoint'; diff --git a/packages/notifications/src/inAppMessaging/providers/index.ts b/packages/notifications/src/inAppMessaging/providers/index.ts index 54b4514593e..51aec634a0c 100644 --- a/packages/notifications/src/inAppMessaging/providers/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/index.ts @@ -1,4 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { identifyUser, syncMessages } from './pinpoint/apis'; +export { + identifyUser, + syncMessages, + dispatchEvent, + setConflictHandler, +} from './pinpoint/apis'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts new file mode 100644 index 00000000000..212e8548139 --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + PINPOINT_KEY_PREFIX, + STORAGE_KEY_SUFFIX, + processInAppMessages, +} from '../utils'; +import { InAppMessage } from '../../../types'; +import flatten from 'lodash/flatten'; +import { defaultStorage } from '@aws-amplify/core'; +import { notifyEventListeners } from '../../../../common'; +import { assertServiceError } from '../../../errors'; +import { DispatchEventInput } from '../types'; +import { syncMessages } from './syncMessages'; +import { conflictHandler, setConflictHandler } from './setConflictHandler'; + +/** + * Triggers an In-App message to be displayed. Use this after your campaigns have been synced to the device using + * {@link syncMessages}. Based on the messages synced and the event passed to this API, it triggers the display + * of the In-App message that meets the criteria. + * To change the conflict handler, use the {@link setConflictHandler} API. + * + * @param DispatchEventInput The input object that holds the event to be dispatched. + * + * @throws service exceptions - Thrown when the underlying Pinpoint service returns an error. + * + * @returns A promise that will resolve when the operation is complete. + * + * @example + * ```ts + * // Sync message before disptaching an event + * await syncMessages(); + * + * // Dispatch an event + * await dispatchEvent({ name: "test_event" }); + * ``` + */ +export async function dispatchEvent(input: DispatchEventInput): Promise { + try { + const key = `${PINPOINT_KEY_PREFIX}${STORAGE_KEY_SUFFIX}`; + const cachedMessages = await defaultStorage.getItem(key); + const messages: InAppMessage[] = await processInAppMessages( + cachedMessages ? JSON.parse(cachedMessages) : [], + input + ); + const flattenedMessages = flatten(messages); + + if (flattenedMessages.length > 0) { + notifyEventListeners( + 'messageReceived', + conflictHandler(flattenedMessages) + ); + } + } catch (error) { + assertServiceError(error); + throw error; + } +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts index b2ca836fa33..d25f562d165 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts @@ -3,3 +3,5 @@ export { identifyUser } from './identifyUser'; export { syncMessages } from './syncMessages'; +export { dispatchEvent } from './dispatchEvent'; +export { setConflictHandler } from './setConflictHandler'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts new file mode 100644 index 00000000000..052450551ff --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { InAppMessage } from '../../../types'; +import { InAppMessageConflictHandler, SetConflictHandlerInput } from '../types'; + +export let conflictHandler: InAppMessageConflictHandler = + defaultConflictHandler; + +/** + * Set a conflict handler that will be used to resolve conflicts that may emerge + * when matching events with synced messages. + * @remark + * The conflict handler is not persisted between sessions + * and needs to be called before dispatching an event to have any effect. + * + * @param SetConflictHandlerInput: The input object that holds the conflict handler to be used. + * + * + * @example + * ```ts + * // Sync messages before dispatching an event + * await syncMessages(); + * + * // Example custom conflict handler + * const myConflictHandler = (messages) => { + * // Return a random message + * const randomIndex = Math.floor(Math.random() * messages.length); + * return messages[randomIndex]; + * }; + * + * // Set the conflict handler + * setConflictHandler(myConflictHandler); + * + * // Dispatch an event + * await dispatchEvent({ name: "test_event" }); + * ``` + */ +export function setConflictHandler(input: SetConflictHandlerInput): void { + conflictHandler = input; +} + +function defaultConflictHandler(messages: InAppMessage[]): InAppMessage { + // default behavior is to return the message closest to expiry + // this function assumes that messages processed by providers already filters out expired messages + const sorted = messages.sort((a, b) => { + const endDateA = a.metadata?.endDate; + const endDateB = b.metadata?.endDate; + // if both message end dates are falsy or have the same date string, treat them as equal + if (endDateA === endDateB) { + return 0; + } + // if only message A has an end date, treat it as closer to expiry + if (endDateA && !endDateB) { + return -1; + } + // if only message B has an end date, treat it as closer to expiry + if (!endDateA && endDateB) { + return 1; + } + // otherwise, compare them + return new Date(endDateA) < new Date(endDateB) ? -1 : 1; + }); + // always return the top sorted + return sorted[0]; +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts index 01e1384253c..970ae16eb7b 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts @@ -1,4 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { identifyUser, syncMessages } from './apis'; +export { + identifyUser, + syncMessages, + dispatchEvent, + setConflictHandler, +} from './apis'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts index fc8965c66b8..80d6de78c80 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts @@ -2,5 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 export { UpdateEndpointException } from './errors'; -export { IdentifyUserInput } from './inputs'; +export { + IdentifyUserInput, + DispatchEventInput, + SetConflictHandlerInput, +} from './inputs'; export { IdentifyUserOptions } from './options'; +export { + PinpointMessageEvent, + MetricsComparator, + InAppMessageCounts, + InAppMessageCountMap, + DailyInAppMessageCounter, + InAppMessageConflictHandler, +} from './types'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts index f103f97b69e..f2edad9b6dc 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts @@ -1,11 +1,24 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { IdentifyUserOptions } from '.'; -import { InAppMessagingIdentifyUserInput } from '../../../types'; +import { IdentifyUserOptions, InAppMessageConflictHandler } from '.'; +import { + InAppMessagingEvent, + InAppMessagingIdentifyUserInput, +} from '../../../types'; /** * Input type for Pinpoint identifyUser API. */ export type IdentifyUserInput = InAppMessagingIdentifyUserInput; + +/** + * Input type for Pinpoint dispatchEvent API. + */ +export type DispatchEventInput = InAppMessagingEvent; + +/** + * Input type for Pinpoint SetConflictHandler API. + */ +export type SetConflictHandlerInput = InAppMessageConflictHandler; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts index 68022a84e83..6c964507666 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { InAppMessage } from '../../../types'; + export type InAppMessageCountMap = Record; export type DailyInAppMessageCounter = { @@ -19,8 +21,12 @@ export type MetricsComparator = ( eventVal: number ) => boolean; -export enum AWSPinpointMessageEvent { +export enum PinpointMessageEvent { MESSAGE_DISPLAYED = '_inapp.message_displayed', MESSAGE_DISMISSED = '_inapp.message_dismissed', MESSAGE_ACTION_TAKEN = '_inapp.message_clicked', } + +export type InAppMessageConflictHandler = ( + messages: InAppMessage[] +) => InAppMessage; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts new file mode 100644 index 00000000000..58a625d41bd --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts @@ -0,0 +1,340 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Hub } from '@aws-amplify/core'; +import { + ConsoleLogger, + InAppMessagingAction, + AMPLIFY_SYMBOL, +} from '@aws-amplify/core/internals/utils'; +import type { InAppMessageCampaign as PinpointInAppMessage } from '@aws-amplify/core/internals/aws-clients/pinpoint'; +import isEmpty from 'lodash/isEmpty'; +import { + InAppMessage, + InAppMessageAction, + InAppMessageContent, + InAppMessageLayout, + InAppMessageTextAlign, + InAppMessagingEvent, +} from '../../../types'; +import { MetricsComparator, PinpointMessageEvent } from '../types'; +import { record as recordCore } from '@aws-amplify/core/internals/providers/pinpoint'; +import { resolveConfig } from './resolveConfig'; +import { resolveCredentials } from './resolveCredentials'; +import { CATEGORY } from './constants'; +import { getInAppMessagingUserAgentString } from './userAgent'; + +const DELIVERY_TYPE = 'IN_APP_MESSAGE'; + +let eventNameMemo = {}; +let eventAttributesMemo = {}; +let eventMetricsMemo = {}; + +export const logger = new ConsoleLogger('InAppMessaging.Pinpoint'); + +export const dispatchInAppMessagingEvent = ( + event: string, + data: any, + message?: string +) => { + Hub.dispatch( + 'inAppMessaging', + { event, data, message }, + 'InAppMessaging', + AMPLIFY_SYMBOL + ); +}; + +export const recordAnalyticsEvent = ( + event: PinpointMessageEvent, + message: InAppMessage +) => { + const { appId, region } = resolveConfig(); + + const { id, metadata } = message; + resolveCredentials() + .then(({ credentials, identityId }) => { + recordCore({ + appId, + category: CATEGORY, + credentials, + event: { + name: event, + attributes: { + campaign_id: id, + delivery_type: DELIVERY_TYPE, + treatment_id: metadata?.treatmentId, + }, + }, + identityId, + region, + userAgentValue: getInAppMessagingUserAgentString( + InAppMessagingAction.DispatchEvent + ), + }); + }) + .catch(e => { + // An error occured while fetching credentials or persisting the event to the buffer + logger.warn('Failed to record event.', e); + }); +}; + +export const getStartOfDay = (): string => { + const now = new Date(); + now.setHours(0, 0, 0, 0); + return now.toISOString(); +}; + +export const matchesEventType = ( + { CampaignId, Schedule }: PinpointInAppMessage, + { name: eventType }: InAppMessagingEvent +) => { + const { EventType } = Schedule?.EventFilter?.Dimensions; + const memoKey = `${CampaignId}:${eventType}`; + if (!eventNameMemo.hasOwnProperty(memoKey)) { + eventNameMemo[memoKey] = !!EventType?.Values.includes(eventType); + } + return eventNameMemo[memoKey]; +}; + +export const matchesAttributes = ( + { CampaignId, Schedule }: PinpointInAppMessage, + { attributes }: InAppMessagingEvent +): boolean => { + const { Attributes } = Schedule?.EventFilter?.Dimensions; + if (isEmpty(Attributes)) { + // if message does not have attributes defined it does not matter what attributes are on the event + return true; + } + if (isEmpty(attributes)) { + // if message does have attributes but the event does not then it always fails the check + return false; + } + const memoKey = `${CampaignId}:${JSON.stringify(attributes)}`; + if (!eventAttributesMemo.hasOwnProperty(memoKey)) { + eventAttributesMemo[memoKey] = Object.entries(Attributes).every( + ([key, { Values }]) => Values.includes(attributes[key]) + ); + } + return eventAttributesMemo[memoKey]; +}; + +export const matchesMetrics = ( + { CampaignId, Schedule }: PinpointInAppMessage, + { metrics }: InAppMessagingEvent +): boolean => { + const { Metrics } = Schedule?.EventFilter?.Dimensions; + if (isEmpty(Metrics)) { + // if message does not have metrics defined it does not matter what metrics are on the event + return true; + } + if (isEmpty(metrics)) { + // if message does have metrics but the event does not then it always fails the check + return false; + } + const memoKey = `${CampaignId}:${JSON.stringify(metrics)}`; + if (!eventMetricsMemo.hasOwnProperty(memoKey)) { + eventMetricsMemo[memoKey] = Object.entries(Metrics).every( + ([key, { ComparisonOperator, Value }]) => { + const compare = getComparator(ComparisonOperator); + // if there is some unknown comparison operator, treat as a comparison failure + return compare ? compare(Value, metrics[key]) : false; + } + ); + } + return eventMetricsMemo[memoKey]; +}; + +export const getComparator = (operator: string): MetricsComparator => { + switch (operator) { + case 'EQUAL': + return (metricsVal, eventVal) => metricsVal === eventVal; + case 'GREATER_THAN': + return (metricsVal, eventVal) => metricsVal < eventVal; + case 'GREATER_THAN_OR_EQUAL': + return (metricsVal, eventVal) => metricsVal <= eventVal; + case 'LESS_THAN': + return (metricsVal, eventVal) => metricsVal > eventVal; + case 'LESS_THAN_OR_EQUAL': + return (metricsVal, eventVal) => metricsVal >= eventVal; + default: + return null; + } +}; + +export const isBeforeEndDate = ({ + Schedule, +}: PinpointInAppMessage): boolean => { + if (!Schedule?.EndDate) { + return true; + } + return new Date() < new Date(Schedule.EndDate); +}; + +export const isQuietTime = (message: PinpointInAppMessage): boolean => { + const { Schedule } = message; + if (!Schedule?.QuietTime) { + return false; + } + + const pattern = /^[0-2]\d:[0-5]\d$/; // basic sanity check, not a fully featured HH:MM validation + const { Start, End } = Schedule.QuietTime; + if ( + !Start || + !End || + Start === End || + !pattern.test(Start) || + !pattern.test(End) + ) { + return false; + } + + const now = new Date(); + const start = new Date(now); + const end = new Date(now); + const [startHours, startMinutes] = Start.split(':'); + const [endHours, endMinutes] = End.split(':'); + + start.setHours( + Number.parseInt(startHours, 10), + Number.parseInt(startMinutes, 10), + 0, + 0 + ); + end.setHours( + Number.parseInt(endHours, 10), + Number.parseInt(endMinutes, 10), + 0, + 0 + ); + + // if quiet time includes midnight, bump the end time to the next day + if (start > end) { + end.setDate(end.getDate() + 1); + } + + const isQuietTime = now >= start && now <= end; + if (isQuietTime) { + logger.debug('message filtered due to quiet time', message); + } + return isQuietTime; +}; + +export const clearMemo = () => { + eventNameMemo = {}; + eventAttributesMemo = {}; + eventMetricsMemo = {}; +}; + +// in the pinpoint console when a message is created with a Modal or Full Screen layout, +// it is assigned a layout value of MOBILE_FEED or OVERLAYS respectively in the message payload. +// In the future, Pinpoint will be updating the layout values in the aforementioned scenario +// to MODAL and FULL_SCREEN. +// +// This utility acts as a safeguard to ensure that: +// - 1. the usage of MOBILE_FEED and OVERLAYS as values for message layouts are not leaked +// outside the Pinpoint provider +// - 2. Amplify correctly handles the legacy layout values from Pinpoint after they are updated +export const interpretLayout = ( + layout: PinpointInAppMessage['InAppMessage']['Layout'] +): InAppMessageLayout => { + if (layout === 'MOBILE_FEED') { + return 'MODAL'; + } + + if (layout === 'OVERLAYS') { + return 'FULL_SCREEN'; + } + + // cast as PinpointInAppMessage['InAppMessage']['Layout'] allows `string` as a value + return layout as InAppMessageLayout; +}; + +export const extractContent = ({ + InAppMessage: message, +}: PinpointInAppMessage): InAppMessageContent[] => { + return ( + message?.Content?.map(content => { + const { + BackgroundColor, + BodyConfig, + HeaderConfig, + ImageUrl, + PrimaryBtn, + SecondaryBtn, + } = content; + const defaultPrimaryButton = PrimaryBtn?.DefaultConfig; + const defaultSecondaryButton = SecondaryBtn?.DefaultConfig; + const extractedContent: InAppMessageContent = {}; + if (BackgroundColor) { + extractedContent.container = { + style: { + backgroundColor: BackgroundColor, + }, + }; + } + if (HeaderConfig) { + extractedContent.header = { + content: HeaderConfig.Header, + style: { + color: HeaderConfig.TextColor, + textAlign: + HeaderConfig.Alignment.toLowerCase() as InAppMessageTextAlign, + }, + }; + } + if (BodyConfig) { + extractedContent.body = { + content: BodyConfig.Body, + style: { + color: BodyConfig.TextColor, + textAlign: + BodyConfig.Alignment.toLowerCase() as InAppMessageTextAlign, + }, + }; + } + if (ImageUrl) { + extractedContent.image = { + src: ImageUrl, + }; + } + if (defaultPrimaryButton) { + extractedContent.primaryButton = { + title: defaultPrimaryButton.Text, + action: defaultPrimaryButton.ButtonAction as InAppMessageAction, + url: defaultPrimaryButton.Link, + style: { + backgroundColor: defaultPrimaryButton.BackgroundColor, + borderRadius: defaultPrimaryButton.BorderRadius, + color: defaultPrimaryButton.TextColor, + }, + }; + } + if (defaultSecondaryButton) { + extractedContent.secondaryButton = { + title: defaultSecondaryButton.Text, + action: defaultSecondaryButton.ButtonAction as InAppMessageAction, + url: defaultSecondaryButton.Link, + style: { + backgroundColor: defaultSecondaryButton.BackgroundColor, + borderRadius: defaultSecondaryButton.BorderRadius, + color: defaultSecondaryButton.TextColor, + }, + }; + } + return extractedContent; + }) ?? [] + ); +}; + +export const extractMetadata = ({ + InAppMessage, + Priority, + Schedule, + TreatmentId, +}: PinpointInAppMessage): InAppMessage['metadata'] => ({ + customData: InAppMessage?.CustomConfig, + endDate: Schedule?.EndDate, + priority: Priority, + treatmentId: TreatmentId, +}); diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts index 892e33cb4b7..d27d53e161f 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts @@ -10,3 +10,5 @@ export { CHANNEL_TYPE, STORAGE_KEY_SUFFIX, } from './constants'; + +export { processInAppMessages } from './processInAppMessages'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/processInAppMessages.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/processInAppMessages.ts new file mode 100644 index 00000000000..1207bcbede9 --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/processInAppMessages.ts @@ -0,0 +1,144 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { InAppMessage, InAppMessagingEvent } from '../../../types'; +import { + InAppMessageCounts, + InAppMessageCountMap, + DailyInAppMessageCounter, +} from '../types'; +import { + extractContent, + extractMetadata, + interpretLayout, + isBeforeEndDate, + matchesAttributes, + matchesEventType, + matchesMetrics, +} from './helpers'; +import type { InAppMessageCampaign as PinpointInAppMessage } from '@aws-amplify/core/internals/aws-clients/pinpoint'; +import { ConsoleLogger } from '@aws-amplify/core/internals/utils'; +import { defaultStorage } from '@aws-amplify/core'; + +const MESSAGE_DAILY_COUNT_KEY = 'pinpointProvider_inAppMessages_dailyCount'; +const MESSAGE_TOTAL_COUNT_KEY = 'pinpointProvider_inAppMessages_totalCount'; +const logger = new ConsoleLogger('InAppMessaging.processInAppMessages'); + +const sessionMessageCountMap: InAppMessageCountMap = {}; + +export async function processInAppMessages( + messages: PinpointInAppMessage[], + event: InAppMessagingEvent +): Promise { + let highestPrioritySeen: number; + let acc: PinpointInAppMessage[] = []; + for (let index = 0; index < messages.length; index++) { + const message = messages[index]; + const messageQualifies = + matchesEventType(message, event) && + matchesAttributes(message, event) && + matchesMetrics(message, event) && + isBeforeEndDate(message) && + (await isBelowCap(message)); + // filter all qualifying messages returning only those that are of (relative) highest priority + if (messageQualifies) { + // have not yet encountered message with priority + if (!highestPrioritySeen) { + // this message has priority, so reset the accumulator with this message only + if (message.Priority) { + highestPrioritySeen = message.Priority; + acc = [message]; + } else { + // this message also has no priority, so just add this message to accumulator + acc.push(message); + } + // have previously encountered message with priority, so only messages with priority matter now + } else if (message.Priority) { + // this message has higher priority (lower number), so reset the accumulator with this message only + if (message.Priority < highestPrioritySeen) { + highestPrioritySeen = message.Priority; + acc = [message]; + // this message has the same priority, so just add this message to accumulator + } else if (message.Priority === highestPrioritySeen) { + acc.push(message); + } + } + } + } + return normalizeMessages(acc); +} + +function normalizeMessages(messages: PinpointInAppMessage[]): InAppMessage[] { + return messages.map(message => { + const { CampaignId, InAppMessage } = message; + return { + id: CampaignId, + content: extractContent(message), + layout: interpretLayout(InAppMessage.Layout), + metadata: extractMetadata(message), + }; + }); +} + +async function isBelowCap({ + CampaignId, + SessionCap, + DailyCap, + TotalCap, +}: PinpointInAppMessage): Promise { + const { sessionCount, dailyCount, totalCount } = await getMessageCounts( + CampaignId + ); + return ( + (!SessionCap ?? sessionCount < SessionCap) && + (!DailyCap ?? dailyCount < DailyCap) && + (!TotalCap ?? totalCount < TotalCap) + ); +} + +async function getMessageCounts( + messageId: string +): Promise { + try { + return { + sessionCount: getSessionCount(messageId), + dailyCount: await getDailyCount(), + totalCount: await getTotalCount(messageId), + }; + } catch (err) { + logger.error('Failed to get message counts from storage', err); + } +} + +function getSessionCount(messageId: string): number { + return sessionMessageCountMap[messageId] || 0; +} + +async function getDailyCount(): Promise { + const today = getStartOfDay(); + const item = await defaultStorage.getItem(MESSAGE_DAILY_COUNT_KEY); + // Parse stored count or initialize as empty count + const counter: DailyInAppMessageCounter = item + ? JSON.parse(item) + : { count: 0, lastCountTimestamp: today }; + // If the stored counter timestamp is today, use it as the count, otherwise reset to 0 + return counter.lastCountTimestamp === today ? counter.count : 0; +} + +async function getTotalCountMap(): Promise { + const item = await defaultStorage.getItem(MESSAGE_TOTAL_COUNT_KEY); + // Parse stored count map or initialize as empty + return item ? JSON.parse(item) : {}; +} + +async function getTotalCount(messageId: string): Promise { + const countMap = await getTotalCountMap(); + // Return stored count or initialize as empty count + return countMap[messageId] || 0; +} + +const getStartOfDay = (): string => { + const now = new Date(); + now.setHours(0, 0, 0, 0); + return now.toISOString(); +}; diff --git a/packages/notifications/src/inAppMessaging/types/index.ts b/packages/notifications/src/inAppMessaging/types/index.ts index 9f1f00bb861..51a42ecab97 100644 --- a/packages/notifications/src/inAppMessaging/types/index.ts +++ b/packages/notifications/src/inAppMessaging/types/index.ts @@ -5,4 +5,10 @@ export { InAppMessagingServiceOptions } from './options'; export { InAppMessagingIdentifyUserInput } from './inputs'; export { InAppMessagingConfig } from './config'; export { InAppMessageInteractionEvent, InAppMessagingEvent } from './event'; -export { InAppMessage } from './message'; +export { + InAppMessage, + InAppMessageAction, + InAppMessageContent, + InAppMessageLayout, + InAppMessageTextAlign, +} from './message'; From b117bfa014cc02db42b4746c116102f9c0f28751 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 10 Oct 2023 12:12:00 -0700 Subject: [PATCH 02/22] test(analytics): add integration test config for KDF (#12254) --- .github/integ-config/integ-all.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index 6cbacd46ce4..eb842b2f27a 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -565,6 +565,24 @@ tests: # Temp fix: browser: *minimal_browser_list + - test_name: integ_react_analytics_kinesis_data_firehose_auth + desc: 'Test record API for KDF with authenticated user' + framework: react + category: analytics + sample_name: [kinesis-firehose-test] + spec: kinesis-firehose + # Temp fix: + browser: *minimal_browser_list + + - test_name: integ_react_analytics_kinesis_data_firehose_unauth + desc: 'Test record API for KDF with guest user' + framework: react + category: analytics + sample_name: [kinesis-firehose-test] + spec: kinesis-firehose-unauth + # Temp fix: + browser: *minimal_browser_list + # GEO # - test_name: integ_react_geo_display_map # desc: 'Display Map' From c16bb5dfe3288d5196329aea3462d177f0ff264d Mon Sep 17 00:00:00 2001 From: Aaron S <94858815+stocaaro@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:18:48 -0500 Subject: [PATCH 03/22] chore: Enable api reconnect test (#12234) --- .github/integ-config/integ-all.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index eb842b2f27a..a0623191edb 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -679,14 +679,14 @@ tests: # spec: reconnection # # Firefox doesn't support network state management in cypress # browser: [chrome] - # - test_name: integ_react_api_reconnect - # desc: 'PubSub - Reconnection for API' - # framework: react - # category: pubsub - # sample_name: [reconnection-api] - # spec: reconnection - # # Firefox doesn't support network state management in cypress - # browser: [chrome] + - test_name: integ_react_api_reconnect + desc: 'PubSub - Reconnection for API' + framework: react + category: pubsub + sample_name: [reconnection-api] + spec: reconnection + # Firefox doesn't support network state management in cypress + browser: [chrome] # STORAGE - test_name: integ_react_storage From f8a7145f67d9db12fc52608b4202e08c02613feb Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Tue, 10 Oct 2023 15:20:33 -0700 Subject: [PATCH 04/22] fix(storage): align cancel behavior with api-rest (#12239) * fix(api-rest): only set custom cancel message when supplied * fix(storage): cancel accepts custom message instead of error instances --------- Co-authored-by: Aaron S <94858815+stocaaro@users.noreply.github.com> --- .../api-rest/src/apis/common/internalPost.ts | 2 +- packages/api-rest/src/errors/CanceledError.ts | 30 +++++++++++++++++++ .../api-rest/src/errors/CancelledError.ts | 26 ---------------- packages/api-rest/src/errors/index.ts | 2 +- packages/api-rest/src/index.ts | 2 +- packages/api-rest/src/server.ts | 2 +- packages/api-rest/src/types/index.ts | 2 +- .../src/utils/createCancellableOperation.ts | 11 ++++--- .../s3/apis/utils/downloadTask.test.ts | 6 ++-- .../s3/apis/utils/uploadTask.test.ts | 6 ++-- packages/storage/src/errors/CanceledError.ts | 10 +++++-- .../src/providers/s3/apis/downloadData.ts | 6 ++-- .../src/providers/s3/apis/uploadData/index.ts | 8 ++--- .../uploadData/multipart/uploadHandlers.ts | 16 ++++------ .../client/runtime/xhrTransferHandler.ts | 6 ++-- .../src/providers/s3/utils/transferTask.ts | 14 ++++----- packages/storage/src/types/common.ts | 10 ++++--- 17 files changed, 82 insertions(+), 77 deletions(-) create mode 100644 packages/api-rest/src/errors/CanceledError.ts delete mode 100644 packages/api-rest/src/errors/CancelledError.ts diff --git a/packages/api-rest/src/apis/common/internalPost.ts b/packages/api-rest/src/apis/common/internalPost.ts index dd9d70724c1..187e54184a5 100644 --- a/packages/api-rest/src/apis/common/internalPost.ts +++ b/packages/api-rest/src/apis/common/internalPost.ts @@ -60,7 +60,7 @@ export const cancel = ( const controller = cancelTokenMap.get(promise); if (controller) { controller.abort(message); - if (controller.signal.reason !== message) { + if (message && controller.signal.reason !== message) { // In runtimes where `AbortSignal.reason` is not supported, we track the reason ourselves. // @ts-expect-error reason is read-only property. controller.signal['reason'] = message; diff --git a/packages/api-rest/src/errors/CanceledError.ts b/packages/api-rest/src/errors/CanceledError.ts new file mode 100644 index 00000000000..aa2a89562b6 --- /dev/null +++ b/packages/api-rest/src/errors/CanceledError.ts @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyErrorParams } from '@aws-amplify/core/internals/utils'; +import { RestApiError } from './RestApiError'; + +/** + * Internal-only class for CanceledError. + * + * @internal + */ +export class CanceledError extends RestApiError { + constructor(params: Partial = {}) { + super({ + name: 'CanceledError', + message: 'Request is canceled by user', + ...params, + }); + + // TODO: Delete the following 2 lines after we change the build target to >= es2015 + this.constructor = CanceledError; + Object.setPrototypeOf(this, CanceledError.prototype); + } +} + +/** + * Check if an error is caused by user calling `cancel()` REST API. + */ +export const isCancelError = (error: unknown): error is CanceledError => + !!error && error instanceof CanceledError; diff --git a/packages/api-rest/src/errors/CancelledError.ts b/packages/api-rest/src/errors/CancelledError.ts deleted file mode 100644 index c5c63c7b483..00000000000 --- a/packages/api-rest/src/errors/CancelledError.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AmplifyErrorParams } from '@aws-amplify/core/internals/utils'; -import { RestApiError } from './RestApiError'; - -/** - * Internal-only class for CancelledError. - * - * @internal - */ -export class CancelledError extends RestApiError { - constructor(params: AmplifyErrorParams) { - super(params); - - // TODO: Delete the following 2 lines after we change the build target to >= es2015 - this.constructor = CancelledError; - Object.setPrototypeOf(this, CancelledError.prototype); - } -} - -/** - * Check if an error is caused by user calling `cancel()` REST API. - */ -export const isCancelError = (error: unknown): boolean => - !!error && error instanceof CancelledError; diff --git a/packages/api-rest/src/errors/index.ts b/packages/api-rest/src/errors/index.ts index b3b7f9ed040..5b9a8ffed64 100644 --- a/packages/api-rest/src/errors/index.ts +++ b/packages/api-rest/src/errors/index.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { CancelledError, isCancelError } from './CancelledError'; +export { CanceledError, isCancelError } from './CanceledError'; export { RestApiError } from './RestApiError'; export { assertValidationError } from './assertValidatonError'; export { RestApiValidationErrorCode, validationErrorMap } from './validation'; diff --git a/packages/api-rest/src/index.ts b/packages/api-rest/src/index.ts index 9eb4a5f0ac9..bb1be9d6ee6 100644 --- a/packages/api-rest/src/index.ts +++ b/packages/api-rest/src/index.ts @@ -1,5 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { isCancelError } from './errors/CancelledError'; +export { isCancelError } from './errors/CanceledError'; export { get, post, put, del, head, patch } from './apis'; diff --git a/packages/api-rest/src/server.ts b/packages/api-rest/src/server.ts index 8fceea3baa1..11b1b888efc 100644 --- a/packages/api-rest/src/server.ts +++ b/packages/api-rest/src/server.ts @@ -1,5 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { isCancelError } from './errors/CancelledError'; +export { isCancelError } from './errors/CanceledError'; export { get, post, put, del, head, patch } from './apis/server'; diff --git a/packages/api-rest/src/types/index.ts b/packages/api-rest/src/types/index.ts index d1a946d594e..5255586493d 100644 --- a/packages/api-rest/src/types/index.ts +++ b/packages/api-rest/src/types/index.ts @@ -40,7 +40,7 @@ export type RestApiOptionsBase = { type Headers = Record; /** - * Type representing an operation that can be cancelled. + * Type representing an operation that can be canceled. * * @internal */ diff --git a/packages/api-rest/src/utils/createCancellableOperation.ts b/packages/api-rest/src/utils/createCancellableOperation.ts index 81a69fc6d1d..d4b592d2a67 100644 --- a/packages/api-rest/src/utils/createCancellableOperation.ts +++ b/packages/api-rest/src/utils/createCancellableOperation.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { HttpResponse } from '@aws-amplify/core/internals/aws-client-utils'; -import { CancelledError, RestApiError } from '../errors'; +import { CanceledError } from '../errors'; import { Operation } from '../types'; import { parseRestApiServiceError } from './serviceError'; import { logger } from './logger'; @@ -59,13 +59,12 @@ export function createCancellableOperation( } catch (error: any) { const abortSignal = internalPostAbortSignal ?? publicApisAbortSignal; if (error.name === 'AbortError' || abortSignal?.aborted === true) { - const cancelledError = new CancelledError({ - name: error.name, - message: abortSignal.reason ?? error.message, + const canceledError = new CanceledError({ + ...(abortSignal.reason ? { message: abortSignal.reason } : undefined), underlyingError: error, }); logger.debug(error); - throw cancelledError; + throw canceledError; } logger.debug(error); throw error; @@ -82,7 +81,7 @@ export function createCancellableOperation( publicApisAbortController.abort(abortMessage); // Abort reason is not widely support enough across runtimes and and browsers, so we set it // if it is not already set. - if (publicApisAbortSignal.reason !== abortMessage) { + if (abortMessage && publicApisAbortSignal.reason !== abortMessage) { type AbortSignalWithReasonSupport = Omit & { reason?: string; }; diff --git a/packages/storage/__tests__/providers/s3/apis/utils/downloadTask.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/downloadTask.test.ts index 75ab4eb508a..771817cbb0a 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/downloadTask.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/downloadTask.test.ts @@ -28,10 +28,10 @@ describe('createDownloadTask', () => { job: jest.fn(), onCancel, }); - const customError = new Error('Custom Error'); - task.cancel(customError); + const customErrorMessage = 'Custom Error'; + task.cancel(customErrorMessage); expect(task.state).toBe('CANCELED'); - expect(onCancel).toHaveBeenCalledWith(customError); + expect(onCancel).toHaveBeenCalledWith(customErrorMessage); }); it('should set status to error after calling error', async () => { diff --git a/packages/storage/__tests__/providers/s3/apis/utils/uploadTask.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/uploadTask.test.ts index 03a3c5465bb..99d92c82a0b 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/uploadTask.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/uploadTask.test.ts @@ -29,10 +29,10 @@ describe('createUploadTask', () => { job: jest.fn(), onCancel, }); - const customError = new Error('Custom Error'); - task.cancel(customError); + const customErrorMessage = 'Custom Error'; + task.cancel(customErrorMessage); expect(task.state).toBe('CANCELED'); - expect(onCancel).toHaveBeenCalledWith(customError); + expect(onCancel).toHaveBeenCalledWith(customErrorMessage); }); it('should set status to error after calling error', async () => { diff --git a/packages/storage/src/errors/CanceledError.ts b/packages/storage/src/errors/CanceledError.ts index 128ed72e089..8303aa86751 100644 --- a/packages/storage/src/errors/CanceledError.ts +++ b/packages/storage/src/errors/CanceledError.ts @@ -11,8 +11,12 @@ import { StorageError } from './StorageError'; * @internal */ export class CanceledError extends StorageError { - constructor(params: AmplifyErrorParams) { - super(params); + constructor(params: Partial = {}) { + super({ + name: 'CanceledError', + message: 'Upload is canceled by user', + ...params, + }); // TODO: Delete the following 2 lines after we change the build target to >= es2015 this.constructor = CanceledError; @@ -24,5 +28,5 @@ export class CanceledError extends StorageError { * Check if an error is caused by user calling `cancel()` on a upload/download task. If an overwriting error is * supplied to `task.cancel(errorOverwrite)`, this function will return `false`. */ -export const isCancelError = (error: unknown): boolean => +export const isCancelError = (error: unknown): error is CanceledError => !!error && error instanceof CanceledError; diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index 2e2caf4a447..acf7c65eb65 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -46,8 +46,8 @@ export const downloadData = (input: DownloadDataInput): DownloadDataOutput => { const downloadTask = createDownloadTask({ job: downloadDataJob(input, abortController.signal), - onCancel: (abortErrorOverwrite?: Error) => { - abortController.abort(abortErrorOverwrite); + onCancel: (message?: string) => { + abortController.abort(message); }, }); return downloadTask; @@ -79,7 +79,7 @@ const downloadDataJob = ...s3Config, abortSignal, onDownloadProgress: downloadDataOptions?.onProgress, - userAgentValue: getStorageUserAgentValue(StorageAction.DownloadData) + userAgentValue: getStorageUserAgentValue(StorageAction.DownloadData), }, { Bucket: bucket, diff --git a/packages/storage/src/providers/s3/apis/uploadData/index.ts b/packages/storage/src/providers/s3/apis/uploadData/index.ts index ff23d9780ee..ad43972d46b 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/index.ts @@ -72,8 +72,8 @@ export const uploadData = (input: UploadDataInput): UploadDataOutput => { return createUploadTask({ isMultipartUpload: false, job: putObjectJob(input, abortController.signal, dataByteLength), - onCancel: (abortErrorOverwrite?: Error) => { - abortController.abort(abortErrorOverwrite); + onCancel: (message?: string) => { + abortController.abort(message); }, }); } else { @@ -82,8 +82,8 @@ export const uploadData = (input: UploadDataInput): UploadDataOutput => { return createUploadTask({ isMultipartUpload: true, job: multipartUploadJob, - onCancel: (abortErrorOverwrite?: Error) => { - onCancel(abortErrorOverwrite); + onCancel: (message?: string) => { + onCancel(message); }, onPause, onResume, diff --git a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts index 6cf6d13fb4f..897f04d3368 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts @@ -150,7 +150,7 @@ export const getMultipartUploadHandlers = ( { ...s3Config, abortSignal: abortController.signal, - userAgentValue: getStorageUserAgentValue(StorageAction.UploadData) + userAgentValue: getStorageUserAgentValue(StorageAction.UploadData), }, { Bucket: bucket, @@ -215,9 +215,9 @@ export const getMultipartUploadHandlers = ( const onResume = () => { startUploadWithResumability(); }; - const onCancel = (abortErrorOverwrite?: Error) => { + const onCancel = (message?: string) => { // 1. abort in-flight API requests - abortController?.abort(abortErrorOverwrite); + abortController?.abort(message); const cancelUpload = async () => { // 2. clear upload cache. @@ -236,13 +236,9 @@ export const getMultipartUploadHandlers = ( }); rejectCallback!( - abortErrorOverwrite ?? - // Internal error that should not be exposed to the users. They should use isCancelError() to check if - // the error is caused by cancel(). - new CanceledError({ - name: 'StorageCanceledError', - message: 'Upload is canceled by user', - }) + // Internal error that should not be exposed to the users. They should use isCancelError() to check if + // the error is caused by cancel(). + new CanceledError(message ? { message } : undefined) ); }; return { diff --git a/packages/storage/src/providers/s3/utils/client/runtime/xhrTransferHandler.ts b/packages/storage/src/providers/s3/utils/client/runtime/xhrTransferHandler.ts index 0ceb88634a2..5638b84c056 100644 --- a/packages/storage/src/providers/s3/utils/client/runtime/xhrTransferHandler.ts +++ b/packages/storage/src/providers/s3/utils/client/runtime/xhrTransferHandler.ts @@ -153,7 +153,7 @@ export const xhrTransferHandler: TransferHandler< }); if (abortSignal) { - const onCancelled = () => { + const onCanceled = () => { // The abort event is triggered after the error or load event. So we need to check if the xhr is null. if (!xhr) { return; @@ -167,8 +167,8 @@ export const xhrTransferHandler: TransferHandler< xhr = null; }; abortSignal.aborted - ? onCancelled() - : abortSignal.addEventListener('abort', onCancelled); + ? onCanceled() + : abortSignal.addEventListener('abort', onCanceled); } if ( diff --git a/packages/storage/src/providers/s3/utils/transferTask.ts b/packages/storage/src/providers/s3/utils/transferTask.ts index b271364eb54..0863b83026b 100644 --- a/packages/storage/src/providers/s3/utils/transferTask.ts +++ b/packages/storage/src/providers/s3/utils/transferTask.ts @@ -10,7 +10,7 @@ import { type CreateCancellableTaskOptions = { job: () => Promise; - onCancel: (abortErrorOverwrite?: Error) => void; + onCancel: (message?: string) => void; }; type CancellableTask = DownloadTask; @@ -20,16 +20,16 @@ const createCancellableTask = ({ onCancel, }: CreateCancellableTaskOptions): CancellableTask => { const state = 'IN_PROGRESS' as TransferTaskState; - let abortErrorOverwriteRecord: Error | undefined = undefined; + let canceledErrorMessage: string | undefined = undefined; const cancelableTask = { - cancel: (abortErrorOverwrite?: Error) => { - abortErrorOverwriteRecord = abortErrorOverwrite; + cancel: (message?: string) => { const { state } = cancelableTask; if (state === 'CANCELED' || state === 'ERROR' || state === 'SUCCESS') { return; } cancelableTask.state = 'CANCELED'; - onCancel(abortErrorOverwrite); + canceledErrorMessage = message; + onCancel(canceledErrorMessage); }, state, }; @@ -42,7 +42,7 @@ const createCancellableTask = ({ } catch (e) { if (isCancelError(e)) { cancelableTask.state = 'CANCELED'; - throw abortErrorOverwriteRecord ?? e; + e.message = canceledErrorMessage ?? e.message; } cancelableTask.state = 'ERROR'; throw e; @@ -58,7 +58,7 @@ export const createDownloadTask = createCancellableTask; type CreateUploadTaskOptions = { job: () => Promise; - onCancel: (abortErrorOverwrite?: Error) => void; + onCancel: (message?: string) => void; onResume?: () => void; onPause?: () => void; isMultipartUpload?: boolean; diff --git a/packages/storage/src/types/common.ts b/packages/storage/src/types/common.ts index a61391b5b17..eaa4d546dfd 100644 --- a/packages/storage/src/types/common.ts +++ b/packages/storage/src/types/common.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import type { CanceledError } from '../errors/CanceledError'; + export type TransferTaskState = | 'IN_PROGRESS' | 'PAUSED' @@ -18,11 +20,11 @@ export type TransferTask = { * Cancel an ongoing transfer(upload/download) task. This will reject the `result` promise with an `AbortError` by * default. You can use `isCancelError` to check if the error is caused by cancellation. * - * @param {Error} [abortErrorOverwrite] - Optional error to overwrite the default `AbortError` thrown when the task is - * canceled. If provided, the `result` promise will be rejected with this error instead, and you can no longer use - * `isCancelError` to check if the error is caused by cancellation. + * @param message - Optional error message to overwrite the default `canceled` message thrown when the task is + * canceled. If provided, the `result` promise will be rejected with a {@link CanceledError} with supplied error + * message instead. */ - cancel: (abortErrorOverwrite?: Error) => void; + cancel: (message?: string) => void; /** * Pause an ongoing transfer(upload/download) task. This method does not support the following scenarios: From 175472e019b1da6a213f826fb1972c16cf912c08 Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Tue, 10 Oct 2023 17:52:24 -0700 Subject: [PATCH 05/22] chore(api-rest): clean up response payload (#12225) --- packages/api-rest/src/apis/common/handler.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/api-rest/src/apis/common/handler.ts b/packages/api-rest/src/apis/common/handler.ts index a24eb9d32a5..72f562c87af 100644 --- a/packages/api-rest/src/apis/common/handler.ts +++ b/packages/api-rest/src/apis/common/handler.ts @@ -76,6 +76,7 @@ export const transferHandler = async ( }; const isIamAuthApplicable = iamAuthApplicable(request, signingServiceInfo); + let response: RestApiResponse; if (isIamAuthApplicable) { const signingInfoFromUrl = parseSigningInfo(url); const signingService = @@ -83,17 +84,23 @@ export const transferHandler = async ( const signingRegion = signingServiceInfo?.region ?? signingInfoFromUrl.region; const credentials = await resolveCredentials(amplify); - return await authenticatedHandler(request, { + response = await authenticatedHandler(request, { ...baseOptions, credentials, region: signingRegion, service: signingService, }); } else { - return await unauthenticatedHandler(request, { + response = await unauthenticatedHandler(request, { ...baseOptions, }); } + // Clean-up un-modeled properties from response. + return { + statusCode: response.statusCode, + headers: response.headers, + body: response.body, + }; }; const iamAuthApplicable = ( From 6d5afce390687e925438d6d208a18c84e61399a9 Mon Sep 17 00:00:00 2001 From: David McAfee Date: Wed, 11 Oct 2023 06:37:16 -0700 Subject: [PATCH 06/22] feat(data): add GraphQL API V6 support for custom headers, non-Appsync endpoints, and custom domains; add / update tests (#12185) --- .../api-graphql/__tests__/GraphQLAPI.test.ts | 3004 +++++++---------- .../__tests__/resolveConfig.test.ts | 54 + .../AWSAppSyncRealTimeProvider/index.ts | 6 +- .../src/internals/InternalGraphQLAPI.ts | 103 +- packages/api-graphql/src/types/index.ts | 11 +- .../src/utils/errors/validation.ts | 20 +- packages/api-graphql/src/utils/index.ts | 2 +- .../api-graphql/src/utils/resolveConfig.ts | 33 +- .../src/utils/resolveCredentials.ts | 14 - .../src/utils/resolveLibraryOptions.ts | 13 + .../@aws-amplify/api-rest/internals/index.ts | 1 + packages/api/__tests__/API.test.ts | 185 +- .../aws-amplify/__tests__/exports.test.ts | 36 +- packages/aws-amplify/package.json | 6 +- packages/core/src/index.ts | 1 + packages/core/src/libraryUtils.ts | 2 +- packages/core/src/singleton/API/types.ts | 7 +- .../authModeStrategies/multiAuthStrategy.ts | 4 +- .../datastore/src/sync/processors/mutation.ts | 4 +- .../src/sync/processors/subscription.ts | 335 +- .../datastore/src/sync/processors/sync.ts | 4 +- packages/datastore/src/sync/utils.ts | 10 +- packages/datastore/src/types.ts | 8 +- 23 files changed, 1721 insertions(+), 2142 deletions(-) create mode 100644 packages/api-graphql/__tests__/resolveConfig.test.ts delete mode 100644 packages/api-graphql/src/utils/resolveCredentials.ts create mode 100644 packages/api-graphql/src/utils/resolveLibraryOptions.ts create mode 100644 packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index a62de6d0e8c..325c3c9d59f 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -1,1693 +1,1313 @@ -// import { InternalAuth } from '@aws-amplify/auth/internals'; -// import { GraphQLAPIClass as API } from '../src'; -// import { InternalGraphQLAPIClass as InternalAPI } from '../src/internals'; -// import { graphqlOperation } from '../src/GraphQLAPI'; -// import { GRAPHQL_AUTH_MODE, GraphQLAuthError } from '../src/types'; -// import { RestClient } from '@aws-amplify/api-rest'; -// import { print } from 'graphql/language/printer'; -// import { parse } from 'graphql/language/parser'; -// import { -// Credentials, -// // Constants, -// // INTERNAL_AWS_APPSYNC_REALTIME_PUBSUB_PROVIDER, -// // Category, -// // Framework, -// // ApiAction, -// // CustomUserAgentDetails, -// } from '@aws-amplify/core'; -// import { -// Constants, -// INTERNAL_AWS_APPSYNC_REALTIME_PUBSUB_PROVIDER, -// Category, -// Framework, -// ApiAction, -// CustomUserAgentDetails, -// } from '@aws-amplify/core/internals/utils'; -// import { InternalPubSub } from '@aws-amplify/pubsub/internals'; -// import { Cache } from '@aws-amplify/cache'; -// import * as Observable from 'zen-observable'; -// import axios, { CancelTokenStatic } from 'axios'; - -// axios.CancelToken = { -// source: () => ({ token: null, cancel: null } as any), -// }; -// axios.isCancel = (value: any): boolean => { -// return false; -// }; - -// let isCancelSpy; -// let cancelTokenSpy; -// let cancelMock; -// let tokenMock; -// let mockCancellableToken; -// jest.mock('axios'); - -// const config = { -// API: { -// region: 'region', -// header: {}, -// }, -// }; - -// const GetEvent = `query GetEvent($id: ID! $nextToken: String) { -// getEvent(id: $id) { -// id -// name -// where -// when -// description -// comments(nextToken: $nextToken) { -// items { -// commentId -// content -// createdAt -// } -// } -// } -// }`; -// const getEventDoc = parse(GetEvent); -// const getEventQuery = print(getEventDoc); - -// /* TODO: Test with actual actions */ -// const expectedUserAgentFrameworkOnly = `${Constants.userAgent} framework/${Framework.WebUnknown}`; -// const customUserAgentDetailsAPI: CustomUserAgentDetails = { -// category: Category.API, -// action: ApiAction.GraphQl, -// }; -// const expectedUserAgentAPI = `${Constants.userAgent} ${Category.API}/${ApiAction.GraphQl} framework/${Framework.WebUnknown}`; - -// afterEach(() => { -// jest.restoreAllMocks(); -// }); - -// describe('API test', () => { -// beforeEach(() => { -// cancelMock = jest.fn(); -// tokenMock = jest.fn(); -// mockCancellableToken = { token: tokenMock, cancel: cancelMock }; -// isCancelSpy = jest.spyOn(axios, 'isCancel').mockReturnValue(true); -// cancelTokenSpy = jest -// .spyOn(axios.CancelToken, 'source') -// .mockImplementation(() => { -// return mockCancellableToken; -// }); -// }); -// describe('graphql test', () => { -// test('happy-case-query', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': apiKey, -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql(graphqlOperation(GetEvent, variables)); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('cancel-graphql-query', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// rej('error cancelled'); -// }); -// }); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': apiKey, -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// const promiseResponse = api.graphql( -// graphqlOperation(GetEvent, variables) -// ); -// api.cancel(promiseResponse as Promise, 'testmessage'); - -// expect.assertions(5); - -// expect(cancelTokenSpy).toBeCalledTimes(1); -// expect(cancelMock).toBeCalledWith('testmessage'); -// try { -// await promiseResponse; -// } catch (err) { -// expect(err).toEqual('error cancelled'); -// expect(api.isCancel(err)).toBeTruthy(); -// } -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('happy-case-query-ast', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': apiKey, -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql(graphqlOperation(getEventDoc, variables)); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('happy-case-query-oidc with Cache token', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// const spyonCache = jest -// .spyOn(Cache, 'getItem') -// .mockImplementationOnce(() => { -// return { -// token: 'id_token', -// }; -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'OPENID_CONNECT', -// }); - -// const headers = { -// Authorization: 'id_token', -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql(graphqlOperation(GetEvent, variables)); - -// expect(spyon).toBeCalledWith(url, init); - -// spyonCache.mockClear(); -// }); - -// test('happy-case-query-oidc with auth storage federated token', async () => { -// const spyonCredentials = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// const spyonCache = jest -// .spyOn(Cache, 'getItem') -// .mockImplementationOnce(() => { -// return null; -// }); - -// const spyonAuth = jest -// .spyOn(InternalAuth, 'currentAuthenticatedUser') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res({ -// name: 'federated user', -// token: 'federated_token_from_storage', -// }); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'OPENID_CONNECT', -// }); - -// const headers = { -// Authorization: 'federated_token_from_storage', -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql(graphqlOperation(GetEvent, variables)); - -// expect(spyon).toBeCalledWith(url, init); - -// spyonCredentials.mockClear(); -// spyonCache.mockClear(); -// spyonAuth.mockClear(); -// }); - -// test('happy case query with AWS_LAMBDA', async () => { -// expect.assertions(1); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com'; -// const region = 'us-east-2'; -// const variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; - -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'AWS_LAMBDA', -// }); - -// const headers = { -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// Authorization: 'myAuthToken', -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql({ -// query: GetEvent, -// variables, -// authToken: 'myAuthToken', -// }); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('additional headers with AWS_LAMBDA', async () => { -// expect.assertions(1); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com'; -// const region = 'us-east-2'; -// const variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; - -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'AWS_LAMBDA', -// }); - -// const headers = { -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// Authorization: 'myAuthToken', -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql( -// { -// query: GetEvent, -// variables, -// authToken: 'myAuthToken', -// }, -// { Authorization: 'anotherAuthToken' } -// ); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('multi-auth default case AWS_IAM, using API_KEY as auth mode', async () => { -// expect.assertions(1); - -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'AWS_IAM', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': 'secret-api-key', -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.API_KEY, -// }); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('multi-auth default case api-key, using AWS_IAM as auth mode', async () => { -// expect.assertions(1); -// jest.spyOn(Credentials, 'get').mockReturnValue(Promise.resolve('cred')); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.AWS_IAM, -// }); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('multi-auth default case api-key, using AWS_LAMBDA as auth mode', async () => { -// expect.assertions(1); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; - -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// Authorization: 'myAuthToken', -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.AWS_LAMBDA, -// authToken: 'myAuthToken', -// }); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('multi-auth default case api-key, using OIDC as auth mode', async () => { -// expect.assertions(1); -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// jest.spyOn(Cache, 'getItem').mockReturnValue({ token: 'oidc_token' }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// Authorization: 'oidc_token', -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.OPENID_CONNECT, -// }); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('multi-auth using OIDC as auth mode, but no federatedSign', async () => { -// expect.assertions(1); - -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// jest.spyOn(Cache, 'getItem').mockReturnValue(null); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// await expect( -// api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.OPENID_CONNECT, -// }) -// ).rejects.toThrowError('No current user'); -// }); - -// test('multi-auth using CUP as auth mode, but no userpool', async () => { -// expect.assertions(1); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// await expect( -// api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, -// }) -// ).rejects.toThrow(); -// }); - -// test('multi-auth using AWS_LAMBDA as auth mode, but no auth token specified', async () => { -// expect.assertions(1); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; - -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'AWS_IAM', -// }); - -// await expect( -// api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.AWS_LAMBDA, -// }) -// ).rejects.toThrowError(GraphQLAuthError.NO_AUTH_TOKEN); -// }); - -// test('multi-auth using API_KEY as auth mode, but no api-key configured', async () => { -// expect.assertions(1); - -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'AWS_IAM', -// }); - -// await expect( -// api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.API_KEY, -// }) -// ).rejects.toThrowError('No api-key configured'); -// }); - -// test('multi-auth using AWS_IAM as auth mode, but no credentials', async () => { -// expect.assertions(1); - -// jest.spyOn(Credentials, 'get').mockReturnValue(Promise.reject()); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// await expect( -// api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.AWS_IAM, -// }) -// ).rejects.toThrowError('No credentials'); -// }); - -// test('multi-auth default case api-key, using CUP as auth mode', async () => { -// expect.assertions(1); -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// jest.spyOn(InternalAuth, 'currentSession').mockReturnValue({ -// getAccessToken: () => ({ -// getJwtToken: () => 'Secret-Token', -// }), -// } as any); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// Authorization: 'Secret-Token', -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql({ -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, -// }); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('authMode on subscription', async () => { -// expect.assertions(1); - -// jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementation(async (url, init) => ({ -// extensions: { -// subscription: { -// newSubscriptions: {}, -// }, -// }, -// })); - -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// jest.spyOn(Cache, 'getItem').mockReturnValue({ token: 'id_token' }); - -// const spyon_pubsub = jest -// .spyOn(InternalPubSub, 'subscribe') -// .mockImplementation(jest.fn(() => Observable.of({}) as any)); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; - -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const SubscribeToEventComments = `subscription SubscribeToEventComments($eventId: String!) { -// subscribeToEventComments(eventId: $eventId) { -// eventId -// commentId -// content -// } -// }`; - -// const doc = parse(SubscribeToEventComments); -// const query = print(doc); - -// ( -// api.graphql({ -// query, -// variables, -// authMode: GRAPHQL_AUTH_MODE.OPENID_CONNECT, -// }) as any -// ).subscribe(); - -// expect(spyon_pubsub).toBeCalledWith( -// '', -// expect.objectContaining({ -// authenticationType: 'OPENID_CONNECT', -// }), -// undefined -// ); -// }); - -// test('happy-case-subscription', async done => { -// jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementation(async (url, init) => ({ -// extensions: { -// subscription: { -// newSubscriptions: {}, -// }, -// }, -// })); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; - -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// InternalPubSub.subscribe = jest.fn(() => Observable.of({}) as any); - -// const SubscribeToEventComments = `subscription SubscribeToEventComments($eventId: String!) { -// subscribeToEventComments(eventId: $eventId) { -// eventId -// commentId -// content -// } -// }`; - -// const doc = parse(SubscribeToEventComments); -// const query = print(doc); - -// const observable = ( -// api.graphql( -// graphqlOperation(query, variables) -// ) as unknown as Observable -// ).subscribe({ -// next: () => { -// expect(InternalPubSub.subscribe).toHaveBeenCalledTimes(1); -// const subscribeOptions = (InternalPubSub.subscribe as any).mock -// .calls[0][1]; -// expect(subscribeOptions.provider).toBe( -// INTERNAL_AWS_APPSYNC_REALTIME_PUBSUB_PROVIDER -// ); -// done(); -// }, -// }); - -// expect(observable).not.toBe(undefined); -// }); - -// test('happy case subscription with additionalHeaders', async done => { -// jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementation(async (url, init) => ({ -// extensions: { -// subscription: { -// newSubscriptions: {}, -// }, -// }, -// })); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; - -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// InternalPubSub.subscribe = jest.fn(() => Observable.of({}) as any); - -// const SubscribeToEventComments = `subscription SubscribeToEventComments($eventId: String!) { -// subscribeToEventComments(eventId: $eventId) { -// eventId -// commentId -// content -// } -// }`; - -// const doc = parse(SubscribeToEventComments); -// const query = print(doc); - -// const additionalHeaders = { -// 'x-custom-header': 'value', -// }; - -// const observable = ( -// api.graphql( -// graphqlOperation(query, variables), -// additionalHeaders -// ) as unknown as Observable -// ).subscribe({ -// next: () => { -// expect(InternalPubSub.subscribe).toHaveBeenCalledTimes(1); -// const subscribeOptions = (InternalPubSub.subscribe as any).mock -// .calls[0][1]; -// expect(subscribeOptions.additionalHeaders).toBe(additionalHeaders); -// done(); -// }, -// }); - -// expect(observable).not.toBe(undefined); -// }); - -// test('happy case mutation', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { -// id: '809392da-ec91-4ef0-b219-5238a8f942b2', -// content: 'lalala', -// createdAt: new Date().toISOString(), -// }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); -// const AddComment = `mutation AddComment($eventId: ID!, $content: String!, $createdAt: String!) { -// commentOnEvent(eventId: $eventId, content: $content, createdAt: $createdAt) { -// eventId -// content -// createdAt -// } -// }`; - -// const doc = parse(AddComment); -// const query = print(doc); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': apiKey, -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await api.graphql(graphqlOperation(AddComment, variables)); - -// expect(spyon).toBeCalledWith(url, init); -// }); - -// test('happy case query with additionalHeaders', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// graphql_headers: async () => -// Promise.resolve({ -// someHeaderSetAtConfigThatWillBeOverridden: 'initialValue', -// someOtherHeaderSetAtConfig: 'expectedValue', -// }), -// }); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': apiKey, -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// const additionalHeaders = { -// someAddtionalHeader: 'foo', -// someHeaderSetAtConfigThatWillBeOverridden: 'expectedValue', -// }; - -// await api.graphql( -// graphqlOperation(GetEvent, variables), -// additionalHeaders -// ); - -// expect(spyon).toBeCalledWith(url, { -// ...init, -// headers: { -// someAddtionalHeader: 'foo', -// someHeaderSetAtConfigThatWillBeOverridden: 'expectedValue', -// ...init.headers, -// someOtherHeaderSetAtConfig: 'expectedValue', -// }, -// }); -// }); - -// test('call isInstanceCreated', () => { -// const createInstanceMock = spyOn(API.prototype, 'createInstance'); -// const api = new API(config); -// api.createInstanceIfNotCreated(); -// expect(createInstanceMock).toHaveBeenCalled(); -// }); - -// test('should not call createInstance when there is already an instance', () => { -// const api = new API(config); -// api.createInstance(); -// const createInstanceMock = spyOn(API.prototype, 'createInstance'); -// api.createInstanceIfNotCreated(); -// expect(createInstanceMock).not.toHaveBeenCalled(); -// }); - -// test('sends cookies with request', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const api = new API(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// api.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// withCredentials: true, -// }); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': apiKey, -// 'x-amz-user-agent': expectedUserAgentFrameworkOnly, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// withCredentials: true, -// }; -// let authToken: undefined; - -// await api.graphql(graphqlOperation(GetEvent, variables, authToken)); - -// expect(spyon).toBeCalledWith(url, init); -// }); -// }); - -// describe('configure test', () => { -// test('without aws_project_region', () => { -// const api = new API({}); - -// const options = { -// myoption: 'myoption', -// }; - -// expect(api.configure(options)).toEqual({ -// myoption: 'myoption', -// }); -// }); - -// test('with aws_project_region', () => { -// const api = new API({}); - -// const options = { -// aws_project_region: 'region', -// }; - -// expect(api.configure(options)).toEqual({ -// aws_project_region: 'region', -// header: {}, -// region: 'region', -// }); -// }); - -// test('with API options', () => { -// const api = new API({}); - -// const options = { -// API: { -// aws_project_region: 'api-region', -// }, -// aws_project_region: 'region', -// aws_appsync_region: 'appsync-region', -// }; - -// expect(api.configure(options)).toEqual({ -// aws_project_region: 'api-region', -// aws_appsync_region: 'appsync-region', -// header: {}, -// region: 'api-region', -// }); -// }); -// }); -// }); - -// describe('Internal API customUserAgent test', () => { -// beforeEach(() => { -// cancelMock = jest.fn(); -// tokenMock = jest.fn(); -// mockCancellableToken = { token: tokenMock, cancel: cancelMock }; -// isCancelSpy = jest.spyOn(axios, 'isCancel').mockReturnValue(true); -// cancelTokenSpy = jest -// .spyOn(axios.CancelToken, 'source') -// .mockImplementation(() => { -// return mockCancellableToken; -// }); -// }); -// describe('graphql test', () => { -// test('happy case mutation - API_KEY', async () => { -// const spyonAuth = jest -// .spyOn(Credentials, 'get') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res('cred'); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const internalApi = new InternalAPI(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { -// id: '809392da-ec91-4ef0-b219-5238a8f942b2', -// content: 'lalala', -// createdAt: new Date().toISOString(), -// }; -// internalApi.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); -// const AddComment = `mutation AddComment($eventId: ID!, $content: String!, $createdAt: String!) { -// commentOnEvent(eventId: $eventId, content: $content, createdAt: $createdAt) { -// eventId -// content -// createdAt -// } -// }`; - -// const doc = parse(AddComment); -// const query = print(doc); - -// const headers = { -// Authorization: null, -// 'X-Api-Key': apiKey, -// 'x-amz-user-agent': expectedUserAgentAPI, -// }; - -// const body = { -// query, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await internalApi.graphql( -// graphqlOperation(AddComment, variables), -// undefined, -// customUserAgentDetailsAPI -// ); - -// expect(spyon).toBeCalledWith(url, init); - -// spyonAuth.mockClear(); -// spyon.mockClear(); -// }); - -// test('happy case mutation - OPENID_CONNECT', async () => { -// const cache_config = { -// capacityInBytes: 3000, -// itemMaxSize: 800, -// defaultTTL: 3000000, -// defaultPriority: 5, -// warningThreshold: 0.8, -// storage: window.localStorage, -// }; - -// Cache.configure(cache_config); - -// const spyonCache = jest -// .spyOn(Cache, 'getItem') -// .mockImplementationOnce(() => { -// return null; -// }); - -// const spyonAuth = jest -// .spyOn(InternalAuth, 'currentAuthenticatedUser') -// .mockImplementationOnce(() => { -// return new Promise((res, rej) => { -// res({ -// name: 'federated user', -// token: 'federated_token_from_storage', -// }); -// }); -// }); - -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementationOnce((url, init) => { -// return new Promise((res, rej) => { -// res({}); -// }); -// }); - -// const internalApi = new InternalAPI(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; -// internalApi.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'OPENID_CONNECT', -// }); - -// const headers = { -// Authorization: 'federated_token_from_storage', -// 'x-amz-user-agent': expectedUserAgentAPI, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await internalApi.graphql( -// graphqlOperation(GetEvent, variables), -// undefined, -// customUserAgentDetailsAPI -// ); - -// expect(spyon).toBeCalledWith(url, init); -// expect(spyonAuth).toBeCalledWith(undefined, customUserAgentDetailsAPI); - -// spyonCache.mockClear(); -// spyonAuth.mockClear(); -// spyon.mockClear(); -// }); - -// test('happy case mutation - AMAZON_COGNITO_USER_POOLS', async () => { -// const spyon = jest -// .spyOn(RestClient.prototype, 'post') -// .mockReturnValue(Promise.resolve({})); - -// const spyonAuth = jest -// .spyOn(InternalAuth, 'currentSession') -// .mockReturnValue({ -// getAccessToken: () => ({ -// getJwtToken: () => 'Secret-Token', -// }), -// } as any); - -// const internalApi = new InternalAPI(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }, -// apiKey = 'secret-api-key'; -// internalApi.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// const headers = { -// Authorization: 'Secret-Token', -// 'x-amz-user-agent': expectedUserAgentAPI, -// }; - -// const body = { -// query: getEventQuery, -// variables, -// }; - -// const init = { -// headers, -// body, -// signerServiceInfo: { -// service: 'appsync', -// region, -// }, -// cancellableToken: mockCancellableToken, -// }; - -// await internalApi.graphql( -// { -// query: GetEvent, -// variables, -// authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, -// }, -// undefined, -// customUserAgentDetailsAPI -// ); - -// expect(spyon).toBeCalledWith(url, init); -// expect(spyonAuth).toBeCalledWith(customUserAgentDetailsAPI); - -// spyon.mockClear(); -// spyonAuth.mockClear(); -// }); - -// test('happy case subscription', async done => { -// jest -// .spyOn(RestClient.prototype, 'post') -// .mockImplementation(async (url, init) => ({ -// extensions: { -// subscription: { -// newSubscriptions: {}, -// }, -// }, -// })); - -// const internalApi = new InternalAPI(config); -// const url = 'https://appsync.amazonaws.com', -// region = 'us-east-2', -// apiKey = 'secret_api_key', -// variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; - -// internalApi.configure({ -// aws_appsync_graphqlEndpoint: url, -// aws_appsync_region: region, -// aws_appsync_authenticationType: 'API_KEY', -// aws_appsync_apiKey: apiKey, -// }); - -// InternalPubSub.subscribe = jest.fn(() => Observable.of({}) as any); - -// const SubscribeToEventComments = `subscription SubscribeToEventComments($eventId: String!) { -// subscribeToEventComments(eventId: $eventId) { -// eventId -// commentId -// content -// } -// }`; - -// const doc = parse(SubscribeToEventComments); -// const query = print(doc); - -// const observable = ( -// internalApi.graphql( -// graphqlOperation(query, variables), -// undefined, -// customUserAgentDetailsAPI -// ) as unknown as Observable -// ).subscribe({ -// next: () => { -// expect(InternalPubSub.subscribe).toHaveBeenCalledTimes(1); -// expect(InternalPubSub.subscribe).toHaveBeenCalledWith( -// expect.anything(), -// expect.anything(), -// customUserAgentDetailsAPI -// ); -// const subscribeOptions = (InternalPubSub.subscribe as any).mock -// .calls[0][1]; -// expect(subscribeOptions.provider).toBe( -// INTERNAL_AWS_APPSYNC_REALTIME_PUBSUB_PROVIDER -// ); -// done(); -// }, -// }); - -// expect(observable).not.toBe(undefined); -// }); -// }); -// }); -// TODO(v6): add tests -describe.skip('API tests', () => { - test('add tests', async () => {}); +import * as raw from '../src'; +import { graphql, cancel, isCancelError } from '../src/internals/v6'; +import { Amplify } from 'aws-amplify'; +import { Amplify as AmplifyCore } from '@aws-amplify/core'; +import * as typedQueries from './fixtures/with-types/queries'; +import { expectGet } from './utils/expects'; + +import { + __amplify, + GraphQLResult, + GraphQLAuthError, + V6Client, +} from '../src/types'; +import { GetThreadQuery } from './fixtures/with-types/API'; + +const serverManagedFields = { + id: 'some-id', + owner: 'wirejobviously', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +/** + * `generateClient()` is only exported from top-level API category, so we create + * the equivalent of the generated client below. First we need to create a + * partial mock of the Amplify core module for the pretend generated client: + */ +let amplify; + +jest.mock('aws-amplify', () => { + const originalModule = jest.requireActual('aws-amplify'); + + const mockedModule = { + __esModule: true, + ...originalModule, + Amplify: { + ...originalModule.Amplify, + Auth: { + ...originalModule.Amplify.Auth, + fetchAuthSession: jest.fn(() => { + return { + tokens: { + accessToken: { + toString: () => 'mock-access-token', + }, + }, + credentials: { + accessKeyId: 'mock-access-key-id', + secretAccessKey: 'mock-secret-access-key', + }, + }; + }), + }, + }, + }; + + amplify = mockedModule.Amplify; + return mockedModule; +}); + +const client = { + [__amplify]: amplify, + graphql, + cancel, + isCancelError, +} as V6Client; + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('API test', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('graphql test', () => { + test('happy-case-query', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'apiKey', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expectGet(spy, 'getThread', graphqlVariables); + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + }); + + test('cancel-graphql-query', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + jest + .spyOn((raw.GraphQLAPI as any)._api, 'cancelREST') + .mockReturnValue(true); + + const request = Promise.resolve(); + expect(client.cancel(request)).toBe(true); + }); + + test('cancel-graphql-query', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + jest + .spyOn((raw.GraphQLAPI as any)._api, 'cancelREST') + .mockReturnValue(true); + + jest + .spyOn((raw.GraphQLAPI as any)._api, 'isCancelErrorREST') + .mockReturnValue(true); + + let promiseToCancel; + let isCancelErrorResult; + + try { + promiseToCancel = client.graphql({ query: 'query' }); + await promiseToCancel; + } catch (e) { + isCancelErrorResult = client.isCancelError(e); + } + + const cancellationResult = client.cancel(promiseToCancel); + + expect(cancellationResult).toBe(true); + expect(isCancelErrorResult).toBe(true); + }); + + test('happy-case-query-oidc', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'oidc', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'oidc', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'mock-access-token', + }), + signingServiceInfo: expect.objectContaining({ + region: 'local-host-h4x', + service: 'appsync', + }), + }), + }); + }); + + // TODO: + // test('happy-case-query-oidc with auth storage federated token', async () => { + // const spyonCredentials = jest + // .spyOn(Credentials, 'get') + // .mockImplementationOnce(() => { + // return new Promise((res, rej) => { + // res('cred'); + // }); + // }); + + // const cache_config = { + // capacityInBytes: 3000, + // itemMaxSize: 800, + // defaultTTL: 3000000, + // defaultPriority: 5, + // warningThreshold: 0.8, + // storage: window.localStorage, + // }; + + // Cache.configure(cache_config); + + // const spyonCache = jest + // .spyOn(Cache, 'getItem') + // .mockImplementationOnce(() => { + // return null; + // }); + + // const spyonAuth = jest + // .spyOn(InternalAuth, 'currentAuthenticatedUser') + // .mockImplementationOnce(() => { + // return new Promise((res, rej) => { + // res({ + // name: 'federated user', + // token: 'federated_token_from_storage', + // }); + // }); + // }); + + // const spyon = jest + // .spyOn(RestClient.prototype, 'post') + // .mockImplementationOnce((url, init) => { + // return new Promise((res, rej) => { + // res({}); + // }); + // }); + + // // const api = new API(config); + // const client = generateClient(); + // const url = 'https://appsync.amazonaws.com', + // region = 'us-east-2', + // variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; + // api.configure({ + // aws_appsync_graphqlEndpoint: url, + // aws_appsync_region: region, + // aws_appsync_authenticationType: 'OPENID_CONNECT', + // }); + + // const headers = { + // Authorization: 'federated_token_from_storage', + // // 'x-amz-user-agent': expectedUserAgentFrameworkOnly, + // }; + + // const body = { + // query: getEventQuery, + // variables, + // }; + + // const init = { + // headers, + // body, + // signerServiceInfo: { + // service: 'appsync', + // region, + // }, + // cancellableToken: mockCancellableToken, + // }; + + // await api.graphql(graphqlOperation(GetEvent, variables)); + + // expect(spyon).toBeCalledWith(url, init); + + // spyonCredentials.mockClear(); + // spyonCache.mockClear(); + // spyonAuth.mockClear(); + // }); + + test('happy case query with AWS_LAMBDA', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'lambda', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'lambda', + authToken: 'myAuthToken', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'myAuthToken', + }), + signingServiceInfo: expect.objectContaining({ + region: 'local-host-h4x', + service: 'appsync', + }), + }), + }); + }); + + // TODO: implement after custom user agent work is complete + // test('additional headers with AWS_LAMBDA', async () => { + // expect.assertions(1); + + // const spyon = jest + // .spyOn(RestClient.prototype, 'post') + // .mockReturnValue(Promise.resolve({})); + + // // const api = new API(config); + // const client = generateClient(); + // const url = 'https://appsync.amazonaws.com'; + // const region = 'us-east-2'; + // const variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; + + // api.configure({ + // aws_appsync_graphqlEndpoint: url, + // aws_appsync_region: region, + // aws_appsync_authenticationType: 'AWS_LAMBDA', + // }); + + // const headers = { + // // 'x-amz-user-agent': expectedUserAgentFrameworkOnly, + // Authorization: 'myAuthToken', + // }; + + // const body = { + // query: getEventQuery, + // variables, + // }; + + // const init = { + // headers, + // body, + // signerServiceInfo: { + // service: 'appsync', + // region, + // }, + // cancellableToken: mockCancellableToken, + // }; + + // await api.graphql( + // { + // query: GetEvent, + // variables, + // authToken: 'myAuthToken', + // }, + // { Authorization: 'anotherAuthToken' } + // ); + + // expect(spyon).toBeCalledWith(url, init); + // }); + + test('multi-auth default case AWS_IAM, using API_KEY as auth mode', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'iam', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'apiKey', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + signingServiceInfo: null, + }), + }); + }); + + test('multi-auth default case api-key, using AWS_IAM as auth mode', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'iam', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.not.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + signingServiceInfo: expect.objectContaining({ + region: 'local-host-h4x', + service: 'appsync', + }), + }), + }); + }); + + test('multi-auth default case api-key, using AWS_LAMBDA as auth mode', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'lambda', + authToken: 'myAuthToken', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'myAuthToken', + }), + signingServiceInfo: expect.objectContaining({ + region: 'local-host-h4x', + service: 'appsync', + }), + }), + }); + }); + + test('multi-auth default case api-key, using OIDC as auth mode', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'oidc', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'mock-access-token', + }), + signingServiceInfo: expect.objectContaining({ + region: 'local-host-h4x', + service: 'appsync', + }), + }), + }); + }); + + // TODO: make this fail without `Cache`? + test.skip('multi-auth default case api-key, OIDC as auth mode, but no federatedSign', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + await expect( + client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'oidc', + }) + ).rejects.toThrowError('No current user'); + }); + + // TODO: + test.skip('multi-auth using CUP as auth mode, but no userpool', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const graphqlVariables = { id: 'some-id' }; + + await expect( + client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'userPool', + }) + ).rejects.toThrow(); + }); + + it('AWS_LAMBDA as auth mode, but no auth token specified', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'lambda', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const graphqlVariables = { id: 'some-id' }; + + await expect( + client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'lambda', + }) + ).rejects.toThrowError(GraphQLAuthError.NO_AUTH_TOKEN); + }); + + test('multi-auth using API_KEY as auth mode, but no api-key configured', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'iam', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const graphqlVariables = { id: 'some-id' }; + + await expect( + client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'apiKey', + }) + ).rejects.toThrowError(GraphQLAuthError.NO_API_KEY); + }); + + // TODO: + test.skip('multi-auth using AWS_IAM as auth mode, but no credentials', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'fake-api-key', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const graphqlVariables = { id: 'some-id' }; + + await expect( + client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'iam', + }) + ).rejects.toThrowError(GraphQLAuthError.NO_API_KEY); + }); + + test('multi-auth default case api-key, using CUP as auth mode', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'userPool', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'mock-access-token', + }), + signingServiceInfo: expect.objectContaining({ + region: 'local-host-h4x', + service: 'appsync', + }), + }), + }); + }); + + // TODO: + // test('authMode on subscription', async () => { + // expect.assertions(1); + + // jest + // .spyOn(RestClient.prototype, 'post') + // .mockImplementation(async (url, init) => ({ + // extensions: { + // subscription: { + // newSubscriptions: {}, + // }, + // }, + // })); + + // const cache_config = { + // capacityInBytes: 3000, + // itemMaxSize: 800, + // defaultTTL: 3000000, + // defaultPriority: 5, + // warningThreshold: 0.8, + // storage: window.localStorage, + // }; + + // Cache.configure(cache_config); + + // jest.spyOn(Cache, 'getItem').mockReturnValue({ token: 'id_token' }); + + // const spyon_pubsub = jest + // .spyOn(InternalPubSub, 'subscribe') + // .mockImplementation(jest.fn(() => Observable.of({}) as any)); + + // // const api = new API(config); + // const client = generateClient(); + // const url = 'https://appsync.amazonaws.com', + // region = 'us-east-2', + // apiKey = 'secret_api_key', + // variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; + + // api.configure({ + // aws_appsync_graphqlEndpoint: url, + // aws_appsync_region: region, + // aws_appsync_authenticationType: 'API_KEY', + // aws_appsync_apiKey: apiKey, + // }); + + // const SubscribeToEventComments = `subscription SubscribeToEventComments($eventId: String!) { + // subscribeToEventComments(eventId: $eventId) { + // eventId + // commentId + // content + // } + // }`; + + // const doc = parse(SubscribeToEventComments); + // const query = print(doc); + + // ( + // api.graphql({ + // query, + // variables, + // authMode: GRAPHQL_AUTH_MODE.OPENID_CONNECT, + // }) as any + // ).subscribe(); + + // expect(spyon_pubsub).toBeCalledWith( + // '', + // expect.objectContaining({ + // authenticationType: 'OPENID_CONNECT', + // }), + // undefined + // ); + // }); + + // TODO: + // test('happy-case-subscription', async done => { + // jest + // .spyOn(RestClient.prototype, 'post') + // .mockImplementation(async (url, init) => ({ + // extensions: { + // subscription: { + // newSubscriptions: {}, + // }, + // }, + // })); + + // // const api = new API(config); + // const client = generateClient(); + // const url = 'https://appsync.amazonaws.com', + // region = 'us-east-2', + // apiKey = 'secret_api_key', + // variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; + + // api.configure({ + // aws_appsync_graphqlEndpoint: url, + // aws_appsync_region: region, + // aws_appsync_authenticationType: 'API_KEY', + // aws_appsync_apiKey: apiKey, + // }); + + // InternalPubSub.subscribe = jest.fn(() => Observable.of({}) as any); + + // const SubscribeToEventComments = `subscription SubscribeToEventComments($eventId: String!) { + // subscribeToEventComments(eventId: $eventId) { + // eventId + // commentId + // content + // } + // }`; + + // const doc = parse(SubscribeToEventComments); + // const query = print(doc); + + // const observable = ( + // api.graphql( + // graphqlOperation(query, variables) + // ) as unknown as Observable + // ).subscribe({ + // next: () => { + // expect(InternalPubSub.subscribe).toHaveBeenCalledTimes(1); + // const subscribeOptions = (InternalPubSub.subscribe as any).mock + // .calls[0][1]; + // expect(subscribeOptions.provider).toBe( + // INTERNAL_AWS_APPSYNC_REALTIME_PUBSUB_PROVIDER + // ); + // done(); + // }, + // }); + + // expect(observable).not.toBe(undefined); + // }); + + // TODO: + // test('happy case subscription with additionalHeaders', async done => { + // jest + // .spyOn(RestClient.prototype, 'post') + // .mockImplementation(async (url, init) => ({ + // extensions: { + // subscription: { + // newSubscriptions: {}, + // }, + // }, + // })); + + // // const api = new API(config); + // const client = generateClient(); + // const url = 'https://appsync.amazonaws.com', + // region = 'us-east-2', + // apiKey = 'secret_api_key', + // variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; + + // api.configure({ + // aws_appsync_graphqlEndpoint: url, + // aws_appsync_region: region, + // aws_appsync_authenticationType: 'API_KEY', + // aws_appsync_apiKey: apiKey, + // }); + + // InternalPubSub.subscribe = jest.fn(() => Observable.of({}) as any); + + // const SubscribeToEventComments = `subscription SubscribeToEventComments($eventId: String!) { + // subscribeToEventComments(eventId: $eventId) { + // eventId + // commentId + // content + // } + // }`; + + // const doc = parse(SubscribeToEventComments); + // const query = print(doc); + + // const additionalHeaders = { + // 'x-custom-header': 'value', + // }; + + // const observable = ( + // api.graphql( + // graphqlOperation(query, variables), + // additionalHeaders + // ) as unknown as Observable + // ).subscribe({ + // next: () => { + // expect(InternalPubSub.subscribe).toHaveBeenCalledTimes(1); + // const subscribeOptions = (InternalPubSub.subscribe as any).mock + // .calls[0][1]; + // expect(subscribeOptions.additionalHeaders).toBe(additionalHeaders); + // done(); + // }, + // }); + + // expect(observable).not.toBe(undefined); + // }); + + // TODO: + // test('happy case mutation', async () => { + // const spyonAuth = jest + // .spyOn(Credentials, 'get') + // .mockImplementationOnce(() => { + // return new Promise((res, rej) => { + // res('cred'); + // }); + // }); + + // const spyon = jest + // .spyOn(RestClient.prototype, 'post') + // .mockImplementationOnce((url, init) => { + // return new Promise((res, rej) => { + // res({}); + // }); + // }); + // // const api = new API(config); + // const client = generateClient(); + // const url = 'https://appsync.amazonaws.com', + // region = 'us-east-2', + // apiKey = 'secret_api_key', + // variables = { + // id: '809392da-ec91-4ef0-b219-5238a8f942b2', + // content: 'lalala', + // createdAt: new Date().toISOString(), + // }; + // api.configure({ + // aws_appsync_graphqlEndpoint: url, + // aws_appsync_region: region, + // aws_appsync_authenticationType: 'API_KEY', + // aws_appsync_apiKey: apiKey, + // }); + // const AddComment = `mutation AddComment($eventId: ID!, $content: String!, $createdAt: String!) { + // commentOnEvent(eventId: $eventId, content: $content, createdAt: $createdAt) { + // eventId + // content + // createdAt + // } + // }`; + + // const doc = parse(AddComment); + // const query = print(doc); + + // const headers = { + // Authorization: null, + // 'X-Api-Key': apiKey, + // // 'x-amz-user-agent': expectedUserAgentFrameworkOnly, + // }; + + // const body = { + // query, + // variables, + // }; + + // const init = { + // headers, + // body, + // signerServiceInfo: { + // service: 'appsync', + // region, + // }, + // cancellableToken: mockCancellableToken, + // }; + + // await api.graphql(graphqlOperation(AddComment, variables)); + + // expect(spyon).toBeCalledWith(url, init); + // }); + + test('happy case query with additionalHeaders', async () => { + /** + * Create a new client with unmocked Amplify imported from `core`. + * This is necessary to preserve the `libraryOptions` on the singleton + * (in this test case, headers passed via configuration options). + */ + const optionsClient = { + [__amplify]: AmplifyCore, + graphql, + cancel, + } as V6Client; + + Amplify.configure( + { + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }, + { + API: { + GraphQL: { + headers: async () => + Promise.resolve({ + someHeaderSetAtConfigThatWillBeOverridden: 'initialValue', + someOtherHeaderSetAtConfig: 'expectedValue', + }), + }, + }, + } + ); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const additionalHeaders = { + someAdditionalHeader: 'foo', + someHeaderSetAtConfigThatWillBeOverridden: 'expectedValue', + }; + + const result: GraphQLResult = await optionsClient.graphql( + { + query: typedQueries.getThread, + variables: graphqlVariables, + }, + additionalHeaders + ); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + expect(spy).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + url: new URL('https://localhost/graphql'), + options: expect.objectContaining({ + headers: expect.objectContaining({ + someAdditionalHeader: 'foo', + someHeaderSetAtConfigThatWillBeOverridden: 'expectedValue', + someOtherHeaderSetAtConfig: 'expectedValue', + }), + signingServiceInfo: null, + }), + }); + }); + + // TODO: + // test('sends cookies with request', async () => { + // const spyonAuth = jest + // .spyOn(Credentials, 'get') + // .mockImplementationOnce(() => { + // return new Promise((res, rej) => { + // res('cred'); + // }); + // }); + + // const spyon = jest + // .spyOn(RestClient.prototype, 'post') + // .mockImplementationOnce((url, init) => { + // return new Promise((res, rej) => { + // res({}); + // }); + // }); + + // // const api = new API(config); + // const client = generateClient(); + // const url = 'https://appsync.amazonaws.com', + // region = 'us-east-2', + // apiKey = 'secret_api_key', + // variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' }; + // api.configure({ + // aws_appsync_graphqlEndpoint: url, + // aws_appsync_region: region, + // aws_appsync_authenticationType: 'API_KEY', + // aws_appsync_apiKey: apiKey, + // withCredentials: true, + // }); + + // const headers = { + // Authorization: null, + // 'X-Api-Key': apiKey, + // // 'x-amz-user-agent': expectedUserAgentFrameworkOnly, + // }; + + // const body = { + // query: getEventQuery, + // variables, + // }; + + // const init = { + // headers, + // body, + // signerServiceInfo: { + // service: 'appsync', + // region, + // }, + // cancellableToken: mockCancellableToken, + // withCredentials: true, + // }; + // let authToken: undefined; + + // await api.graphql(graphqlOperation(GetEvent, variables, authToken)); + + // expect(spyon).toBeCalledWith(url, init); + // }); + }); }); diff --git a/packages/api-graphql/__tests__/resolveConfig.test.ts b/packages/api-graphql/__tests__/resolveConfig.test.ts new file mode 100644 index 00000000000..a1ec4b4ecb4 --- /dev/null +++ b/packages/api-graphql/__tests__/resolveConfig.test.ts @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { resolveConfig } from '../src/utils'; +import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; +import { AmplifyClassV6 } from '@aws-amplify/core'; + +describe('GraphQL API Util: resolveConfig', () => { + const GraphQLConfig = { + endpoint: 'https://test.us-west-2.amazonaws.com/graphql', + region: 'us-west-2', + apiKey: 'mock-api-key', + defaultAuthMode: { + type: 'apiKey' as GraphQLAuthMode, + apiKey: '0123456789', + }, + }; + + it('returns required config', () => { + const amplify = { + getConfig: jest.fn(() => { + return { + API: { GraphQL: GraphQLConfig }, + }; + }), + } as unknown as AmplifyClassV6; + + const expected = { + ...GraphQLConfig, + customEndpoint: undefined, + customEndpointRegion: undefined, + }; + + expect(resolveConfig(amplify)).toStrictEqual(expected); + }); + + it('throws if custom endpoint region exists without custom endpoint:', () => { + const amplify = { + getConfig: jest.fn(() => { + return { + API: { + GraphQL: { + ...GraphQLConfig, + customEndpoint: undefined, + customEndpointRegion: 'some-region', + }, + }, + }; + }), + } as unknown as AmplifyClassV6; + + expect(() => resolveConfig(amplify)).toThrow(); + }); +}); diff --git a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts index 0a616130850..542c84b5e66 100644 --- a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts @@ -8,7 +8,7 @@ import { Buffer } from 'buffer'; import { Hub, fetchAuthSession } from '@aws-amplify/core'; import { signRequest } from '@aws-amplify/core/internals/aws-client-utils'; import { - APIAuthMode, + GraphQLAuthMode, CustomUserAgentDetails, Logger, NonRetryableError, @@ -88,7 +88,7 @@ type ParsedMessagePayload = { export interface AWSAppSyncRealTimeProviderOptions { appSyncGraphqlEndpoint?: string; - authenticationType?: APIAuthMode; + authenticationType?: GraphQLAuthMode; query?: string; variables?: Record; apiKey?: string; @@ -884,7 +884,7 @@ export class AWSAppSyncRealTimeProvider { Record | undefined > { const headerHandler: { - [key in APIAuthMode]: (arg0: AWSAppSyncRealTimeAuthInput) => {}; + [key in GraphQLAuthMode]: (arg0: AWSAppSyncRealTimeAuthInput) => {}; } = { apiKey: this._awsRealTimeApiKeyHeader.bind(this), iam: this._awsRealTimeIAMHeader.bind(this), diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index 79d0d277ac5..ec60f965fbb 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -11,7 +11,7 @@ import { import { Observable } from 'rxjs'; import { AmplifyClassV6 } from '@aws-amplify/core'; import { - APIAuthMode, + GraphQLAuthMode, CustomUserAgentDetails, ConsoleLogger as Logger, getAmplifyUserAgent, @@ -29,6 +29,7 @@ import { updateRequestToBeCancellable, } from '@aws-amplify/api-rest/internals'; import { AWSAppSyncRealTimeProvider } from '../Providers/AWSAppSyncRealTimeProvider'; +import { resolveConfig, resolveLibraryOptions } from '../utils'; const USER_AGENT_HEADER = 'x-amz-user-agent'; @@ -54,7 +55,12 @@ export class InternalGraphQLAPIClass { private _options; private appSyncRealTime: AWSAppSyncRealTimeProvider | null; - private _api = { post, updateRequestToBeCancellable }; + private _api = { + post, + cancelREST, + isCancelErrorREST, + updateRequestToBeCancellable, + }; /** * Initialize GraphQL API with AWS configuration @@ -71,7 +77,7 @@ export class InternalGraphQLAPIClass { private async _headerBasedAuth( amplify: AmplifyClassV6, - authMode: APIAuthMode, + authMode: GraphQLAuthMode, additionalHeaders: { [key: string]: string } = {} ) { const config = amplify.getConfig(); @@ -80,7 +86,7 @@ export class InternalGraphQLAPIClass { endpoint: appSyncGraphqlEndpoint, apiKey, defaultAuthMode, - } = config.API?.GraphQL || {}; + } = resolveConfig(amplify); const authenticationType = authMode || defaultAuthMode || 'iam'; let headers = {}; @@ -130,9 +136,6 @@ export class InternalGraphQLAPIClass { case 'none': break; default: - headers = { - Authorization: null, - }; break; } @@ -218,25 +221,42 @@ export class InternalGraphQLAPIClass { abortController: AbortController, customUserAgentDetails?: CustomUserAgentDetails ): Promise> { - const config = amplify.getConfig(); - - const { region: region, endpoint: appSyncGraphqlEndpoint } = - config.API?.GraphQL || {}; + const { + region: region, + endpoint: appSyncGraphqlEndpoint, + customEndpoint, + customEndpointRegion, + } = resolveConfig(amplify); - const customGraphqlEndpoint = null; - const customEndpointRegion = null; + // Retrieve library options from Amplify configuration + const { headers: customHeaders, withCredentials } = + resolveLibraryOptions(amplify); // TODO: Figure what we need to do to remove `!`'s. const headers = { - ...(!customGraphqlEndpoint && + ...(!customEndpoint && (await this._headerBasedAuth(amplify, authMode!, additionalHeaders))), - ...((customGraphqlEndpoint && + /** + * Custom endpoint headers. + * If there is both a custom endpoint and custom region present, we get the headers. + * If there is a custom endpoint but no region, we return an empty object. + * If neither are present, we return an empty object. + */ + ...((customEndpoint && (customEndpointRegion ? await this._headerBasedAuth(amplify, authMode!, additionalHeaders) - : { Authorization: null })) || + : {})) || {}), + // Custom headers included in Amplify configuration options: + ...(customHeaders && + (await customHeaders({ + query: print(query as DocumentNode), + variables, + }))), + // Headers from individual calls to `graphql`: ...additionalHeaders, - ...(!customGraphqlEndpoint && { + // User agent headers: + ...(!customEndpoint && { [USER_AGENT_HEADER]: getAmplifyUserAgent(customUserAgentDetails), }), }; @@ -246,7 +266,31 @@ export class InternalGraphQLAPIClass { variables: variables || null, }; - const endpoint = customGraphqlEndpoint || appSyncGraphqlEndpoint; + let signingServiceInfo; + + /** + * We do not send the signing service info to the REST API under the + * following conditions (i.e. it will not sign the request): + * - there is a custom endpoint but no region + * - the auth mode is `none`, or `apiKey` + * - the auth mode is a type other than the types listed below + */ + if ( + (customEndpoint && !customEndpointRegion) || + (authMode !== 'oidc' && + authMode !== 'userPool' && + authMode !== 'iam' && + authMode !== 'lambda') + ) { + signingServiceInfo = null; + } else { + signingServiceInfo = { + service: !customEndpointRegion ? 'appsync' : 'execute-api', + region: !customEndpointRegion ? region : customEndpointRegion, + }; + } + + const endpoint = customEndpoint || appSyncGraphqlEndpoint; if (!endpoint) { const error = new GraphQLError('No graphql endpoint provided.'); @@ -264,10 +308,8 @@ export class InternalGraphQLAPIClass { options: { headers, body, - signingServiceInfo: { - service: 'appsync', - region, - }, + signingServiceInfo, + withCredentials, }, abortController, }); @@ -279,7 +321,7 @@ export class InternalGraphQLAPIClass { // If the exception is because user intentionally // cancelled the request, do not modify the exception // so that clients can identify the exception correctly. - if (isCancelErrorREST(err)) { + if (this._api.isCancelErrorREST(err)) { throw err; } @@ -304,7 +346,7 @@ export class InternalGraphQLAPIClass { * @return {boolean} - A boolean indicating if the error was from an api request cancellation */ isCancelError(error: any): boolean { - return isCancelErrorREST(error); + return this._api.isCancelErrorREST(error); } /** @@ -313,7 +355,7 @@ export class InternalGraphQLAPIClass { * @returns - A boolean indicating if the request was cancelled */ cancel(request: Promise, message?: string): boolean { - return cancelREST(request, message); + return this._api.cancelREST(request, message); } private _graphqlSubscribe( @@ -322,7 +364,8 @@ export class InternalGraphQLAPIClass { additionalHeaders = {}, customUserAgentDetails?: CustomUserAgentDetails ): Observable { - const { GraphQL } = amplify.getConfig().API ?? {}; + const config = resolveConfig(amplify); + if (!this.appSyncRealTime) { this.appSyncRealTime = new AWSAppSyncRealTimeProvider(); } @@ -330,10 +373,10 @@ export class InternalGraphQLAPIClass { { query: print(query as DocumentNode), variables, - appSyncGraphqlEndpoint: GraphQL?.endpoint, - region: GraphQL?.region, - authenticationType: authMode ?? GraphQL?.defaultAuthMode, - apiKey: GraphQL?.apiKey, + appSyncGraphqlEndpoint: config?.endpoint, + region: config?.region, + authenticationType: config?.defaultAuthMode, + apiKey: config?.apiKey, additionalHeaders, }, customUserAgentDetails diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index bb4f8d0a5ca..b878aedc5b6 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -5,7 +5,10 @@ import { Source, DocumentNode, GraphQLError } from 'graphql'; export { OperationTypeNode } from 'graphql'; import { Observable } from 'rxjs'; -import { APIAuthMode, DocumentType } from '@aws-amplify/core/internals/utils'; +import { + GraphQLAuthMode, + DocumentType, +} from '@aws-amplify/core/internals/utils'; export { CONTROL_MSG, ConnectionState } from './PubSub'; /** * Loose/Unknown options for raw GraphQLAPICategory `graphql()`. @@ -13,7 +16,7 @@ export { CONTROL_MSG, ConnectionState } from './PubSub'; export interface GraphQLOptions { query: string | DocumentNode; variables?: Record; - authMode?: APIAuthMode; + authMode?: GraphQLAuthMode; authToken?: string; /** * @deprecated This property should not be used @@ -159,7 +162,7 @@ export type GraphqlSubscriptionMessage = { export interface AWSAppSyncRealTimeProviderOptions { appSyncGraphqlEndpoint?: string; - authenticationType?: APIAuthMode; + authenticationType?: GraphQLAuthMode; query?: string; variables?: Record; apiKey?: string; @@ -198,7 +201,7 @@ export interface GraphQLOptionsV6< > { query: TYPED_GQL_STRING | DocumentNode; variables?: GraphQLVariablesV6; - authMode?: APIAuthMode; + authMode?: GraphQLAuthMode; authToken?: string; /** * @deprecated This property should not be used diff --git a/packages/api-graphql/src/utils/errors/validation.ts b/packages/api-graphql/src/utils/errors/validation.ts index 44ed721174d..e303c3730ec 100644 --- a/packages/api-graphql/src/utils/errors/validation.ts +++ b/packages/api-graphql/src/utils/errors/validation.ts @@ -4,23 +4,23 @@ import { AmplifyErrorMap } from '@aws-amplify/core/internals/utils'; export enum APIValidationErrorCode { - NoAppId = 'NoAppId', - NoCredentials = 'NoCredentials', + NoAuthSession = 'NoAuthSession', NoRegion = 'NoRegion', - NoDefaultAuthMode = 'NoDefaultAuthMode', + NoCustomEndpoint = 'NoCustomEndpoint', } export const validationErrorMap: AmplifyErrorMap = { - [APIValidationErrorCode.NoAppId]: { - message: 'Missing application id.', - }, - [APIValidationErrorCode.NoCredentials]: { - message: 'Credentials should not be empty.', + [APIValidationErrorCode.NoAuthSession]: { + message: 'Auth session should not be empty.', }, + // TODO: re-enable when working in all test environments: + // [APIValidationErrorCode.NoEndpoint]: { + // message: 'Missing endpoint', + // }, [APIValidationErrorCode.NoRegion]: { message: 'Missing region.', }, - [APIValidationErrorCode.NoDefaultAuthMode]: { - message: 'Missing default auth mode', + [APIValidationErrorCode.NoCustomEndpoint]: { + message: 'Custom endpoint region is present without custom endpoint.', }, }; diff --git a/packages/api-graphql/src/utils/index.ts b/packages/api-graphql/src/utils/index.ts index 0afb6284246..d995921b106 100644 --- a/packages/api-graphql/src/utils/index.ts +++ b/packages/api-graphql/src/utils/index.ts @@ -2,4 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { resolveConfig } from './resolveConfig'; -export { resolveCredentials } from './resolveCredentials'; +export { resolveLibraryOptions } from './resolveLibraryOptions'; diff --git a/packages/api-graphql/src/utils/resolveConfig.ts b/packages/api-graphql/src/utils/resolveConfig.ts index d824d9ba1cb..18337727fd4 100644 --- a/packages/api-graphql/src/utils/resolveConfig.ts +++ b/packages/api-graphql/src/utils/resolveConfig.ts @@ -1,20 +1,35 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyClassV6 } from '@aws-amplify/core'; import { APIValidationErrorCode, assertValidationError } from './errors'; /** * @internal */ -export const resolveConfig = () => { - const { region, defaultAuthMode, endpoint } = - Amplify.getConfig().API?.GraphQL ?? {}; - assertValidationError(!!endpoint, APIValidationErrorCode.NoAppId); - assertValidationError(!!region, APIValidationErrorCode.NoRegion); +export const resolveConfig = (amplify: AmplifyClassV6) => { + const { + apiKey, + customEndpoint, + customEndpointRegion, + defaultAuthMode, + endpoint, + region, + } = amplify.getConfig().API?.GraphQL ?? {}; + + // TODO: re-enable when working in all test environments: + // assertValidationError(!!endpoint, APIValidationErrorCode.NoEndpoint); assertValidationError( - !!defaultAuthMode, - APIValidationErrorCode.NoDefaultAuthMode + !(!customEndpoint && customEndpointRegion), + APIValidationErrorCode.NoCustomEndpoint ); - return { endpoint, region, defaultAuthMode }; + + return { + apiKey, + customEndpoint, + customEndpointRegion, + defaultAuthMode, + endpoint, + region, + }; }; diff --git a/packages/api-graphql/src/utils/resolveCredentials.ts b/packages/api-graphql/src/utils/resolveCredentials.ts deleted file mode 100644 index 3036c34d788..00000000000 --- a/packages/api-graphql/src/utils/resolveCredentials.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { fetchAuthSession } from '@aws-amplify/core'; -import { APIValidationErrorCode, assertValidationError } from './errors'; - -/** - * @internal - */ -export const resolveCredentials = async () => { - const { credentials, identityId } = await fetchAuthSession(); - assertValidationError(!!credentials, APIValidationErrorCode.NoCredentials); - return { credentials, identityId }; -}; diff --git a/packages/api-graphql/src/utils/resolveLibraryOptions.ts b/packages/api-graphql/src/utils/resolveLibraryOptions.ts new file mode 100644 index 00000000000..82702f5c16a --- /dev/null +++ b/packages/api-graphql/src/utils/resolveLibraryOptions.ts @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyClassV6 } from '@aws-amplify/core'; + +/** + * @internal + */ +export const resolveLibraryOptions = (amplify: AmplifyClassV6) => { + const headers = amplify.libraryOptions?.API?.GraphQL?.headers; + const withCredentials = amplify.libraryOptions?.API?.GraphQL?.withCredentials; + return { headers, withCredentials }; +}; diff --git a/packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts b/packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts new file mode 100644 index 00000000000..e53f52b1f93 --- /dev/null +++ b/packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts @@ -0,0 +1 @@ +export const cancel = jest.fn(() => true); diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 952a971e09c..71cd22f5207 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -1,170 +1,17 @@ -// import { RestAPIClass } from '@aws-amplify/api-rest'; -// import { InternalGraphQLAPIClass } from '@aws-amplify/api-graphql/internals'; -// import { APIClass as API } from '../src/API'; -// import { ApiAction, Category } from '@aws-amplify/core/internals/utils'; - -// describe('API test', () => { -// test('configure', () => { -// jest -// .spyOn(RestAPIClass.prototype, 'configure') -// .mockReturnValue({ restapi: 'configured' }); -// jest -// .spyOn(InternalGraphQLAPIClass.prototype, 'configure') -// .mockReturnValue({ graphqlapi: 'configured' }); -// const api = new API(null); -// expect(api.configure(null)).toStrictEqual({ -// graphqlapi: 'configured', -// restapi: 'configured', -// }); -// }); - -// test('get', async () => { -// const spy = jest -// .spyOn(RestAPIClass.prototype, 'get') -// .mockResolvedValue('getResponse'); -// const api = new API(null); -// expect(await api.get(null, null, null)).toBe('getResponse'); - -// expect(spy).toBeCalledWith(null, null, { -// customUserAgentDetails: { -// category: Category.API, -// action: ApiAction.Get, -// }, -// }); -// }); - -// test('post', async () => { -// const spy = jest -// .spyOn(RestAPIClass.prototype, 'post') -// .mockResolvedValue('postResponse'); -// const api = new API(null); -// expect(await api.post(null, null, null)).toBe('postResponse'); - -// expect(spy).toBeCalledWith(null, null, { -// customUserAgentDetails: { -// category: Category.API, -// action: ApiAction.Post, -// }, -// }); -// }); - -// test('put', async () => { -// const spy = jest -// .spyOn(RestAPIClass.prototype, 'put') -// .mockResolvedValue('putResponse'); -// const api = new API(null); -// expect(await api.put(null, null, null)).toBe('putResponse'); - -// expect(spy).toBeCalledWith(null, null, { -// customUserAgentDetails: { -// category: Category.API, -// action: ApiAction.Put, -// }, -// }); -// }); - -// test('patch', async () => { -// const spy = jest -// .spyOn(RestAPIClass.prototype, 'patch') -// .mockResolvedValue('patchResponse'); -// const api = new API(null); -// expect(await api.patch(null, null, null)).toBe('patchResponse'); - -// expect(spy).toBeCalledWith(null, null, { -// customUserAgentDetails: { -// category: Category.API, -// action: ApiAction.Patch, -// }, -// }); -// }); - -// test('del', async () => { -// jest.spyOn(RestAPIClass.prototype, 'del').mockResolvedValue('delResponse'); -// const api = new API(null); -// expect(await api.del(null, null, null)).toBe('delResponse'); -// }); - -// test('head', async () => { -// const spy = jest -// .spyOn(RestAPIClass.prototype, 'head') -// .mockResolvedValue('headResponse'); -// const api = new API(null); -// expect(await api.head(null, null, null)).toBe('headResponse'); - -// expect(spy).toBeCalledWith(null, null, { -// customUserAgentDetails: { -// category: Category.API, -// action: ApiAction.Head, -// }, -// }); -// }); - -// test('endpoint', async () => { -// jest -// .spyOn(RestAPIClass.prototype, 'endpoint') -// .mockResolvedValue('endpointResponse'); -// const api = new API(null); -// expect(await api.endpoint(null)).toBe('endpointResponse'); -// }); - -// test('getGraphqlOperationType', () => { -// jest -// .spyOn(InternalGraphQLAPIClass.prototype, 'getGraphqlOperationType') -// .mockReturnValueOnce('getGraphqlOperationTypeResponse' as any); -// const api = new API(null); -// expect(api.getGraphqlOperationType(null)).toBe( -// 'getGraphqlOperationTypeResponse' -// ); -// }); - -// test('graphql', async () => { -// const spy = jest -// .spyOn(InternalGraphQLAPIClass.prototype, 'graphql') -// .mockResolvedValue('grapqhqlResponse' as any); -// const api = new API(null); -// expect(await api.graphql({ query: 'query' })).toBe('grapqhqlResponse'); - -// expect(spy).toBeCalledWith(expect.anything(), undefined, { -// category: Category.API, -// action: ApiAction.GraphQl, -// }); -// }); - -// describe('cancel', () => { -// test('cancel RestAPI request', async () => { -// jest -// .spyOn(InternalGraphQLAPIClass.prototype, 'hasCancelToken') -// .mockImplementation(() => false); -// const restAPICancelSpy = jest -// .spyOn(RestAPIClass.prototype, 'cancel') -// .mockImplementation(() => true); -// jest -// .spyOn(RestAPIClass.prototype, 'hasCancelToken') -// .mockImplementation(() => true); -// const api = new API(null); -// const request = Promise.resolve(); -// expect(api.cancel(request)).toBe(true); -// expect(restAPICancelSpy).toHaveBeenCalled(); -// }); - -// test('cancel GraphQLAPI request', async () => { -// jest -// .spyOn(InternalGraphQLAPIClass.prototype, 'hasCancelToken') -// .mockImplementation(() => true); -// const graphQLAPICancelSpy = jest -// .spyOn(InternalGraphQLAPIClass.prototype, 'cancel') -// .mockImplementation(() => true); -// jest -// .spyOn(RestAPIClass.prototype, 'hasCancelToken') -// .mockImplementation(() => false); -// const api = new API(null); -// const request = Promise.resolve(); -// expect(api.cancel(request)).toBe(true); -// expect(graphQLAPICancelSpy).toHaveBeenCalled(); -// }); -// }); -// }); -// TODO(v6): add tests -describe.skip('API tests', () => { - test('add tests', async () => {}); +import { InternalGraphQLAPIClass } from '@aws-amplify/api-graphql/internals'; +import { generateClient } from 'aws-amplify/api'; + +describe('API generateClient', () => { + test('client.graphql', async () => { + const spy = jest + .spyOn(InternalGraphQLAPIClass.prototype, 'graphql') + .mockResolvedValue('grapqhqlResponse' as any); + const client = generateClient(); + expect(await client.graphql({ query: 'query' })).toBe('grapqhqlResponse'); + expect(spy).toBeCalledWith( + { Auth: {}, libraryOptions: {}, resourcesConfig: {} }, + { query: 'query' }, + undefined + ); + }); }); diff --git a/packages/aws-amplify/__tests__/exports.test.ts b/packages/aws-amplify/__tests__/exports.test.ts index 28740921bc9..2dad7377d59 100644 --- a/packages/aws-amplify/__tests__/exports.test.ts +++ b/packages/aws-amplify/__tests__/exports.test.ts @@ -43,6 +43,24 @@ describe('aws-amplify Exports', () => { }); }); + describe('API exports', () => { + it('should only export expected symbols from the top level', () => { + expect(Object.keys(apiTopLevelExports)).toMatchInlineSnapshot(` + Array [ + "generateClient", + "GraphQLAuthError", + "get", + "put", + "post", + "del", + "head", + "patch", + "isCancelError", + ] + `); + }); + }); + describe('Analytics exports', () => { it('should only export expected symbols from the top-level', () => { expect(Object.keys(analyticsTopLevelExports)).toMatchInlineSnapshot(` @@ -229,22 +247,4 @@ describe('aws-amplify Exports', () => { `); }); }); - - describe('API exports', () => { - it('should only export expected symbols from the top-level', () => { - expect(Object.keys(apiTopLevelExports)).toMatchInlineSnapshot(` - Array [ - "generateClient", - "GraphQLAuthError", - "get", - "put", - "post", - "del", - "head", - "patch", - "isCancelError", - ] - `); - }); - }); }); diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 28573ac5087..f62f55ba627 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -306,7 +306,7 @@ "name": "[Auth] signUp (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ signUp }", - "limit": "30.51 kB" + "limit": "30.52 kB" }, { "name": "[Auth] resetPassword (Cognito)", @@ -372,7 +372,7 @@ "name": "[Auth] setUpTOTP (Cognito)", "path": "./lib-esm/auth/index.js", "import": "{ setUpTOTP }", - "limit": "12.75 kB" + "limit": "12.76 kB" }, { "name": "[Auth] updateUserAttributes (Cognito)", @@ -426,7 +426,7 @@ "name": "[Storage] downloadData (S3)", "path": "./lib-esm/storage/index.js", "import": "{ downloadData }", - "limit": "18.30 kB" + "limit": "18.67 kB" }, { "name": "[Storage] getProperties (S3)", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e8f762e16b8..e62f745d581 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,6 +23,7 @@ export { AuthConfig, AuthUserPoolConfig, AuthUserPoolAndIdentityPoolConfig, + APIConfig, StorageAccessLevel, StorageConfig, GetCredentialsOptions, diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index a332e037b02..64436bcb684 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -29,7 +29,7 @@ export { assertOAuthConfig, } from './singleton/Auth/utils'; export { isTokenExpired } from './singleton/Auth'; -export { APIAuthMode, DocumentType } from './singleton/API/types'; +export { GraphQLAuthMode, DocumentType } from './singleton/API/types'; export { Signer } from './Signer'; export { JWT, diff --git a/packages/core/src/singleton/API/types.ts b/packages/core/src/singleton/API/types.ts index 6407ed3871f..99e8748f234 100644 --- a/packages/core/src/singleton/API/types.ts +++ b/packages/core/src/singleton/API/types.ts @@ -7,8 +7,9 @@ export type LibraryAPIOptions = { // custom headers for given GraphQL service. Will be applied to all operations. headers?: (options: { query: string; - variables: Record; + variables?: Record; }) => Promise; + withCredentials?: boolean; }; REST?: { // custom headers for given REST service. Will be applied to all operations. @@ -41,7 +42,7 @@ type APIGraphQLConfig = { /** * Default auth mode for all the API calls to given service. */ - defaultAuthMode: APIAuthMode; + defaultAuthMode: GraphQLAuthMode; }; type APIRestConfig = { @@ -69,7 +70,7 @@ export type APIConfig = { GraphQL?: APIGraphQLConfig; }; -export type APIAuthMode = +export type GraphQLAuthMode = | 'apiKey' | 'oidc' | 'userPool' diff --git a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts index 59dbf9a91eb..82b1998e065 100644 --- a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts +++ b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts @@ -8,7 +8,7 @@ import { ModelAttributeAuthAllow, AmplifyContext, } from '../types'; -import { APIAuthMode } from '@aws-amplify/core/internals/utils'; +import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; function getProviderFromRule( rule: ModelAttributeAuthProperty @@ -63,7 +63,7 @@ function getAuthRules({ currentUser: unknown; }) { // Using Set to ensure uniqueness - const authModes = new Set(); + const authModes = new Set(); rules.forEach(rule => { switch (rule.allow) { diff --git a/packages/datastore/src/sync/processors/mutation.ts b/packages/datastore/src/sync/processors/mutation.ts index 34193941d0e..74d111e4627 100644 --- a/packages/datastore/src/sync/processors/mutation.ts +++ b/packages/datastore/src/sync/processors/mutation.ts @@ -11,7 +11,7 @@ import { NonRetryableError, retry, BackgroundProcessManager, - APIAuthMode, + GraphQLAuthMode, AmplifyError, } from '@aws-amplify/core/internals/utils'; @@ -315,7 +315,7 @@ class MutationProcessor { modelConstructor: PersistentModelConstructor, MutationEvent: PersistentModelConstructor, mutationEvent: MutationEvent, - authMode: APIAuthMode, + authMode: GraphQLAuthMode, onTerminate: Promise ): Promise< [GraphQLResult>, string, SchemaModel] diff --git a/packages/datastore/src/sync/processors/subscription.ts b/packages/datastore/src/sync/processors/subscription.ts index dd021466064..aef830f3d6d 100644 --- a/packages/datastore/src/sync/processors/subscription.ts +++ b/packages/datastore/src/sync/processors/subscription.ts @@ -9,7 +9,7 @@ import { CustomUserAgentDetails, DataStoreAction, BackgroundProcessManager, - APIAuthMode, + GraphQLAuthMode, AmplifyError, JwtPayload, } from '@aws-amplify/core/internals/utils'; @@ -60,7 +60,7 @@ export enum USER_CREDENTIALS { } type AuthorizationInfo = { - authMode: APIAuthMode; + authMode: GraphQLAuthMode; isOwner: boolean; ownerField?: string; ownerValue?: string; @@ -97,13 +97,13 @@ class SubscriptionProcessor { transformerMutationType: TransformerMutationType, userCredentials: USER_CREDENTIALS, oidcTokenPayload: JwtPayload | undefined, - authMode: APIAuthMode, + authMode: GraphQLAuthMode, filterArg: boolean = false ): { opType: TransformerMutationType; opName: string; query: string; - authMode: APIAuthMode; + authMode: GraphQLAuthMode; isOwner: boolean; ownerField?: string; ownerValue?: string; @@ -132,9 +132,9 @@ class SubscriptionProcessor { private getAuthorizationInfo( model: SchemaModel, userCredentials: USER_CREDENTIALS, - defaultAuthType: APIAuthMode, + defaultAuthType: GraphQLAuthMode, oidcTokenPayload: JwtPayload | undefined, - authMode: APIAuthMode + authMode: GraphQLAuthMode ): AuthorizationInfo { const rules = getAuthorizationRules(model); // Return null if user doesn't have proper credentials for private API with IAM auth @@ -406,178 +406,173 @@ class SubscriptionProcessor { subscriptions[modelDefinition.name][ transformerMutationType ].push( - queryObservable - .subscribe({ - next: result => { - const { data, errors } = result; - if (Array.isArray(errors) && errors.length > 0) { - const messages = (< - { - message: string; - }[] - >errors).map(({ message }) => message); - - logger.warn( - `Skipping incoming subscription. Messages: ${messages.join( - '\n' - )}` - ); - - this.drainBuffer(); - return; - } + queryObservable.subscribe({ + next: result => { + const { data, errors } = result; + if (Array.isArray(errors) && errors.length > 0) { + const messages = (< + { + message: string; + }[] + >errors).map(({ message }) => message); + + logger.warn( + `Skipping incoming subscription. Messages: ${messages.join( + '\n' + )}` + ); - const predicatesGroup = - ModelPredicateCreator.getPredicates( - this.syncPredicates.get(modelDefinition)!, - false - ); - - // @ts-ignore - const { [opName]: record } = data; - - // checking incoming subscription against syncPredicate. - // once AppSync implements filters on subscriptions, we'll be - // able to set these when establishing the subscription instead. - // Until then, we'll need to filter inbound - if ( - this.passesPredicateValidation( - record, - predicatesGroup! - ) - ) { - this.pushToBuffer( - transformerMutationType, - modelDefinition, - record - ); - } this.drainBuffer(); - }, - error: async subscriptionError => { - const { - error: { errors: [{ message = '' } = {}] } = { - errors: [], - }, - } = subscriptionError; - - const isRTFError = - // only attempt catch if a filter variable was added to the subscription query - addFilter && - this.catchRTFError( - message, - modelDefinition, - predicatesGroup - ); - - // Catch RTF errors - if (isRTFError) { - // Unsubscribe and clear subscription array for model/operation - subscriptions[modelDefinition.name][ - transformerMutationType - ].forEach(subscription => - subscription.unsubscribe() - ); - - subscriptions[modelDefinition.name][ - transformerMutationType - ] = []; - - // retry subscription connection without filter - subscriptionRetry(operation, false); - return; - } - + return; + } + + const predicatesGroup = + ModelPredicateCreator.getPredicates( + this.syncPredicates.get(modelDefinition)!, + false + ); + + // @ts-ignore + const { [opName]: record } = data; + + // checking incoming subscription against syncPredicate. + // once AppSync implements filters on subscriptions, we'll be + // able to set these when establishing the subscription instead. + // Until then, we'll need to filter inbound + if ( + this.passesPredicateValidation( + record, + predicatesGroup! + ) + ) { + this.pushToBuffer( + transformerMutationType, + modelDefinition, + record + ); + } + this.drainBuffer(); + }, + error: async subscriptionError => { + const { + error: { errors: [{ message = '' } = {}] } = { + errors: [], + }, + } = subscriptionError; + + const isRTFError = + // only attempt catch if a filter variable was added to the subscription query + addFilter && + this.catchRTFError( + message, + modelDefinition, + predicatesGroup + ); + + // Catch RTF errors + if (isRTFError) { + // Unsubscribe and clear subscription array for model/operation + subscriptions[modelDefinition.name][ + transformerMutationType + ].forEach(subscription => + subscription.unsubscribe() + ); + + subscriptions[modelDefinition.name][ + transformerMutationType + ] = []; + + // retry subscription connection without filter + subscriptionRetry(operation, false); + return; + } + + if ( + message.includes( + PUBSUB_CONTROL_MSG.REALTIME_SUBSCRIPTION_INIT_ERROR + ) || + message.includes( + PUBSUB_CONTROL_MSG.CONNECTION_FAILED + ) + ) { + // Unsubscribe and clear subscription array for model/operation + subscriptions[modelDefinition.name][ + transformerMutationType + ].forEach(subscription => + subscription.unsubscribe() + ); + subscriptions[modelDefinition.name][ + transformerMutationType + ] = []; + + operationAuthModeAttempts[operation]++; if ( - message.includes( - PUBSUB_CONTROL_MSG.REALTIME_SUBSCRIPTION_INIT_ERROR - ) || - message.includes( - PUBSUB_CONTROL_MSG.CONNECTION_FAILED - ) + operationAuthModeAttempts[operation] >= + readAuthModes.length ) { - // Unsubscribe and clear subscription array for model/operation - subscriptions[modelDefinition.name][ - transformerMutationType - ].forEach(subscription => - subscription.unsubscribe() + // last auth mode retry. Continue with error + logger.debug( + `${operation} subscription failed with authMode: ${ + readAuthModes[ + operationAuthModeAttempts[operation] - 1 + ] + }` ); - subscriptions[modelDefinition.name][ - transformerMutationType - ] = []; - - operationAuthModeAttempts[operation]++; - if ( - operationAuthModeAttempts[operation] >= - readAuthModes.length - ) { - // last auth mode retry. Continue with error - logger.debug( - `${operation} subscription failed with authMode: ${ - readAuthModes[ - operationAuthModeAttempts[operation] - 1 - ] - }` - ); - } else { - // retry with different auth mode. Do not trigger - // observer error or error handler - logger.debug( - `${operation} subscription failed with authMode: ${ - readAuthModes[ - operationAuthModeAttempts[operation] - 1 - ] - }. Retrying with authMode: ${ - readAuthModes[ - operationAuthModeAttempts[operation] - ] - }` - ); - subscriptionRetry(operation); - return; - } - } - - logger.warn('subscriptionError', message); - - try { - await this.errorHandler({ - recoverySuggestion: - 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues', - localModel: null!, - message, - model: modelDefinition.name, - operation, - errorType: - getSubscriptionErrorType(subscriptionError), - process: ProcessName.subscribe, - remoteModel: null!, - cause: subscriptionError, - }); - } catch (e) { - logger.error( - 'Subscription error handler failed with:', - e + } else { + // retry with different auth mode. Do not trigger + // observer error or error handler + logger.debug( + `${operation} subscription failed with authMode: ${ + readAuthModes[ + operationAuthModeAttempts[operation] - 1 + ] + }. Retrying with authMode: ${ + readAuthModes[ + operationAuthModeAttempts[operation] + ] + }` ); - } - - if ( - typeof subscriptionReadyCallback === 'function' - ) { - subscriptionReadyCallback(); - } - - if ( - message.includes('"errorType":"Unauthorized"') || - message.includes( - '"errorType":"OperationDisabled"' - ) - ) { + subscriptionRetry(operation); return; } - observer.error(message); - }, - }) + } + + logger.warn('subscriptionError', message); + + try { + await this.errorHandler({ + recoverySuggestion: + 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues', + localModel: null!, + message, + model: modelDefinition.name, + operation, + errorType: + getSubscriptionErrorType(subscriptionError), + process: ProcessName.subscribe, + remoteModel: null!, + cause: subscriptionError, + }); + } catch (e) { + logger.error( + 'Subscription error handler failed with:', + e + ); + } + + if (typeof subscriptionReadyCallback === 'function') { + subscriptionReadyCallback(); + } + + if ( + message.includes('"errorType":"Unauthorized"') || + message.includes('"errorType":"OperationDisabled"') + ) { + return; + } + observer.error(message); + }, + }) ); promises.push( diff --git a/packages/datastore/src/sync/processors/sync.ts b/packages/datastore/src/sync/processors/sync.ts index 19d400dcd63..4e118356fd5 100644 --- a/packages/datastore/src/sync/processors/sync.ts +++ b/packages/datastore/src/sync/processors/sync.ts @@ -31,7 +31,7 @@ import { DataStoreAction, NonRetryableError, BackgroundProcessManager, - APIAuthMode, + GraphQLAuthMode, AmplifyError, } from '@aws-amplify/core/internals/utils'; @@ -204,7 +204,7 @@ class SyncProcessor { variables: { limit: number; lastSync: number; nextToken: string }; opName: string; modelDefinition: SchemaModel; - authMode: APIAuthMode; + authMode: GraphQLAuthMode; onTerminate: Promise; }): Promise< GraphQLResult<{ diff --git a/packages/datastore/src/sync/utils.ts b/packages/datastore/src/sync/utils.ts index fbe48cddc31..b9df4179b7c 100644 --- a/packages/datastore/src/sync/utils.ts +++ b/packages/datastore/src/sync/utils.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { GraphQLAuthError } from '@aws-amplify/api'; -import { Logger, APIAuthMode } from '@aws-amplify/core/internals/utils'; +import { Logger, GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; import { ModelInstanceCreator } from '../datastore/datastore'; import { AuthorizationRule, @@ -820,16 +820,16 @@ export async function getModelAuthModes({ schema, }: { authModeStrategy: AuthModeStrategy; - defaultAuthMode: APIAuthMode; + defaultAuthMode: GraphQLAuthMode; modelName: string; schema: InternalSchema; }): Promise<{ - [key in ModelOperation]: APIAuthMode[]; + [key in ModelOperation]: GraphQLAuthMode[]; }> { const operations = Object.values(ModelOperation); const modelAuthModes: { - [key in ModelOperation]: APIAuthMode[]; + [key in ModelOperation]: GraphQLAuthMode[]; } = { CREATE: [], READ: [], @@ -894,7 +894,7 @@ export function getClientSideAuthError(error) { } export async function getTokenForCustomAuth( - authMode: APIAuthMode, + authMode: GraphQLAuthMode, amplifyConfig: Record = {} ): Promise { if (authMode === 'lambda') { diff --git a/packages/datastore/src/types.ts b/packages/datastore/src/types.ts index eb4341653b2..9d8ceb1d899 100644 --- a/packages/datastore/src/types.ts +++ b/packages/datastore/src/types.ts @@ -17,7 +17,7 @@ import { import { PredicateAll } from './predicates'; import { InternalAPI } from '@aws-amplify/api/internals'; import { Adapter } from './storage/adapter'; -import { APIAuthMode } from '@aws-amplify/core/internals/utils'; +import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; export type Scalar = T extends Array ? InnerType : T; @@ -960,8 +960,8 @@ export enum AuthModeStrategyType { } export type AuthModeStrategyReturn = - | APIAuthMode - | APIAuthMode[] + | GraphQLAuthMode + | GraphQLAuthMode[] | undefined | null; @@ -985,7 +985,7 @@ export enum ModelOperation { export type ModelAuthModes = Record< string, { - [Property in ModelOperation]: APIAuthMode[]; + [Property in ModelOperation]: GraphQLAuthMode[]; } >; From e64ce428e3b8a879e67fd98ad4c3145374611255 Mon Sep 17 00:00:00 2001 From: Chris F <5827964+cshfang@users.noreply.github.com> Date: Wed, 11 Oct 2023 09:19:58 -0700 Subject: [PATCH 07/22] chore: clean up ClientDevice (#12228) * chore: clean up ClientDevice * Updated missed awsClient casing change --- packages/core/__tests__/ClientDevice.test.ts | 59 ------------------- .../pinpoint/apis/updateEndpoint.test.ts | 13 ++-- .../__tests__/utils/getClientInfo.test.ts | 42 +++++++++++++ packages/core/package.json | 1 - packages/core/src/ClientDevice/index.ts | 14 ----- packages/core/src/ClientDevice/reactnative.ts | 18 ------ packages/core/src/libraryUtils.ts | 1 - .../providers/pinpoint/apis/updateEndpoint.ts | 4 +- .../getClientInfo/getClientInfo.android.ts} | 4 +- .../getClientInfo/getClientInfo.ios.ts} | 6 +- .../getClientInfo/getClientInfo.ts} | 20 ++----- .../core/src/utils/getClientInfo/index.ts | 4 ++ packages/core/src/utils/index.ts | 1 + .../common/AWSPinpointProviderCommon/index.ts | 15 ++--- 14 files changed, 71 insertions(+), 131 deletions(-) delete mode 100644 packages/core/__tests__/ClientDevice.test.ts create mode 100644 packages/core/__tests__/utils/getClientInfo.test.ts delete mode 100644 packages/core/src/ClientDevice/index.ts delete mode 100644 packages/core/src/ClientDevice/reactnative.ts rename packages/core/src/{ClientDevice/android.ts => utils/getClientInfo/getClientInfo.android.ts} (83%) rename packages/core/src/{ClientDevice/ios.ts => utils/getClientInfo/getClientInfo.ios.ts} (91%) rename packages/core/src/{ClientDevice/browser.ts => utils/getClientInfo/getClientInfo.ts} (81%) create mode 100644 packages/core/src/utils/getClientInfo/index.ts diff --git a/packages/core/__tests__/ClientDevice.test.ts b/packages/core/__tests__/ClientDevice.test.ts deleted file mode 100644 index 46784fc8a7e..00000000000 --- a/packages/core/__tests__/ClientDevice.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ClientDevice } from '../src/ClientDevice'; -import { browserType } from '../src/ClientDevice/browser'; - -describe('ClientDevice', () => { - test('clientInfo', () => { - expect(ClientDevice.clientInfo()).toBeInstanceOf(Object); - }); - - test('clientInfo', () => { - const dimensions = ClientDevice.dimension(); - expect(typeof dimensions.width).toBe('number'); - expect(typeof dimensions.height).toBe('number'); - }); -}); - -describe('browserType', () => { - test('opera', () => { - expect( - browserType( - 'Opera/9.80 (Macintosh; Intel Mac OS X; U; en) Presto/2.2.15 Version/10.00' - ) - ).toStrictEqual({ - type: 'n', - version: '10.00', - }); - }); - - test('ie', () => { - expect( - browserType(`Internet Explorer 10 -Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)`) - ).toStrictEqual({ - type: 'Trident', - version: '6.0', - }); - }); - - test('safari', () => { - expect( - browserType( - `Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1` - ) - ).toStrictEqual({ - type: 'Safari', - version: '604.1', - }); - }); - - test('chrome', () => { - expect( - browserType( - `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36` - ) - ).toStrictEqual({ - type: 'Chrome', - version: '51.0.2704.103', - }); - }); -}); diff --git a/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts b/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts index 7536697f5bb..083d2b9b1ef 100644 --- a/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts +++ b/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { v4 } from 'uuid'; -import { ClientDevice } from '../../../../src/ClientDevice'; +import { getClientInfo } from '../../../../src/utils/getClientInfo'; import { updateEndpoint as clientUpdateEndpoint } from '../../../../src/awsClients/pinpoint'; import { cacheEndpointId, @@ -25,6 +25,7 @@ import { getExpectedInput } from './testUtils/getExpectedInput'; jest.mock('uuid'); jest.mock('../../../../src/awsClients/pinpoint'); jest.mock('../../../../src/providers/pinpoint/utils'); +jest.mock('../../../../src/utils/getClientInfo'); describe('Pinpoint Provider API: updateEndpoint', () => { const createdEndpointId = 'created-endpoint'; @@ -38,24 +39,26 @@ describe('Pinpoint Provider API: updateEndpoint', () => { platformVersion: 'user-platform-version', timezone: 'user-timezone', }; - // create spies - const clientInfoSpy = jest.spyOn(ClientDevice, 'clientInfo'); // assert mocks const mockCacheEndpointId = cacheEndpointId as jest.Mock; const mockClientUpdateEndpoint = clientUpdateEndpoint as jest.Mock; + const mockGetClientInfo = getClientInfo as jest.Mock; const mockGetEndpointId = getEndpointId as jest.Mock; const mockUuid = v4 as jest.Mock; beforeAll(() => { mockUuid.mockReturnValue(uuid); - clientInfoSpy.mockReturnValue(clientDemographic as any); + mockGetClientInfo.mockReturnValue(clientDemographic); }); beforeEach(() => { + mockGetEndpointId.mockReturnValue(endpointId); + }); + + afterEach(() => { mockCacheEndpointId.mockClear(); mockClientUpdateEndpoint.mockClear(); mockGetEndpointId.mockReset(); - mockGetEndpointId.mockReturnValue(endpointId); }); it('calls the service API with a baseline input', async () => { diff --git a/packages/core/__tests__/utils/getClientInfo.test.ts b/packages/core/__tests__/utils/getClientInfo.test.ts new file mode 100644 index 00000000000..b119ae423dd --- /dev/null +++ b/packages/core/__tests__/utils/getClientInfo.test.ts @@ -0,0 +1,42 @@ +import { getClientInfo } from '../../src/utils/getClientInfo'; + +describe('getClientInfo', () => { + // create spies + const userAgentSpy = jest.spyOn(window.navigator, 'userAgent', 'get'); + + afterEach(() => { + userAgentSpy.mockReset(); + }); + + it('gets opera info', () => { + userAgentSpy.mockReturnValue( + 'Opera/9.80 (Macintosh; Intel Mac OS X; U; en) Presto/2.2.15 Version/10.00' + ); + expect(getClientInfo().model).toBe('n'); + expect(getClientInfo().version).toBe('10.00'); + }); + + it('gets ie info', () => { + userAgentSpy.mockReturnValue( + 'Internet Explorer 10 Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)' + ); + expect(getClientInfo().model).toBe('Trident'); + expect(getClientInfo().version).toBe('6.0'); + }); + + it('gets safari info', () => { + userAgentSpy.mockReturnValue( + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1' + ); + expect(getClientInfo().model).toBe('Safari'); + expect(getClientInfo().version).toBe('604.1'); + }); + + it('gets safari info', () => { + userAgentSpy.mockReturnValue( + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36' + ); + expect(getClientInfo().model).toBe('Chrome'); + expect(getClientInfo().version).toBe('51.0.2704.103'); + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json index f11fb95f781..cbddaf77a2d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,6 @@ }, "react-native": { "./lib/index": "./lib-esm/index.js", - "./lib-esm/ClientDevice": "./lib-esm/ClientDevice/reactnative.js", "./lib-esm/Cache": "./lib-esm/Cache/reactnative.js" }, "repository": { diff --git a/packages/core/src/ClientDevice/index.ts b/packages/core/src/ClientDevice/index.ts deleted file mode 100644 index 3bd82927fd5..00000000000 --- a/packages/core/src/ClientDevice/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { clientInfo, dimension } from './browser'; - -export class ClientDevice { - static clientInfo() { - return clientInfo(); - } - - static dimension() { - return dimension(); - } -} diff --git a/packages/core/src/ClientDevice/reactnative.ts b/packages/core/src/ClientDevice/reactnative.ts deleted file mode 100644 index e1e8b588956..00000000000 --- a/packages/core/src/ClientDevice/reactnative.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// @ts-ignore: missing type definition -import { Platform } from 'react-native'; -import { clientInfo as iOSClientInfo } from './ios'; -import { clientInfo as androidClientInfo } from './android'; - -const { OS } = Platform; - -export class ClientDevice { - static clientInfo() { - if (OS === 'ios') { - return iOSClientInfo(); - } else { - return androidClientInfo(); - } - } -} diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 64436bcb684..944fdee0396 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -44,7 +44,6 @@ export { export { ConsoleLogger, ConsoleLogger as Logger } from './Logger'; // Platform & user-agent utilities -export { ClientDevice } from './ClientDevice'; export { Platform, getAmplifyUserAgentObject, diff --git a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts index 2700b79a861..63c38785b4a 100644 --- a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts +++ b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { v4 as uuidv4 } from 'uuid'; -import { ClientDevice } from '../../../ClientDevice'; +import { getClientInfo } from '../../../utils/getClientInfo'; import { updateEndpoint as clientUpdateEndpoint, UpdateEndpointInput, @@ -39,7 +39,7 @@ export const updateEndpoint = async ({ name, plan, } = userProfile ?? {}; - const clientInfo = ClientDevice.clientInfo(); + const clientInfo = getClientInfo(); const mergedDemographic = { appVersion: clientInfo.appVersion, make: clientInfo.make, diff --git a/packages/core/src/ClientDevice/android.ts b/packages/core/src/utils/getClientInfo/getClientInfo.android.ts similarity index 83% rename from packages/core/src/ClientDevice/android.ts rename to packages/core/src/utils/getClientInfo/getClientInfo.android.ts index b4b26d56d60..cb842903ca8 100644 --- a/packages/core/src/ClientDevice/android.ts +++ b/packages/core/src/utils/getClientInfo/getClientInfo.android.ts @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // @ts-ignore: missing type definition import { Platform, Dimensions } from 'react-native'; -import { ConsoleLogger as Logger } from '../Logger'; +import { ConsoleLogger as Logger } from '../../Logger'; const logger = new Logger('DeviceInfo'); -export const clientInfo = () => { +export const getClientInfo = () => { const dim = Dimensions.get('screen'); logger.debug(Platform, dim); diff --git a/packages/core/src/ClientDevice/ios.ts b/packages/core/src/utils/getClientInfo/getClientInfo.ios.ts similarity index 91% rename from packages/core/src/ClientDevice/ios.ts rename to packages/core/src/utils/getClientInfo/getClientInfo.ios.ts index 2610673f8de..de2edb02a2e 100644 --- a/packages/core/src/ClientDevice/ios.ts +++ b/packages/core/src/utils/getClientInfo/getClientInfo.ios.ts @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // @ts-ignore: missing type definition import { Platform, Dimensions } from 'react-native'; -import { ConsoleLogger as Logger } from '../Logger'; +import { ConsoleLogger as Logger } from '../../Logger'; const logger = new Logger('DeviceInfo'); -export const clientInfo = () => { +export const getClientInfo = () => { const dim = Dimensions.get('screen'); logger.debug(Platform, dim); const OS = 'ios'; @@ -21,7 +21,7 @@ export const clientInfo = () => { }; }; -function dimToMake(dim:{height:number, width:number;}) { +function dimToMake(dim: { height: number; width: number }) { let { height, width } = dim; if (height < width) { const tmp = height; diff --git a/packages/core/src/ClientDevice/browser.ts b/packages/core/src/utils/getClientInfo/getClientInfo.ts similarity index 81% rename from packages/core/src/ClientDevice/browser.ts rename to packages/core/src/utils/getClientInfo/getClientInfo.ts index 35ef99e3023..ca573f397a6 100644 --- a/packages/core/src/ClientDevice/browser.ts +++ b/packages/core/src/utils/getClientInfo/getClientInfo.ts @@ -1,10 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ConsoleLogger as Logger } from '../Logger'; +import { ConsoleLogger as Logger } from '../../Logger'; const logger = new Logger('ClientDevice_Browser'); -export function clientInfo() { +export function getClientInfo() { if (typeof window === 'undefined') { return {}; } @@ -25,7 +25,7 @@ function browserClientInfo() { } const { platform, product, vendor, userAgent, language } = nav; - const type = browserType(userAgent); + const type = getBrowserType(userAgent); const timezone = browserTimezone(); return { @@ -39,24 +39,12 @@ function browserClientInfo() { }; } -export function dimension() { - if (typeof window === 'undefined') { - logger.warn('No window object available to get browser client info'); - return { width: 320, height: 320 }; - } - - return { - width: window.innerWidth, - height: window.innerHeight, - }; -} - function browserTimezone() { const tzMatch = /\(([A-Za-z\s].*)\)/.exec(new Date().toString()); return tzMatch ? tzMatch[1] || '' : ''; } -export function browserType(userAgent: string) { +function getBrowserType(userAgent: string) { const operaMatch = /.+(Opera[\s[A-Z]*|OPR[\sA-Z]*)\/([0-9\.]+).*/i.exec( userAgent ); diff --git a/packages/core/src/utils/getClientInfo/index.ts b/packages/core/src/utils/getClientInfo/index.ts new file mode 100644 index 00000000000..b7657c0b22e --- /dev/null +++ b/packages/core/src/utils/getClientInfo/index.ts @@ -0,0 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { getClientInfo } from './getClientInfo'; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 8192605cd1c..dc5975c73ca 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export { generateRandomString } from './generateRandomString'; +export { getClientInfo } from './getClientInfo'; export { isBrowser } from './isBrowser'; export { isWebWorker } from './isWebWorker'; export { diff --git a/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts b/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts index 5bcb0e74918..52189c78569 100644 --- a/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts +++ b/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts @@ -3,7 +3,6 @@ import { Category, - ClientDevice, ConsoleLogger, CustomUserAgentDetails, getAmplifyUserAgent, @@ -35,15 +34,12 @@ export default abstract class AWSPinpointProviderCommon static category: NotificationsCategory = 'Notifications'; static providerName = 'AWSPinpoint'; - protected clientInfo; protected config: Record = {}; protected endpointInitialized = false; protected initialized = false; protected logger: ConsoleLogger; constructor(logger) { - // this.config = { storage: new StorageHelper().getStorage() }; - this.clientInfo = ClientDevice.clientInfo() ?? {}; this.logger = logger; } @@ -185,7 +181,6 @@ export default abstract class AWSPinpointProviderCommon try { const { address, attributes, demographic, location, metrics, optOut } = userInfo ?? {}; - const { appVersion, make, model, platform, version } = this.clientInfo; // Create the UpdateEndpoint input, prioritizing passed in user info and falling back to // defaults (if any) obtained from the config const input: UpdateEndpointInput = { @@ -201,11 +196,11 @@ export default abstract class AWSPinpointProviderCommon ...attributes, }, Demographic: { - AppVersion: appVersion, - Make: make, - Model: model, - ModelVersion: version, - Platform: platform, + AppVersion: null, + Make: null, + Model: null, + ModelVersion: null, + Platform: null, ...this.transferKeyToUpperCase({ ...endpointInfo.demographic, ...demographic, From b369d70c9dac3db516b84351f588d661703aee3c Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Wed, 11 Oct 2023 10:37:49 -0700 Subject: [PATCH 08/22] docs(api-rest): in-line documentation (#12197) --- packages/api-rest/src/apis/index.ts | 179 ++++++++++++++++++ packages/api-rest/src/apis/server.ts | 126 ++++++++++++ packages/api-rest/src/errors/CanceledError.ts | 5 +- .../cognito/apis/resendSignUpCode.ts | 10 +- .../providers/cognito/apis/resetPassword.ts | 10 +- .../src/providers/cognito/types/models.ts | 8 +- .../src/providers/cognito/types/outputs.ts | 2 +- .../clients/CognitoIdentityProvider/base.ts | 6 +- .../core/__tests__/parseAWSExports.test.ts | 33 ++-- .../__tests__/singleton/Singleton.test.ts | 12 +- packages/core/src/parseAWSExports.ts | 68 ++++--- 11 files changed, 397 insertions(+), 62 deletions(-) diff --git a/packages/api-rest/src/apis/index.ts b/packages/api-rest/src/apis/index.ts index 1e95d80f2ae..a51f49d0afb 100644 --- a/packages/api-rest/src/apis/index.ts +++ b/packages/api-rest/src/apis/index.ts @@ -24,37 +24,216 @@ import { PutInput, PutOperation, } from '../types'; +import { RestApiError } from '../errors'; /** * GET HTTP request + * @param {GetInput} input - Input for GET operation + * @returns {GetOperation} Operation for GET request + * @throws - {@link RestApiError} + * @example + * Send a GET request + * ```js + * import { get, isCancelError } from '@aws-amplify/api'; + * + * const { body } = await get({ + * apiName, + * path, + * options: { + * headers, // Optional, A map of custom header key/values + * body, // Optional, JSON object or FormData + * queryParams, // Optional, A map of query strings + * } + * }).response; + * const data = await body.json(); + * ``` + * @example + * Cancel a GET request + * + * ```js + * import { get, isCancelError } from '@aws-amplify/api'; + * + * const { response, cancel } = get({apiName, path, options}); + * cancel(message); + * try { + * await response; + * } cache (e) { + * if (isCancelError(e)) { + * // handle request cancellation + * } + * //... + * } + * ``` */ export const get = (input: GetInput): GetOperation => commonGet(Amplify, input); /** * POST HTTP request + * @param {PostInput} input - Input for POST operation + * @returns {PostOperation} Operation for POST request + * @throws - {@link RestApiError} + * @example + * Send a POST request + * ```js + * import { post, isCancelError } from '@aws-amplify/api'; + * + * const { body } = await post({ + * apiName, + * path, + * options: { + * headers, // Optional, A map of custom header key/values + * body, // Optional, JSON object or FormData + * queryParams, // Optional, A map of query strings + * } + * }).response; + * const data = await body.json(); + * ``` + * @example + * Cancel a POST request + * + * ```js + * import { post, isCancelError } from '@aws-amplify/api'; + * + * const { response, cancel } = post({apiName, path, options}); + * cancel(message); + * try { + * await response; + * } cache (e) { + * if (isCancelError(e)) { + * // handle request cancellation + * } + * //... + * } + * ``` */ export const post = (input: PostInput): PostOperation => commonPost(Amplify, input); /** * PUT HTTP request + * @param {PutInput} input - Input for PUT operation + * @returns {PutOperation} Operation for PUT request + * @throws - {@link RestApiError} + * @example + * Send a PUT request + * ```js + * import { put, isCancelError } from '@aws-amplify/api'; + * + * const { body } = await put({ + * apiName, + * path, + * options: { + * headers, // Optional, A map of custom header key/values + * body, // Optional, JSON object or FormData + * queryParams, // Optional, A map of query strings + * } + * }).response; + * const data = await body.json(); + * ``` + * @example + * Cancel a PUT request + * ```js + * import { put, isCancelError } from '@aws-amplify/api'; + * + * const { response, cancel } = put({apiName, path, options}); + * cancel(message); + * try { + * await response; + * } cache (e) { + * if (isCancelError(e)) { + * // handle request cancellation + * } + * //... + * } + * ``` */ export const put = (input: PutInput): PutOperation => commonPut(Amplify, input); /** * DELETE HTTP request + * @param {DeleteInput} input - Input for DELETE operation + * @returns {DeleteOperation} Operation for DELETE request + * @throws - {@link RestApiError} + * @example + * Send a DELETE request + * ```js + * import { del } from '@aws-amplify/api'; + * + * const { statusCode } = await del({ + * apiName, + * path, + * options: { + * headers, // Optional, A map of custom header key/values + * queryParams, // Optional, A map of query strings + * } + * }).response; + * ``` */ export const del = (input: DeleteInput): DeleteOperation => commonDel(Amplify, input); /** * HEAD HTTP request + * @param {HeadInput} input - Input for HEAD operation + * @returns {HeadOperation} Operation for HEAD request + * @throws - {@link RestApiError} + * @example + * Send a HEAD request + * ```js + * import { head, isCancelError } from '@aws-amplify/api'; + * + * const { headers, statusCode } = await head({ + * apiName, + * path, + * options: { + * headers, // Optional, A map of custom header key/values + * queryParams, // Optional, A map of query strings + * } + * }),response; + * ``` + * */ export const head = (input: HeadInput): HeadOperation => commonHead(Amplify, input); /** * PATCH HTTP request + * @param {PatchInput} input - Input for PATCH operation + * @returns {PatchOperation} Operation for PATCH request + * @throws - {@link RestApiError} + * @example + * Send a PATCH request + * ```js + * import { patch } from '@aws-amplify/api'; + * + * const { body } = await patch({ + * apiName, + * path, + * options: { + * headers, // Optional, A map of custom header key/values + * body, // Optional, JSON object or FormData + * queryParams, // Optional, A map of query strings + * } + * }).response; + * const data = await body.json(); + * ``` + * + * @example + * Cancel a PATCH request + * ```js + * import { patch, isCancelError } from '@aws-amplify/api'; + * + * const { response, cancel } = patch({apiName, path, options}); + * cancel(message); + * try { + * await response; + * } cache (e) { + * if (isCancelError(e)) { + * // handle request cancellation + * } + * //... + * } + * ``` */ export const patch = (input: PatchInput): PatchOperation => commonPatch(Amplify, input); diff --git a/packages/api-rest/src/apis/server.ts b/packages/api-rest/src/apis/server.ts index 763e0da28dc..3afe1bb9e32 100644 --- a/packages/api-rest/src/apis/server.ts +++ b/packages/api-rest/src/apis/server.ts @@ -27,9 +27,32 @@ import { PutInput, PutOperation, } from '../types'; +import { RestApiError } from '../errors'; /** * GET HTTP request (server-side) + * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. + * @param {GetInput} input - Input for GET operation. + * @throws - {@link RestApiError} + * @example + * Send a GET request + * ```js + * import { get } from 'aws-amplify/api/server'; + * //... + * const restApiResponse = await runWithAmplifyServerContext({ + * nextServerContext: { request, response }, + * operation: async (contextSpec) => { + * try { + * const { body } = await get(contextSpec, input).response; + * return await body.json(); + * } catch (error) { + * console.log(error); + * return false; + * } + * }, + * }); + * ``` + * @see {@link clientGet} */ export const get = ( contextSpec: AmplifyServer.ContextSpec, @@ -39,6 +62,27 @@ export const get = ( /** * POST HTTP request (server-side) + * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. + * @param {PostInput} input - Input for POST operation. + * @throws - {@link RestApiError} + * @example + * Send a POST request + * ```js + * import { post } from 'aws-amplify/api/server'; + * //... + * const restApiResponse = await runWithAmplifyServerContext({ + * nextServerContext: { request, response }, + * operation: async (contextSpec) => { + * try { + * const { body } = await post(contextSpec, input).response; + * return await body.json(); + * } catch (error) { + * console.log(error); + * return false; + * } + * }, + * }); + * ``` */ export const post = ( contextSpec: AmplifyServer.ContextSpec, @@ -48,6 +92,27 @@ export const post = ( /** * PUT HTTP request (server-side) + * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. + * @param {PutInput} input - Input for PUT operation. + * @throws - {@link RestApiError} + * @example + * Send a PUT request + * ```js + * import { put } from 'aws-amplify/api/server'; + * //... + * const restApiResponse = await runWithAmplifyServerContext({ + * nextServerContext: { request, response }, + * operation: async (contextSpec) => { + * try { + * const { body } = await put(contextSpec, input).response; + * return await body.json(); + * } catch (error) { + * console.log(error); + * return false; + * } + * }, + * }); + * ``` */ export const put = ( contextSpec: AmplifyServer.ContextSpec, @@ -57,6 +122,26 @@ export const put = ( /** * DELETE HTTP request (server-side) + * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. + * @param {DeleteInput} input - Input for DELETE operation. + * @throws - {@link RestApiError} + * @example + * Send a DELETE request + * ```js + * import { del } from 'aws-amplify/api/server'; + * //... + * const restApiResponse = await runWithAmplifyServerContext({ + * nextServerContext: { request, response }, + * operation: async (contextSpec) => { + * try { + * const { headers } = await del(contextSpec, input).response; + * } catch (error) { + * console.log(error); + * return false; + * } + * }, + * }); + * ``` */ export const del = ( contextSpec: AmplifyServer.ContextSpec, @@ -66,6 +151,26 @@ export const del = ( /** * HEAD HTTP request (server-side) + * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. + * @param {HeadInput} input - Input for HEAD operation. + * @throws - {@link RestApiError} + * @example + * Send a HEAD request + * ```js + * import { head } from 'aws-amplify/api/server'; + * //... + * const restApiResponse = await runWithAmplifyServerContext({ + * nextServerContext: { request, response }, + * operation: async (contextSpec) => { + * try { + * const { headers } = await head(contextSpec, input).response; + * } catch (error) { + * console.log(error); + * return false; + * } + * }, + * }); + * ``` */ export const head = ( contextSpec: AmplifyServer.ContextSpec, @@ -75,6 +180,27 @@ export const head = ( /** * PATCH HTTP request (server-side) + * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. + * @param {PatchInput} input - Input for PATCH operation. + * @throws - {@link RestApiError} + * @example + * Send a PATCH request + * ```js + * import { patch } from 'aws-amplify/api/server'; + * //... + * const restApiResponse = await runWithAmplifyServerContext({ + * nextServerContext: { request, response }, + * operation: async (contextSpec) => { + * try { + * const { body } = await patch(contextSpec, input).response; + * return await body.json(); + * } catch (error) { + * console.log(error); + * return false; + * } + * }, + * }); + * ``` */ export const patch = ( contextSpec: AmplifyServer.ContextSpec, diff --git a/packages/api-rest/src/errors/CanceledError.ts b/packages/api-rest/src/errors/CanceledError.ts index aa2a89562b6..254c2c69eef 100644 --- a/packages/api-rest/src/errors/CanceledError.ts +++ b/packages/api-rest/src/errors/CanceledError.ts @@ -24,7 +24,10 @@ export class CanceledError extends RestApiError { } /** - * Check if an error is caused by user calling `cancel()` REST API. + * Check if an error is caused by user calling `cancel()` in REST API. + * + * @note This function works **ONLY** for errors thrown by REST API. For GraphQL APIs, use `client.isCancelError(error)` + * instead. `client` is generated from `generateClient()` API from `aws-amplify/api`. */ export const isCancelError = (error: unknown): error is CanceledError => !!error && error instanceof CanceledError; diff --git a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts index 10907b09527..a6fde7bf439 100644 --- a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts +++ b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction, AuthStandardAttributeKey } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, + AuthStandardAttributeKey, +} from '@aws-amplify/core/internals/utils'; import { AuthDeliveryMedium } from '../../../types'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -32,9 +36,9 @@ export async function resendSignUpCode( assertTokenProviderConfig(authConfig); const clientMetadata = input.options?.serviceOptions?.clientMetadata; const { CodeDeliveryDetails } = await resendConfirmationCode( - { + { region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ResendSignUpCode) + userAgentValue: getAuthUserAgentValue(AuthAction.ResendSignUpCode), }, { Username: username, diff --git a/packages/auth/src/providers/cognito/apis/resetPassword.ts b/packages/auth/src/providers/cognito/apis/resetPassword.ts index 5144a723d9d..185d862e054 100644 --- a/packages/auth/src/providers/cognito/apis/resetPassword.ts +++ b/packages/auth/src/providers/cognito/apis/resetPassword.ts @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction, AuthStandardAttributeKey } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, + AuthStandardAttributeKey, +} from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { AuthDeliveryMedium } from '../../../types'; @@ -35,9 +39,9 @@ export async function resetPassword( assertTokenProviderConfig(authConfig); const clientMetadata = input.options?.serviceOptions?.clientMetadata; const res = await forgotPassword( - { + { region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ResetPassword) + userAgentValue: getAuthUserAgentValue(AuthAction.ResetPassword), }, { Username: username, diff --git a/packages/auth/src/providers/cognito/types/models.ts b/packages/auth/src/providers/cognito/types/models.ts index d6c0ff24511..43abfc243d2 100644 --- a/packages/auth/src/providers/cognito/types/models.ts +++ b/packages/auth/src/providers/cognito/types/models.ts @@ -1,11 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AuthStandardAttributeKey, AuthVerifiableAttributeKey } from "@aws-amplify/core/internals/utils"; import { - AuthUserAttribute, - AuthDevice, -} from '../../../types'; + AuthStandardAttributeKey, + AuthVerifiableAttributeKey, +} from '@aws-amplify/core/internals/utils'; +import { AuthUserAttribute, AuthDevice } from '../../../types'; import { AuthProvider } from '../../../types/inputs'; import { SignInOutput, SignUpOutput } from './outputs'; diff --git a/packages/auth/src/providers/cognito/types/outputs.ts b/packages/auth/src/providers/cognito/types/outputs.ts index 52c03038a35..27d4f38fda9 100644 --- a/packages/auth/src/providers/cognito/types/outputs.ts +++ b/packages/auth/src/providers/cognito/types/outputs.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AuthStandardAttributeKey } from "@aws-amplify/core/internals/utils"; +import { AuthStandardAttributeKey } from '@aws-amplify/core/internals/utils'; import { AuthMFAType, AuthUserAttributes, diff --git a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts index 9bdbf611ff4..d053d052b6c 100644 --- a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts +++ b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts @@ -29,11 +29,13 @@ const SERVICE_NAME = 'cognito-idp'; const endpointResolver = ({ region }: EndpointResolverOptions) => { const authConfig = Amplify.getConfig().Auth?.Cognito; const customURL = authConfig?.endpoint; - const defaultURL = new URL(`https://${SERVICE_NAME}.${region}.${getDnsSuffix(region)}`); + const defaultURL = new URL( + `https://${SERVICE_NAME}.${region}.${getDnsSuffix(region)}` + ); return { url: customURL ? new URL(customURL) : defaultURL, - } + }; }; /** diff --git a/packages/core/__tests__/parseAWSExports.test.ts b/packages/core/__tests__/parseAWSExports.test.ts index cc3d634a2ac..2122efcf39f 100644 --- a/packages/core/__tests__/parseAWSExports.test.ts +++ b/packages/core/__tests__/parseAWSExports.test.ts @@ -71,21 +71,16 @@ describe('Parser', () => { aws_cognito_username_attributes: ['PHONE_NUMBER'], aws_cognito_signup_attributes: ['PHONE_NUMBER'], aws_cognito_mfa_configuration: 'OFF', - aws_cognito_mfa_types: [ - 'SMS', - 'TOTP' - ], + aws_cognito_mfa_types: ['SMS', 'TOTP'], aws_cognito_password_protection_settings: { passwordPolicyMinLength: 8, passwordPolicyCharacters: [ - 'REQUIRES_SYMBOLS', - 'REQUIRES_UPPERCASE', - 'REQUIRES_NUMBERS' - ] + 'REQUIRES_SYMBOLS', + 'REQUIRES_UPPERCASE', + 'REQUIRES_NUMBERS', + ], }, - aws_cognito_verification_mechanisms: [ - 'EMAIL' - ], + aws_cognito_verification_mechanisms: ['EMAIL'], aws_mandatory_sign_in: 'enable', aws_mobile_analytics_app_id: appId, aws_mobile_analytics_app_region: region, @@ -124,7 +119,7 @@ describe('Parser', () => { loginWith: { email: false, phone: true, - username: false + username: false, }, mfa: { smsEnabled: true, @@ -136,20 +131,20 @@ describe('Parser', () => { requireLowercase: false, requireNumbers: true, requireSpecialCharacters: true, - requireUppercase: true + requireUppercase: true, }, signUpVerificationMethod, userAttributes: [ { - 'email': { - required: true + email: { + required: true, }, }, { - 'phone_number': { - required: true - } - } + phone_number: { + required: true, + }, + }, ], userPoolId, userPoolClientId, diff --git a/packages/core/__tests__/singleton/Singleton.test.ts b/packages/core/__tests__/singleton/Singleton.test.ts index 3a91307688f..cf65ba0a156 100644 --- a/packages/core/__tests__/singleton/Singleton.test.ts +++ b/packages/core/__tests__/singleton/Singleton.test.ts @@ -43,7 +43,7 @@ describe('Amplify.configure() and Amplify.getConfig()', () => { loginWith: { email: false, phone: false, - username: true + username: true, }, mfa: { smsEnabled: true, @@ -55,15 +55,15 @@ describe('Amplify.configure() and Amplify.getConfig()', () => { requireLowercase: false, requireNumbers: false, requireSpecialCharacters: false, - requireUppercase: false + requireUppercase: false, }, userAttributes: [ { phone_number: { - required: true - } - } - ] + required: true, + }, + }, + ], }, }, }); diff --git a/packages/core/src/parseAWSExports.ts b/packages/core/src/parseAWSExports.ts index 881b02e89bf..c9465b1f0c6 100644 --- a/packages/core/src/parseAWSExports.ts +++ b/packages/core/src/parseAWSExports.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ConsoleLogger as Logger } from './Logger'; -import { OAuthConfig, AuthStandardAttributeKey, AuthConfigUserAttributes } from './singleton/Auth/types'; +import { + OAuthConfig, + AuthStandardAttributeKey, + AuthConfigUserAttributes, +} from './singleton/Auth/types'; import { ResourcesConfig } from './singleton/types'; const logger = new Logger('parseAWSExports'); @@ -99,35 +103,52 @@ export const parseAWSExports = ( } // Auth - const mfaConfig = aws_cognito_mfa_configuration ? { - status: aws_cognito_mfa_configuration && aws_cognito_mfa_configuration.toLowerCase(), - totpEnabled: aws_cognito_mfa_types?.includes('TOTP') ?? false, - smsEnabled: aws_cognito_mfa_types?.includes('SMS') ?? false - } : undefined; - const passwordFormatConfig = aws_cognito_password_protection_settings ? { - minLength: aws_cognito_password_protection_settings.passwordPolicyMinLength, - requireLowercase: - aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_LOWERCASE') ?? false, - requireUppercase: - aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_UPPERCASE') ?? false, - requireNumbers: - aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_NUMBERS') ?? false, - requireSpecialCharacters: - aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes('REQUIRES_SYMBOLS') ?? false, - } : undefined; + const mfaConfig = aws_cognito_mfa_configuration + ? { + status: + aws_cognito_mfa_configuration && + aws_cognito_mfa_configuration.toLowerCase(), + totpEnabled: aws_cognito_mfa_types?.includes('TOTP') ?? false, + smsEnabled: aws_cognito_mfa_types?.includes('SMS') ?? false, + } + : undefined; + const passwordFormatConfig = aws_cognito_password_protection_settings + ? { + minLength: + aws_cognito_password_protection_settings.passwordPolicyMinLength, + requireLowercase: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes( + 'REQUIRES_LOWERCASE' + ) ?? false, + requireUppercase: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes( + 'REQUIRES_UPPERCASE' + ) ?? false, + requireNumbers: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes( + 'REQUIRES_NUMBERS' + ) ?? false, + requireSpecialCharacters: + aws_cognito_password_protection_settings.passwordPolicyCharacters?.includes( + 'REQUIRES_SYMBOLS' + ) ?? false, + } + : undefined; const mergedUserAttributes = Array.from( new Set([ ...(aws_cognito_verification_mechanisms ?? []), - ...(aws_cognito_signup_attributes ?? []) + ...(aws_cognito_signup_attributes ?? []), ]) ); const userAttributesConfig = mergedUserAttributes.map((s: string) => ({ [s.toLowerCase()]: { - required: true // All user attributes generated by the CLI will be required - } + required: true, // All user attributes generated by the CLI will be required + }, })) as unknown as AuthConfigUserAttributes; - const loginWithEmailEnabled = aws_cognito_username_attributes?.includes('EMAIL') ?? false; - const loginWithPhoneEnabled = aws_cognito_username_attributes?.includes('PHONE_NUMBER') ?? false; + const loginWithEmailEnabled = + aws_cognito_username_attributes?.includes('EMAIL') ?? false; + const loginWithPhoneEnabled = + aws_cognito_username_attributes?.includes('PHONE_NUMBER') ?? false; if (aws_cognito_identity_pool_id || aws_user_pools_id) { amplifyConfig.Auth = { Cognito: { @@ -140,7 +161,8 @@ export const parseAWSExports = ( mfa: mfaConfig, passwordFormat: passwordFormatConfig, loginWith: { - username: (loginWithEmailEnabled || loginWithPhoneEnabled) ? false : true, + username: + loginWithEmailEnabled || loginWithPhoneEnabled ? false : true, email: loginWithEmailEnabled, phone: loginWithPhoneEnabled, ...(oauth && From e5b60ee588cd477182a4ea6965910fd1d299b785 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 11 Oct 2023 12:14:07 -0700 Subject: [PATCH 09/22] test(analytics): add integration test config for Personalize (#12255) --- .github/integ-config/integ-all.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index a0623191edb..08c0116ed42 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -565,6 +565,24 @@ tests: # Temp fix: browser: *minimal_browser_list + - test_name: integ_react_analytics_personalize_auth + desc: 'Test record API for Personalize with authenticated user' + framework: react + category: analytics + sample_name: [personalize-test] + spec: personalize + # Temp fix: + browser: *minimal_browser_list + + - test_name: integ_react_analytics_personalize_unauth + desc: 'Test record API for Personalize with guest user' + framework: react + category: analytics + sample_name: [personalize-test] + spec: personalize-unauth + # Temp fix: + browser: *minimal_browser_list + - test_name: integ_react_analytics_kinesis_data_firehose_auth desc: 'Test record API for KDF with authenticated user' framework: react From d298a26418cdfb4b0d3a05bebccaaaca9e76b845 Mon Sep 17 00:00:00 2001 From: Francisco Rodriguez Date: Wed, 11 Oct 2023 12:44:45 -0700 Subject: [PATCH 10/22] feat: Cognito Advance Security features (#12262) * feat(auth): Cognito Advance Security Feature --- .../cognito/confirmResetPassword.test.ts | 48 +++ .../cognito/confirmSignInHappyCases.test.ts | 280 ++++++++++++++++++ .../providers/cognito/confirmSignUp.test.ts | 58 ++++ .../providers/cognito/refreshToken.test.ts | 75 +++++ .../cognito/resendSignUpCode.test.ts | 57 +++- .../providers/cognito/resetPassword.test.ts | 51 ++++ .../cognito/signInWithCustomAuth.test.ts | 61 ++++ .../cognito/signInWithCustomSRPAuth.test.ts | 62 ++++ .../providers/cognito/signInWithSRP.test.ts | 53 ++++ .../cognito/signInWithUserPassword.test.ts | 58 ++++ .../cognito/apis/confirmResetPassword.ts | 21 +- .../providers/cognito/apis/confirmSignUp.ts | 10 +- .../cognito/apis/resendSignUpCode.ts | 10 + .../providers/cognito/apis/resetPassword.ts | 10 + .../tokenProvider/TokenOrchestrator.ts | 5 + .../cognito/tokenProvider/TokenStore.ts | 2 +- .../providers/cognito/tokenProvider/types.ts | 3 + .../cognito/utils/refreshAuthTokens.ts | 11 + .../providers/cognito/utils/signInHelpers.ts | 81 +++++ .../cognito/utils/userContextData.native.ts | 16 + .../cognito/utils/userContextData.ts | 33 +++ 21 files changed, 995 insertions(+), 10 deletions(-) create mode 100644 packages/auth/src/providers/cognito/utils/userContextData.native.ts create mode 100644 packages/auth/src/providers/cognito/utils/userContextData.ts diff --git a/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts b/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts index da6ce682974..b84cb712514 100644 --- a/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts @@ -134,3 +134,51 @@ describe('ConfirmResetPassword API error path cases', () => { } }); }); + +describe('Cognito ASF', () => { + let confirmForgotPasswordSpy; + + beforeEach(() => { + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + confirmForgotPasswordSpy = jest + .spyOn(confirmResetPasswordClient, 'confirmForgotPassword') + .mockImplementationOnce(async () => { + return authAPITestParams.confirmResetPasswordHttpCallResult; + }); + }); + + afterEach(() => { + confirmForgotPasswordSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + test('Check UserContextData is added', async () => { + await confirmResetPassword({ + username: 'username', + newPassword: 'password', + confirmationCode: 'code', + options: { + serviceOptions: { + clientMetadata: { fooo: 'fooo' }, + }, + }, + }); + expect(confirmForgotPasswordSpy).toHaveBeenCalledWith( + expect.objectContaining({ region: 'us-west-2' }), + expect.objectContaining({ + Username: 'username', + ConfirmationCode: 'code', + Password: 'password', + ClientMetadata: { fooo: 'fooo' }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + UserContextData: { + EncodedData: 'abcd', + }, + }) + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts index 15369cce05c..3afb23d3ee8 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts @@ -14,6 +14,7 @@ import { CognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; jest.mock('../../../src/providers/cognito/apis/getCurrentUser'); const authConfig = { @@ -59,6 +60,10 @@ describe('confirmSignIn API happy path cases', () => { handleChallengeNameSpy.mockClear(); }); + afterAll(() => { + jest.restoreAllMocks(); + }); + test(`confirmSignIn test SMS_MFA ChallengeName.`, async () => { Amplify.configure({ Auth: authConfig, @@ -261,3 +266,278 @@ describe('confirmSignIn API happy path cases', () => { handleUserSRPAuthFlowSpy.mockClear(); }); }); + +describe('Cognito ASF', () => { + let respondToAuthChallengeSpy; + let handleUserSRPAuthFlowSpy; + + const username = authAPITestParams.user1.username; + const password = authAPITestParams.user1.password; + beforeEach(() => { + Amplify.configure({ + Auth: authConfig, + }); + + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + + respondToAuthChallengeSpy = jest + .spyOn(clients, 'respondToAuthChallenge') + .mockImplementation( + async (): Promise => { + return { + Session: '1234234232', + $metadata: {}, + ChallengeName: undefined, + ChallengeParameters: {}, + AuthenticationResult: { + AccessToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0', + ExpiresIn: 1000, + IdToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0', + RefreshToken: 'qwersfsafsfssfasf', + }, + }; + } + ); + }); + + afterEach(() => { + respondToAuthChallengeSpy.mockClear(); + handleUserSRPAuthFlowSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + test('SMS_MFA challengeCheck UserContextData is added', async () => { + handleUserSRPAuthFlowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'SMS_MFA', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + CODE_DELIVERY_DELIVERY_MEDIUM: 'SMS_MFA', + CODE_DELIVERY_DESTINATION: 'aaa@awsamplify.com', + }, + }) + ); + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe('CONFIRM_SIGN_IN_WITH_SMS_CODE'); + try { + await confirmSignIn({ + challengeResponse: '777', + }); + } catch (err) { + console.log(err); + } + + expect(respondToAuthChallengeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + ChallengeName: 'SMS_MFA', + ChallengeResponses: { SMS_MFA_CODE: '777', USERNAME: 'user1' }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + ClientMetadata: undefined, + Session: '1234234232', + UserContextData: { EncodedData: 'abcd' }, + }) + ); + }); + + test('SELECT_MFA_TYPE challengeCheck UserContextData is added', async () => { + handleUserSRPAuthFlowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'SELECT_MFA_TYPE', + Session: '1234234232', + ChallengeParameters: { + MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA"]', + }, + $metadata: {}, + }) + ); + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe( + 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION' + ); + try { + await confirmSignIn({ + challengeResponse: 'SMS', + }); + } catch (err) { + console.log(err); + } + + expect(respondToAuthChallengeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + ChallengeName: 'SELECT_MFA_TYPE', + ChallengeResponses: { + ANSWER: 'SMS_MFA', + USERNAME: 'user1', + }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + ClientMetadata: undefined, + Session: '1234234232', + UserContextData: { EncodedData: 'abcd' }, + }) + ); + }); + + test(`confirmSignIn tests MFA_SETUP sends UserContextData`, async () => { + Amplify.configure({ + Auth: authConfig, + }); + const handleUserSRPAuthflowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'SOFTWARE_TOKEN_MFA', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: {}, + }) + ); + + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe('CONFIRM_SIGN_IN_WITH_TOTP_CODE'); + try { + await confirmSignIn({ + challengeResponse: '123456', + }); + } catch (err) { + console.log(err); + } + + expect(respondToAuthChallengeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + ChallengeName: 'SOFTWARE_TOKEN_MFA', + ChallengeResponses: { + SOFTWARE_TOKEN_MFA_CODE: '123456', + USERNAME: 'user1', + }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + ClientMetadata: undefined, + Session: '1234234232', + UserContextData: { EncodedData: 'abcd' }, + }) + ); + }); + + test(`confirmSignIn tests NEW_PASSWORD_REQUIRED sends UserContextData`, async () => { + Amplify.configure({ + Auth: authConfig, + }); + const handleUserSRPAuthflowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'NEW_PASSWORD_REQUIRED', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: {}, + }) + ); + + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe( + 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED' + ); + try { + await confirmSignIn({ + challengeResponse: 'password', + }); + } catch (err) { + console.log(err); + } + + expect(respondToAuthChallengeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + ChallengeName: 'NEW_PASSWORD_REQUIRED', + ChallengeResponses: { + NEW_PASSWORD: 'password', + USERNAME: 'user1', + }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + ClientMetadata: undefined, + Session: '1234234232', + UserContextData: { EncodedData: 'abcd' }, + }) + ); + }); + test(`confirmSignIn tests CUSTOM_CHALLENGE sends UserContextData`, async () => { + Amplify.configure({ + Auth: authConfig, + }); + const handleUserSRPAuthflowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'CUSTOM_CHALLENGE', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: {}, + }) + ); + + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe( + 'CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE' + ); + try { + await confirmSignIn({ + challengeResponse: 'secret-answer', + }); + } catch (err) { + console.log(err); + } + + expect(respondToAuthChallengeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + ChallengeName: 'CUSTOM_CHALLENGE', + ChallengeResponses: { + ANSWER: 'secret-answer', + USERNAME: 'user1', + }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + ClientMetadata: undefined, + Session: '1234234232', + UserContextData: { EncodedData: 'abcd' }, + }) + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts index 55657207274..eeede4231aa 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts @@ -37,6 +37,9 @@ describe('confirmSignUp API Happy Path Cases:', () => { afterEach(() => { confirmSignUpClientSpy.mockClear(); }); + afterAll(() => { + jest.restoreAllMocks(); + }); test('confirmSignUp API should call the UserPoolClient and should return a SignUpResult', async () => { const result = await confirmSignUp({ username: user1.username, @@ -148,3 +151,58 @@ describe('confirmSignUp API Error Path Cases:', () => { } }); }); + +describe('Cognito ASF', () => { + Amplify.configure({ + Auth: authConfig, + }); + let confirmSignUpClientSpy; + const { user1 } = authAPITestParams; + const confirmationCode = '123456'; + beforeEach(() => { + confirmSignUpClientSpy = jest + .spyOn(confirmSignUpClient, 'confirmSignUp') + .mockImplementationOnce(async (): Promise => { + return {} as ConfirmSignUpCommandOutput; + }); + + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + confirmSignUpClientSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + afterAll(() => { + jest.restoreAllMocks(); + }); + test('confirmSignUp should send UserContextData', async () => { + const result = await confirmSignUp({ + username: user1.username, + confirmationCode, + }); + expect(result).toEqual({ + isSignUpComplete: true, + nextStep: { + signUpStep: 'DONE', + }, + }); + expect(confirmSignUpClientSpy).toHaveBeenCalledWith( + expect.objectContaining({ region: 'us-west-2' }), + { + ClientMetadata: undefined, + ConfirmationCode: confirmationCode, + Username: user1.username, + ForceAliasCreation: undefined, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + UserContextData: { EncodedData: 'abcd' }, + } + ); + expect(confirmSignUpClientSpy).toBeCalledTimes(1); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/refreshToken.test.ts b/packages/auth/__tests__/providers/cognito/refreshToken.test.ts index 6fca42a86e8..30e523ab134 100644 --- a/packages/auth/__tests__/providers/cognito/refreshToken.test.ts +++ b/packages/auth/__tests__/providers/cognito/refreshToken.test.ts @@ -3,6 +3,8 @@ import { fetchTransferHandler } from '@aws-amplify/core/internals/aws-client-uti import { mockJsonResponse, mockRequestId } from './testUtils/data'; import { refreshAuthTokens } from '../../../src/providers/cognito/utils/refreshAuthTokens'; import { CognitoAuthTokens } from '../../../src/providers/cognito/tokenProvider/types'; +import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; + jest.mock('@aws-amplify/core/lib/clients/handlers/fetch'); describe('refresh token tests', () => { @@ -87,3 +89,76 @@ describe('refresh token tests', () => { ); }); }); + +describe('Cognito ASF', () => { + let initiateAuthSpy; + let tokenProviderSpy; + afterAll(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + initiateAuthSpy = jest + .spyOn(clients, 'initiateAuth') + .mockImplementationOnce(async () => ({ + AuthenticationResult: { + AccessToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0', + ExpiresIn: 3600, + IdToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0', + TokenType: 'Bearer', + }, + ChallengeParameters: {}, + $metadata: { + attempts: 1, + httpStatusCode: 200, + requestId: mockRequestId, + }, + })); + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + initiateAuthSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + + test('refreshTokens API should send UserContextData', async () => { + const response = await refreshAuthTokens({ + username: 'username', + tokens: { + accessToken: decodeJWT( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0' + ), + idToken: decodeJWT( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0' + ), + clockDrift: 0, + refreshToken: 'refreshtoken', + username: 'username', + }, + authConfig: { + Cognito: { + userPoolId: 'us-east-1_aaaaaaa', + userPoolClientId: 'aaaaaaaaaaaa', + }, + }, + }); + expect(initiateAuthSpy).toBeCalledWith( + expect.objectContaining({ + region: 'us-east-1', + }), + expect.objectContaining({ + AuthFlow: 'REFRESH_TOKEN_AUTH', + AuthParameters: { REFRESH_TOKEN: 'refreshtoken' }, + ClientId: 'aaaaaaaaaaaa', + UserContextData: { EncodedData: 'abcd' }, + }) + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts b/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts index 9414c1f6161..59903595d72 100644 --- a/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts +++ b/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts @@ -34,15 +34,18 @@ describe('ResendSignUp API Happy Path Cases:', () => { afterEach(() => { resendSignUpSpy.mockClear(); }); + afterAll(() => { + jest.restoreAllMocks(); + }); test('ResendSignUp API should call the UserPoolClient and should return a ResendSignUpCodeResult', async () => { const result = await resendSignUpCode({ username: user1.username, }); expect(result).toEqual(authAPITestParams.resendSignUpAPIResult); expect(resendSignUpSpy).toHaveBeenCalledWith( - { + { region: 'us-west-2', - userAgentValue: expect.any(String) + userAgentValue: expect.any(String), }, { ClientMetadata: undefined, @@ -56,7 +59,9 @@ describe('ResendSignUp API Happy Path Cases:', () => { describe('ResendSignUp API Error Path Cases:', () => { const { user1 } = authAPITestParams; - + afterAll(() => { + jest.restoreAllMocks(); + }); test('ResendSignUp API should throw a validation AuthError when username is empty', async () => { expect.assertions(2); try { @@ -89,3 +94,49 @@ describe('ResendSignUp API Error Path Cases:', () => { }); describe('ResendSignUp API Edge Cases:', () => {}); + +describe('Cognito ASF', () => { + let resendSignUpSpy; + const { user1 } = authAPITestParams; + beforeEach(() => { + resendSignUpSpy = jest + .spyOn(resendSignUpConfirmationCodeClient, 'resendConfirmationCode') + .mockImplementationOnce(async () => { + return authAPITestParams.resendSignUpClientResult as ResendConfirmationCodeCommandOutput; + }); + + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + resendSignUpSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + afterAll(() => { + jest.restoreAllMocks(); + }); + test('ResendSignUp API should send UserContextData', async () => { + const result = await resendSignUpCode({ + username: user1.username, + }); + expect(result).toEqual(authAPITestParams.resendSignUpAPIResult); + expect(resendSignUpSpy).toHaveBeenCalledWith( + { + region: 'us-west-2', + userAgentValue: expect.any(String), + }, + { + ClientMetadata: undefined, + Username: user1.username, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + UserContextData: { EncodedData: 'abcd' }, + } + ); + expect(resendSignUpSpy).toBeCalledTimes(1); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/resetPassword.test.ts b/packages/auth/__tests__/providers/cognito/resetPassword.test.ts index f47fcd423f4..f1bee1b5f4e 100644 --- a/packages/auth/__tests__/providers/cognito/resetPassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/resetPassword.test.ts @@ -35,6 +35,10 @@ describe('ResetPassword API happy path cases', () => { resetPasswordSpy.mockClear(); }); + afterAll(() => { + jest.restoreAllMocks(); + }); + test('ResetPassword API should call the UserPoolClient and should return a ResetPasswordResult', async () => { const result = await resetPassword(authAPITestParams.resetPasswordRequest); expect(result).toEqual(authAPITestParams.resetPasswordResult); @@ -92,3 +96,50 @@ describe('ResetPassword API error path cases:', () => { } }); }); + +describe('Cognito ASF', () => { + let resetPasswordSpy; + + beforeEach(() => { + resetPasswordSpy = jest + .spyOn(resetPasswordClient, 'forgotPassword') + .mockImplementationOnce(async () => { + return authAPITestParams.resetPasswordHttpCallResult as ForgotPasswordCommandOutput; + }); + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + resetPasswordSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + test('ResetPassword API should send UserContextData', async () => { + await resetPassword({ + username: 'username', + options: { + serviceOptions: { + clientMetadata: { foo: 'foo' }, + }, + }, + }); + expect(resetPasswordSpy).toHaveBeenCalledWith( + expect.objectContaining({ region: 'us-west-2' }), + expect.objectContaining({ + Username: 'username', + ClientMetadata: { foo: 'foo' }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + UserContextData: { EncodedData: 'abcd' }, + }) + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts b/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts index 76ed87b8109..f6feebc4bae 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts @@ -11,6 +11,7 @@ import { CognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; const authConfig = { Cognito: { @@ -26,6 +27,9 @@ CognitoUserPoolsTokenProvider.setAuthConfig(authConfig); describe('signIn API happy path cases', () => { let handleCustomAuthFlowWithoutSRPSpy; + afterAll(() => { + jest.restoreAllMocks(); + }); beforeEach(() => { handleCustomAuthFlowWithoutSRPSpy = jest .spyOn(initiateAuthHelpers, 'handleCustomAuthFlowWithoutSRP') @@ -76,3 +80,60 @@ describe('signIn API happy path cases', () => { ); }); }); + +describe('Cognito ASF', () => { + let initiateAuthSpy; + + afterAll(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + initiateAuthSpy = jest + .spyOn(clients, 'initiateAuth') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'SMS_MFA', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + CODE_DELIVERY_DELIVERY_MEDIUM: 'SMS', + CODE_DELIVERY_DESTINATION: '*******9878', + }, + }) + ); + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + initiateAuthSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + + test('signIn API should send UserContextData', async () => { + const result = await signIn({ + username: authAPITestParams.user1.username, + options: { + serviceOptions: { + authFlowType: 'CUSTOM_WITHOUT_SRP', + }, + }, + }); + expect(initiateAuthSpy).toBeCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + { + AuthFlow: 'CUSTOM_AUTH', + AuthParameters: { USERNAME: 'user1' }, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + ClientMetadata: undefined, + UserContextData: { EncodedData: 'abcd' }, + } + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts b/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts index 11033d07988..8027eb42cdd 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts @@ -11,6 +11,7 @@ import { CognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; const authConfig = { Cognito: { @@ -40,6 +41,10 @@ describe('signIn API happy path cases', () => { handleCustomSRPAuthFlowSpy.mockClear(); }); + afterAll(() => { + jest.restoreAllMocks(); + }); + test('signIn API invoked with CUSTOM_WITH_SRP authFlowType should return a SignInResult', async () => { const result = await signIn({ username: authAPITestParams.user1.username, @@ -82,3 +87,60 @@ describe('signIn API happy path cases', () => { ); }); }); + +describe('Cognito ASF', () => { + let initiateAuthSpy; + + afterAll(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + initiateAuthSpy = jest + .spyOn(clients, 'initiateAuth') + .mockImplementationOnce(async () => ({ + ChallengeName: 'SRP_AUTH', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + USER_ID_FOR_SRP: authAPITestParams.user1.username, + }, + })); + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + initiateAuthSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + + test('signIn API invoked with CUSTOM_WITH_SRP should send UserContextData', async () => { + try { + await signIn({ + username: authAPITestParams.user1.username, + password: authAPITestParams.user1.password, + options: { + serviceOptions: { + authFlowType: 'CUSTOM_WITH_SRP', + }, + }, + }); + } catch (_) { + // only want to test the contents + } + expect(initiateAuthSpy).toBeCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + UserContextData: { + EncodedData: 'abcd', + }, + }) + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts index b69fa4cc71a..314bcad956d 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts @@ -11,6 +11,7 @@ import { CognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; const authConfig = { Cognito: { @@ -91,3 +92,55 @@ describe('signIn API happy path cases', () => { ); }); }); + +describe('Cognito ASF', () => { + let initiateAuthSpy; + + afterAll(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + initiateAuthSpy = jest + .spyOn(clients, 'initiateAuth') + .mockImplementationOnce(async () => ({ + ChallengeName: 'SRP_AUTH', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + USER_ID_FOR_SRP: authAPITestParams.user1.username, + }, + })); + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + initiateAuthSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + + test('signIn SRP should send UserContextData', async () => { + try { + await signIn({ + username: authAPITestParams.user1.username, + password: authAPITestParams.user1.password, + }); + } catch (_) { + // only want to test the contents + } + expect(initiateAuthSpy).toBeCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + UserContextData: { + EncodedData: 'abcd', + }, + }) + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts b/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts index 20b8efaa6fd..14d700bb137 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts @@ -11,6 +11,7 @@ import { CognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; const authConfig = { Cognito: { @@ -72,3 +73,60 @@ describe('signIn API happy path cases', () => { ); }); }); + +describe('Cognito ASF', () => { + let initiateAuthSpy; + + afterAll(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + initiateAuthSpy = jest + .spyOn(clients, 'initiateAuth') + .mockImplementationOnce(async () => ({ + ChallengeName: 'SRP_AUTH', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + USER_ID_FOR_SRP: authAPITestParams.user1.username, + }, + })); + // load Cognito ASF polyfill + window['AmazonCognitoAdvancedSecurityData'] = { + getData() { + return 'abcd'; + }, + }; + }); + + afterEach(() => { + initiateAuthSpy.mockClear(); + window['AmazonCognitoAdvancedSecurityData'] = undefined; + }); + + test('signIn API should send UserContextData', async () => { + try { + await signIn({ + username: authAPITestParams.user1.username, + password: authAPITestParams.user1.password, + options: { + serviceOptions: { + authFlowType: 'USER_PASSWORD_AUTH', + }, + }, + }); + } catch (_) { + // only want to test the contents + } + expect(initiateAuthSpy).toBeCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + UserContextData: { + EncodedData: 'abcd', + }, + }) + ); + }); +}); diff --git a/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts b/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts index ae7205189bc..4fb01eae8b5 100644 --- a/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts +++ b/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, +} from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { ConfirmResetPasswordInput } from '../types'; @@ -10,6 +13,7 @@ import { confirmForgotPassword } from '../utils/clients/CognitoIdentityProvider' import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; import { ConfirmForgotPasswordException } from '../../cognito/types/errors'; import { getAuthUserAgentValue } from '../../../utils'; +import { getUserContextData } from '../utils/userContextData'; /** * Confirms the new password and verification code to reset the password. * @@ -25,7 +29,7 @@ export async function confirmResetPassword( ): Promise { const authConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(authConfig); - + const { userPoolClientId, userPoolId } = authConfig; const { username, newPassword } = input; assertValidationError( !!username, @@ -43,10 +47,16 @@ export async function confirmResetPassword( ); const metadata = input.options?.serviceOptions?.clientMetadata; + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + await confirmForgotPassword( - { - region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmResetPassword) + { + region: getRegion(authConfig.userPoolId), + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmResetPassword), }, { Username: username, @@ -54,6 +64,7 @@ export async function confirmResetPassword( Password: newPassword, ClientMetadata: metadata, ClientId: authConfig.userPoolClientId, + UserContextData: UserContextData, } ); } diff --git a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts index 9d8e340f0e9..1ad47aff0e0 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts @@ -20,6 +20,7 @@ import { setAutoSignInStarted, } from '../utils/signUpHelpers'; import { getAuthUserAgentValue } from '../../../utils'; +import { getUserContextData } from '../utils/userContextData'; /** * Confirms a new user account. @@ -39,6 +40,7 @@ export async function confirmSignUp( const authConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(authConfig); + const { userPoolId, userPoolClientId } = authConfig; const clientMetadata = options?.serviceOptions?.clientMetadata; assertValidationError( !!username, @@ -49,6 +51,12 @@ export async function confirmSignUp( AuthValidationErrorCode.EmptyConfirmSignUpCode ); + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + await confirmSignUpClient( { region: getRegion(authConfig.userPoolId), @@ -60,7 +68,7 @@ export async function confirmSignUp( ClientMetadata: clientMetadata, ForceAliasCreation: options?.serviceOptions?.forceAliasCreation, ClientId: authConfig.userPoolClientId, - // TODO: handle UserContextData + UserContextData, } ); diff --git a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts index a6fde7bf439..fcb5f8153c1 100644 --- a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts +++ b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts @@ -14,6 +14,7 @@ import { ResendSignUpCodeInput, ResendSignUpCodeOutput } from '../types'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; import { resendConfirmationCode } from '../utils/clients/CognitoIdentityProvider'; import { getAuthUserAgentValue } from '../../../utils'; +import { getUserContextData } from '../utils/userContextData'; /** * Resend the confirmation code while signing up @@ -34,7 +35,15 @@ export async function resendSignUpCode( ); const authConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(authConfig); + const { userPoolClientId, userPoolId } = authConfig; const clientMetadata = input.options?.serviceOptions?.clientMetadata; + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const { CodeDeliveryDetails } = await resendConfirmationCode( { region: getRegion(authConfig.userPoolId), @@ -44,6 +53,7 @@ export async function resendSignUpCode( Username: username, ClientMetadata: clientMetadata, ClientId: authConfig.userPoolClientId, + UserContextData, } ); const { DeliveryMedium, AttributeName, Destination } = { diff --git a/packages/auth/src/providers/cognito/apis/resetPassword.ts b/packages/auth/src/providers/cognito/apis/resetPassword.ts index 185d862e054..08543139d01 100644 --- a/packages/auth/src/providers/cognito/apis/resetPassword.ts +++ b/packages/auth/src/providers/cognito/apis/resetPassword.ts @@ -15,6 +15,7 @@ import { forgotPassword } from '../utils/clients/CognitoIdentityProvider'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; import { ForgotPasswordException } from '../../cognito/types/errors'; import { getAuthUserAgentValue } from '../../../utils'; +import { getUserContextData } from '../utils/userContextData'; /** * Resets a user's password. @@ -37,7 +38,15 @@ export async function resetPassword( ); const authConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(authConfig); + const { userPoolClientId, userPoolId } = authConfig; const clientMetadata = input.options?.serviceOptions?.clientMetadata; + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const res = await forgotPassword( { region: getRegion(authConfig.userPoolId), @@ -47,6 +56,7 @@ export async function resetPassword( Username: username, ClientMetadata: clientMetadata, ClientId: authConfig.userPoolClientId, + UserContextData, } ); const codeDeliveryDetails = res.CodeDeliveryDetails; diff --git a/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts b/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts index 86483aee49f..3ea7672f99b 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts @@ -73,6 +73,7 @@ export class TokenOrchestrator implements AuthTokenOrchestrator { } await this.waitForInflightOAuth(); tokens = await this.getTokenStore().loadTokens(); + const username = await this.getTokenStore().getLastAuthUser(); if (tokens === null) { return null; @@ -91,6 +92,7 @@ export class TokenOrchestrator implements AuthTokenOrchestrator { if (options?.forceRefresh || idTokenExpired || accessTokenExpired) { tokens = await this.refreshTokens({ tokens, + username, }); if (tokens === null) { @@ -106,13 +108,16 @@ export class TokenOrchestrator implements AuthTokenOrchestrator { private async refreshTokens({ tokens, + username, }: { tokens: CognitoAuthTokens; + username: string; }): Promise { try { const newTokens = await this.getTokenRefresher()({ tokens, authConfig: this.authConfig, + username, }); this.setTokens({ tokens: newTokens }); diff --git a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts index 7849b86a629..082799c9e22 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts @@ -209,7 +209,7 @@ export class DefaultTokenStore implements AuthTokenStore { return `${this.name}.${identifier}.LastAuthUser`; } - private async getLastAuthUser(): Promise { + async getLastAuthUser(): Promise { const lastAuthUser = (await this.getKeyValueStorage().getItem(this.getLastAuthUserKey())) ?? 'username'; diff --git a/packages/auth/src/providers/cognito/tokenProvider/types.ts b/packages/auth/src/providers/cognito/tokenProvider/types.ts index e88acd826dd..63d0d41df2e 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/types.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/types.ts @@ -11,9 +11,11 @@ import { export type TokenRefresher = ({ tokens, authConfig, + username, }: { tokens: CognitoAuthTokens; authConfig?: AuthConfig; + username: string; }) => Promise; export type AuthKeys = { @@ -32,6 +34,7 @@ export const AuthTokenStorageKeys = { }; export interface AuthTokenStore { + getLastAuthUser(): Promise; loadTokens(): Promise; storeTokens(tokens: CognitoAuthTokens): Promise; clearTokens(): Promise; diff --git a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts index 92d1b82e223..929f9e4a50a 100644 --- a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts +++ b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts @@ -11,13 +11,16 @@ import { initiateAuth } from '../utils/clients/CognitoIdentityProvider'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; import { assertAuthTokensWithRefreshToken } from '../utils/types'; import { AuthError } from '../../../errors/AuthError'; +import { getUserContextData } from './userContextData'; export const refreshAuthTokens: TokenRefresher = async ({ tokens, authConfig, + username, }: { tokens: CognitoAuthTokens; authConfig?: AuthConfig; + username: string; }): Promise => { assertTokenProviderConfig(authConfig?.Cognito); const region = getRegion(authConfig.Cognito.userPoolId); @@ -30,12 +33,20 @@ export const refreshAuthTokens: TokenRefresher = async ({ if (tokens.deviceMetadata?.deviceKey) { AuthParameters['DEVICE_KEY'] = tokens.deviceMetadata.deviceKey; } + + const UserContextData = getUserContextData({ + username, + userPoolId: authConfig.Cognito.userPoolId, + userPoolClientId: authConfig.Cognito.userPoolClientId, + }); + const { AuthenticationResult } = await initiateAuth( { region }, { ClientId: authConfig?.Cognito?.userPoolClientId, AuthFlow: 'REFRESH_TOKEN_AUTH', AuthParameters, + UserContextData, } ); diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 67f95c5e068..b5c6f11b4ab 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -59,6 +59,7 @@ import { getCurrentUser } from '../apis/getCurrentUser'; import { AuthTokenOrchestrator, DeviceMetadata } from '../tokenProvider/types'; import { assertDeviceMetadata } from './types'; import { getAuthUserAgentValue } from '../../../utils'; +import { getUserContextData } from './userContextData'; const USER_ATTRIBUTES = 'userAttributes.'; @@ -100,12 +101,19 @@ export async function handleCustomChallenge({ challengeResponses['DEVICE_KEY'] = deviceMetadata.deviceKey; } + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: RespondToAuthChallengeCommandInput = { ChallengeName: 'CUSTOM_CHALLENGE', ChallengeResponses: challengeResponses, Session: session, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; const response = await respondToAuthChallenge( @@ -185,12 +193,19 @@ export async function handleSelectMFATypeChallenge({ ANSWER: mapMfaType(challengeResponse), }; + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: RespondToAuthChallengeCommandInput = { ChallengeName: 'SELECT_MFA_TYPE', ChallengeResponses: challengeResponses, Session: session, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; return respondToAuthChallenge( @@ -214,12 +229,18 @@ export async function handleSMSMFAChallenge({ USERNAME: username, SMS_MFA_CODE: challengeResponse, }; + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); const jsonReq: RespondToAuthChallengeCommandInput = { ChallengeName: 'SMS_MFA', ChallengeResponses: challengeResponses, Session: session, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; return respondToAuthChallenge( @@ -242,12 +263,20 @@ export async function handleSoftwareTokenMFAChallenge({ USERNAME: username, SOFTWARE_TOKEN_MFA_CODE: challengeResponse, }; + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: RespondToAuthChallengeCommandInput = { ChallengeName: 'SOFTWARE_TOKEN_MFA', ChallengeResponses: challengeResponses, Session: session, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; return respondToAuthChallenge( { @@ -272,12 +301,19 @@ export async function handleCompleteNewPasswordChallenge({ USERNAME: username, }; + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: RespondToAuthChallengeCommandInput = { ChallengeName: 'NEW_PASSWORD_REQUIRED', ChallengeResponses: challengeResponses, ClientMetadata: clientMetadata, Session: session, ClientId: userPoolClientId, + UserContextData, }; return respondToAuthChallenge( @@ -306,11 +342,19 @@ export async function handleUserPasswordAuthFlow( if (deviceMetadata && deviceMetadata.deviceKey) { authParameters['DEVICE_KEY'] = deviceMetadata.deviceKey; } + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: InitiateAuthCommandInput = { AuthFlow: 'USER_PASSWORD_AUTH', AuthParameters: authParameters, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; const response = await initiateAuth( @@ -352,11 +396,19 @@ export async function handleUserSRPAuthFlow( if (deviceMetadata && deviceMetadata.deviceKey) { authParameters['DEVICE_KEY'] = deviceMetadata.deviceKey; } + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: InitiateAuthCommandInput = { AuthFlow: 'USER_SRP_AUTH', AuthParameters: authParameters, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; const resp = await initiateAuth( @@ -394,11 +446,19 @@ export async function handleCustomAuthFlowWithoutSRP( if (deviceMetadata && deviceMetadata.deviceKey) { authParameters['DEVICE_KEY'] = deviceMetadata.deviceKey; } + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: InitiateAuthCommandInput = { AuthFlow: 'CUSTOM_AUTH', AuthParameters: authParameters, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; const response = await initiateAuth( @@ -443,11 +503,18 @@ export async function handleCustomSRPAuthFlow( authParameters['DEVICE_KEY'] = deviceMetadata.deviceKey; } + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReq: InitiateAuthCommandInput = { AuthFlow: 'CUSTOM_AUTH', AuthParameters: authParameters, ClientMetadata: clientMetadata, ClientId: userPoolClientId, + UserContextData, }; const { ChallengeParameters: challengeParameters, Session: session } = @@ -551,12 +618,19 @@ async function handleDevicePasswordVerifier( DEVICE_KEY: deviceKey, } as { [key: string]: string }; + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { ChallengeName: 'DEVICE_PASSWORD_VERIFIER', ClientId: userPoolClientId, ChallengeResponses: challengeResponses, Session: session, ClientMetadata: clientMetadata, + UserContextData, }; return respondToAuthChallenge( @@ -611,12 +685,19 @@ export async function handlePasswordVerifierChallenge( challengeResponses['DEVICE_KEY'] = deviceMetadata.deviceKey; } + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { ChallengeName: 'PASSWORD_VERIFIER', ChallengeResponses: challengeResponses, ClientMetadata: clientMetadata, Session: session, ClientId: userPoolClientId, + UserContextData, }; const response = await respondToAuthChallenge( diff --git a/packages/auth/src/providers/cognito/utils/userContextData.native.ts b/packages/auth/src/providers/cognito/utils/userContextData.native.ts new file mode 100644 index 00000000000..ff5d7f226d5 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/userContextData.native.ts @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// TODO: add support after https://amazon-cognito-assets.us-east-1.amazoncognito.com/amazon-cognito-advanced-security-data.min.js can be imported + +export function getUserContextData({ + username, + userPoolId, + userPoolClientId, +}: { + username: string; + userPoolId: string; + userPoolClientId: string; +}) { + return undefined; +} diff --git a/packages/auth/src/providers/cognito/utils/userContextData.ts b/packages/auth/src/providers/cognito/utils/userContextData.ts new file mode 100644 index 00000000000..61c0dc61c2f --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/userContextData.ts @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export function getUserContextData({ + username, + userPoolId, + userPoolClientId, +}: { + username: string; + userPoolId: string; + userPoolClientId: string; +}) { + const amazonCognitoAdvancedSecurityData = (window as any) + .AmazonCognitoAdvancedSecurityData as any; + if (typeof amazonCognitoAdvancedSecurityData === 'undefined') { + return undefined; + } + + const advancedSecurityData = amazonCognitoAdvancedSecurityData.getData( + username, + userPoolId, + userPoolClientId + ); + + if (advancedSecurityData) { + const userContextData = { + EncodedData: advancedSecurityData, + }; + return userContextData; + } + + return {}; +} From 49b394dc3bdc1a3426738f6f331b1ebc65d7d3af Mon Sep 17 00:00:00 2001 From: israx <70438514+israx@users.noreply.github.com> Date: Wed, 11 Oct 2023 18:06:05 -0400 Subject: [PATCH 11/22] fix(auth): adds retry logic on `ResourceNotFoundException` (#12241) * chore: create retry method * chore: apply retry logic * add doc strings * chore: address feedback * fix unit test --- .../providers/cognito/signInWithSRP.test.ts | 71 +++++++++++- .../cognito/apis/signInWithCustomAuth.ts | 7 +- .../cognito/apis/signInWithCustomSRPAuth.ts | 8 +- .../providers/cognito/apis/signInWithSRP.ts | 8 +- .../cognito/apis/signInWithUserPassword.ts | 8 +- .../providers/cognito/utils/signInHelpers.ts | 101 +++++++++++++----- 6 files changed, 159 insertions(+), 44 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts index 314bcad956d..686d265dd56 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts @@ -11,6 +11,8 @@ import { CognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import { AuthError } from '../../../src'; +import { createKeysForAuthStorage } from '../../../src/providers/cognito/tokenProvider/TokenStore'; import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; const authConfig = { @@ -25,13 +27,37 @@ Amplify.configure({ Auth: authConfig, }); +const mockedDeviceMetadata = { + deviceKey: 'mockedKey', + deviceGrouKey: 'mockedKey', + randomPasswordKey: 'mockedKey', +}; + +const lastAuthUser = 'lastAuthUser'; +const authKeys = createKeysForAuthStorage( + 'CognitoIdentityServiceProvider', + `${authConfig.Cognito.userPoolClientId}.${lastAuthUser}` +); + +function setDeviceKeys() { + localStorage.setItem(authKeys.deviceKey, mockedDeviceMetadata.deviceKey); + localStorage.setItem( + authKeys.deviceGroupKey, + mockedDeviceMetadata.deviceGrouKey + ); + localStorage.setItem( + authKeys.randomPasswordKey, + mockedDeviceMetadata.randomPasswordKey + ); +} + describe('signIn API happy path cases', () => { let handleUserSRPAuthflowSpy; beforeEach(() => { handleUserSRPAuthflowSpy = jest .spyOn(initiateAuthHelpers, 'handleUserSRPAuthFlow') - .mockImplementationOnce( + .mockImplementation( async (): Promise => authAPITestParams.RespondToAuthChallengeCommandOutput ); @@ -41,6 +67,47 @@ describe('signIn API happy path cases', () => { handleUserSRPAuthflowSpy.mockClear(); }); + test('signIn should retry on ResourceNotFoundException and delete device keys', async () => { + setDeviceKeys(); + handleUserSRPAuthflowSpy = jest + .spyOn(initiateAuthHelpers, 'handleUserSRPAuthFlow') + .mockImplementation( + async (): Promise => { + const deviceKeys = await tokenOrchestrator.getDeviceMetadata( + lastAuthUser + ); + if (deviceKeys) { + throw new AuthError({ + name: 'ResourceNotFoundException', + message: 'Device does not exist.', + }); + } + + return { + ChallengeName: 'CUSTOM_CHALLENGE', + AuthenticationResult: undefined, + Session: 'aaabbbcccddd', + $metadata: {}, + }; + } + ); + + const result = await signIn({ + username: lastAuthUser, + password: 'XXXXXXXX', + }); + + expect(result).toEqual({ + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE', + additionalInfo: undefined, + }, + }); + expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(2); + expect(await tokenOrchestrator.getDeviceMetadata(lastAuthUser)).toBeNull(); + }); + test('signIn API invoked with authFlowType should return a SignInResult', async () => { const result = await signIn({ username: authAPITestParams.user1.username, @@ -96,7 +163,7 @@ describe('signIn API happy path cases', () => { describe('Cognito ASF', () => { let initiateAuthSpy; - afterAll(() => { + beforeAll(() => { jest.restoreAllMocks(); }); beforeEach(() => { diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts index 7c8a8744767..a0a048f4efa 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts @@ -9,6 +9,7 @@ import { getSignInResult, getSignInResultFromError, getNewDeviceMetatada, + retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { Amplify, Hub } from '@aws-amplify/core'; import { @@ -64,10 +65,10 @@ export async function signInWithCustomAuth( ChallengeParameters, AuthenticationResult, Session, - } = await handleCustomAuthFlowWithoutSRP( + } = await retryOnResourceNotFoundException( + handleCustomAuthFlowWithoutSRP, + [username, metadata, authConfig, tokenOrchestrator], username, - metadata, - authConfig, tokenOrchestrator ); diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts index e51a67418aa..4b842b710aa 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts @@ -14,6 +14,7 @@ import { getSignInResult, getSignInResultFromError, getNewDeviceMetatada, + retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { InitiateAuthException, @@ -68,11 +69,10 @@ export async function signInWithCustomSRPAuth( ChallengeParameters, AuthenticationResult, Session, - } = await handleCustomSRPAuthFlow( + } = await retryOnResourceNotFoundException( + handleCustomSRPAuthFlow, + [username, password, metadata, authConfig, tokenOrchestrator], username, - password, - metadata, - authConfig, tokenOrchestrator ); diff --git a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts index 6f8bb08f532..654a9e189c8 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts @@ -22,6 +22,7 @@ import { getSignInResult, getSignInResultFromError, handleUserSRPAuthFlow, + retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { SignInWithSRPInput, SignInWithSRPOutput } from '../types'; import { @@ -65,11 +66,10 @@ export async function signInWithSRP( ChallengeParameters, AuthenticationResult, Session, - } = await handleUserSRPAuthFlow( + } = await retryOnResourceNotFoundException( + handleUserSRPAuthFlow, + [username, password, clientMetaData, authConfig, tokenOrchestrator], username, - password, - clientMetaData, - authConfig, tokenOrchestrator ); diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts index 44b51c813d9..8df7d8ada64 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts @@ -13,6 +13,7 @@ import { getSignInResult, getSignInResultFromError, handleUserPasswordAuthFlow, + retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { Amplify, Hub } from '@aws-amplify/core'; import { @@ -64,11 +65,10 @@ export async function signInWithUserPassword( ChallengeParameters, AuthenticationResult, Session, - } = await handleUserPasswordAuthFlow( + } = await retryOnResourceNotFoundException( + handleUserPasswordAuthFlow, + [username, password, metadata, authConfig, tokenOrchestrator], username, - password, - metadata, - authConfig, tokenOrchestrator ); diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index b5c6f11b4ab..3aaea797a32 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -21,8 +21,6 @@ import { AuthAdditionalInfo, AuthSignInOutput, AuthDeliveryMedium, - AuthSignUpOutput, - AuthSignInInput, } from '../../../types'; import { AuthError } from '../../../errors/AuthError'; import { InitiateAuthException } from '../types/errors'; @@ -71,7 +69,6 @@ type HandleAuthChallengeRequest = { deviceName?: string; requiredAttributes?: AuthUserAttributes; config: CognitoUserPoolConfig; - tokenOrchestrator?: AuthTokenOrchestrator; }; type HandleDeviceSRPInput = { @@ -89,7 +86,9 @@ export async function handleCustomChallenge({ username, config, tokenOrchestrator, -}: HandleAuthChallengeRequest): Promise { +}: HandleAuthChallengeRequest & { + tokenOrchestrator: AuthTokenOrchestrator; +}): Promise { const { userPoolId, userPoolClientId } = config; const challengeResponses: Record = { USERNAME: username, @@ -124,7 +123,7 @@ export async function handleCustomChallenge({ jsonReq ); - if (response.ChallengeName === 'DEVICE_SRP_AUTH') + if (response.ChallengeName === 'DEVICE_SRP_AUTH') { return handleDeviceSRPAuth({ username, config, @@ -132,6 +131,8 @@ export async function handleCustomChallenge({ session: response.Session, tokenOrchestrator, }); + } + return response; } @@ -356,7 +357,7 @@ export async function handleUserPasswordAuthFlow( ClientId: userPoolClientId, UserContextData, }; - + // TODO: add the retry here const response = await initiateAuth( { region: getRegion(userPoolId), @@ -420,13 +421,18 @@ export async function handleUserSRPAuthFlow( ); const { ChallengeParameters: challengeParameters, Session: session } = resp; - return handlePasswordVerifierChallenge( - password, - challengeParameters as ChallengeParameters, - clientMetadata, - session, - authenticationHelper, - config, + return retryOnResourceNotFoundException( + handlePasswordVerifierChallenge, + [ + password, + challengeParameters as ChallengeParameters, + clientMetadata, + session, + authenticationHelper, + config, + tokenOrchestrator, + ], + username, tokenOrchestrator ); } @@ -526,13 +532,18 @@ export async function handleCustomSRPAuthFlow( jsonReq ); - return handlePasswordVerifierChallenge( - password, - challengeParameters as ChallengeParameters, - clientMetadata, - session, - authenticationHelper, - config, + return retryOnResourceNotFoundException( + handlePasswordVerifierChallenge, + [ + password, + challengeParameters as ChallengeParameters, + clientMetadata, + session, + authenticationHelper, + config, + tokenOrchestrator, + ], + username, tokenOrchestrator ); } @@ -921,14 +932,21 @@ export async function handleChallengeName( config, }); case 'CUSTOM_CHALLENGE': - return handleCustomChallenge({ - challengeResponse, - clientMetadata, - session, + return retryOnResourceNotFoundException( + handleCustomChallenge, + [ + { + challengeResponse, + clientMetadata, + session, + username, + config, + tokenOrchestrator, + }, + ], username, - config, - tokenOrchestrator, - }); + tokenOrchestrator + ); case 'SOFTWARE_TOKEN_MFA': return handleSoftwareTokenMFAChallenge({ challengeResponse, @@ -1053,3 +1071,32 @@ export async function getNewDeviceMetatada( return undefined; } } + +/** + * It will retry the function if the error is a `ResourceNotFoundException` and + * will clean the device keys stored in the storage mechanism. + * + */ +export async function retryOnResourceNotFoundException< + F extends (...args: any[]) => any +>( + func: F, + args: Parameters, + username: string, + tokenOrchestrator: AuthTokenOrchestrator +): Promise> { + try { + return await func(...args); + } catch (error) { + if ( + error instanceof AuthError && + error.name === 'ResourceNotFoundException' && + error.message.includes('Device does not exist.') + ) { + await tokenOrchestrator.clearDeviceMetadata(username); + + return await func(...args); + } + throw error; + } +} From 62beb836dc163d590b43b3319e255f8ceb0800ae Mon Sep 17 00:00:00 2001 From: ManojNB Date: Wed, 11 Oct 2023 16:23:58 -0700 Subject: [PATCH 12/22] feat(inapp): interaction events APIs (#12242) * feat: interaction events apis * chore: update doc strings --- .../aws-amplify/__tests__/exports.test.ts | 10 ++++ packages/notifications/__mocks__/data.ts | 10 +--- .../pinpoint/apis/dispatchEvent.test.ts | 8 +-- .../pinpoint/apis/interactionEvents.test.ts | 57 +++++++++++++++++++ .../pinpoint/apis/setConflictHandler.test.ts | 14 +---- .../src/common/eventListeners/index.ts | 8 ++- .../src/common/eventListeners/types.ts | 4 ++ packages/notifications/src/common/index.ts | 6 +- .../notifications/src/inAppMessaging/index.ts | 5 ++ .../src/inAppMessaging/providers/index.ts | 5 ++ .../providers/pinpoint/apis/dispatchEvent.ts | 9 +-- .../providers/pinpoint/apis/identifyUser.ts | 3 - .../providers/pinpoint/apis/index.ts | 5 ++ .../pinpoint/apis/notifyMessageInteraction.ts | 24 ++++++++ .../pinpoint/apis/onMessageActionTaken.ts | 25 ++++++++ .../pinpoint/apis/onMessageDismissed.ts | 25 ++++++++ .../pinpoint/apis/onMessageDisplayed.ts | 25 ++++++++ .../pinpoint/apis/onMessageReceived.ts | 25 ++++++++ .../pinpoint/apis/setConflictHandler.ts | 9 ++- .../providers/pinpoint/apis/syncMessages.ts | 7 +-- .../providers/pinpoint/index.ts | 5 ++ .../providers/pinpoint/types/index.ts | 1 + .../providers/pinpoint/types/inputs.ts | 36 +++++++++++- .../providers/pinpoint/types/outputs.ts | 24 ++++++++ .../providers/pinpoint/types/types.ts | 2 + .../providers/pinpoint/utils/helpers.ts | 15 +---- 26 files changed, 309 insertions(+), 58 deletions(-) create mode 100644 packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/types/outputs.ts diff --git a/packages/aws-amplify/__tests__/exports.test.ts b/packages/aws-amplify/__tests__/exports.test.ts index 2dad7377d59..fbf09baa9ab 100644 --- a/packages/aws-amplify/__tests__/exports.test.ts +++ b/packages/aws-amplify/__tests__/exports.test.ts @@ -122,6 +122,11 @@ describe('aws-amplify Exports', () => { "syncMessages", "dispatchEvent", "setConflictHandler", + "onMessageReceived", + "onMessageDisplayed", + "onMessageDismissed", + "onMessageActionTaken", + "notifyMessageInteraction", ] `); }); @@ -134,6 +139,11 @@ describe('aws-amplify Exports', () => { "syncMessages", "dispatchEvent", "setConflictHandler", + "onMessageReceived", + "onMessageDisplayed", + "onMessageDismissed", + "onMessageActionTaken", + "notifyMessageInteraction", ] `); }); diff --git a/packages/notifications/__mocks__/data.ts b/packages/notifications/__mocks__/data.ts index 51077ce0a53..a4afbef9c62 100644 --- a/packages/notifications/__mocks__/data.ts +++ b/packages/notifications/__mocks__/data.ts @@ -172,20 +172,16 @@ export const pinpointInAppMessage: PinpointInAppMessage = { }, Priority: 3, Schedule: { - EndDate: '2024-01-01T00:00:00Z', + EndDate: '2021-01-01T00:00:00Z', EventFilter: { FilterType: 'SYSTEM', Dimensions: { - Attributes: { - interests: { Values: ['test-interest'] }, - }, + Attributes: {}, EventType: { DimensionType: 'INCLUSIVE', Values: ['clicked', 'swiped'], }, - Metrics: { - clicks: { ComparisonOperator: 'EQUAL', Value: 5 }, - }, + Metrics: {}, }, }, QuietTime: { diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts index d7f7c3a5e4f..d23b28c7768 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts @@ -26,7 +26,7 @@ describe('dispatchEvent', () => { mockDefaultStorage.setItem.mockClear(); mockNotifyEventListeners.mockClear(); }); - test('gets in-app messages from store and notifies listeners', async () => { + it('gets in-app messages from store and notifies listeners', async () => { const [message] = inAppMessages; mockDefaultStorage.getItem.mockResolvedValueOnce( JSON.stringify(simpleInAppMessages) @@ -40,7 +40,7 @@ describe('dispatchEvent', () => { expect(mockNotifyEventListeners).toBeCalledWith('messageReceived', message); }); - test('handles conflicts through default conflict handler', async () => { + it('handles conflicts through default conflict handler', async () => { mockDefaultStorage.getItem.mockResolvedValueOnce( JSON.stringify(simpleInAppMessages) ); @@ -56,7 +56,7 @@ describe('dispatchEvent', () => { ); }); - test('does not notify listeners if no messages are returned', async () => { + it('does not notify listeners if no messages are returned', async () => { mockProcessInAppMessages.mockReturnValueOnce([]); mockDefaultStorage.getItem.mockResolvedValueOnce( JSON.stringify(simpleInAppMessages) @@ -67,7 +67,7 @@ describe('dispatchEvent', () => { expect(mockNotifyEventListeners).not.toBeCalled(); }); - test('logs error if storage retrieval fails', async () => { + it('logs error if storage retrieval fails', async () => { mockDefaultStorage.getItem.mockRejectedValueOnce(Error); await expect( dispatchEvent(simpleInAppMessagingEvent) diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts new file mode 100644 index 00000000000..c6b62f42cab --- /dev/null +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { inAppMessages } from '../../../../../__mocks__/data'; +import { + notifyEventListeners, + addEventListener, +} from '../../../../../src/common'; +import { + notifyMessageInteraction, + onMessageActionTaken, + onMessageDismissed, + onMessageDisplayed, + onMessageReceived, +} from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; + +jest.mock('../../../../../src/common/eventListeners'); + +const mockNotifyEventListeners = notifyEventListeners as jest.Mock; +const mockAddEventListener = addEventListener as jest.Mock; + +describe('Interaction events', () => { + const handler = jest.fn(); + it('can be listened to by onMessageReceived', () => { + onMessageReceived(handler); + + expect(mockAddEventListener).toBeCalledWith('messageReceived', handler); + }); + + it('can be listened to by onMessageDisplayed', () => { + onMessageDisplayed(handler); + + expect(mockAddEventListener).toBeCalledWith('messageDisplayed', handler); + }); + + it('can be listened to by onMessageDismissed', () => { + onMessageDismissed(handler); + + expect(mockAddEventListener).toBeCalledWith('messageDismissed', handler); + }); + + it('can be listened to by onMessageActionTaken', () => { + onMessageActionTaken(handler); + + expect(mockAddEventListener).toBeCalledWith('messageActionTaken', handler); + }); + it('can be notified by notifyMessageInteraction', () => { + const [message] = inAppMessages; + + notifyMessageInteraction({ + type: 'messageReceived', + message, + }); + + expect(mockNotifyEventListeners).toBeCalledWith('messageReceived', message); + }); +}); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts index b8bae764502..65fa9e1dafb 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts @@ -8,7 +8,6 @@ import { } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; import { processInAppMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; import { - closestExpiryMessage, customHandledMessage, inAppMessages, simpleInAppMessagingEvent, @@ -24,21 +23,12 @@ const mockDefaultStorage = defaultStorage as jest.Mocked; const mockNotifyEventListeners = notifyEventListeners as jest.Mock; const mockProcessInAppMessages = processInAppMessages as jest.Mock; -describe('Conflict handling', () => { +describe('setConflictHandler', () => { beforeEach(() => { mockDefaultStorage.setItem.mockClear(); mockNotifyEventListeners.mockClear(); }); - test('has a default implementation', async () => { - mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); - await dispatchEvent(simpleInAppMessagingEvent); - expect(mockNotifyEventListeners).toBeCalledWith( - 'messageReceived', - closestExpiryMessage - ); - }); - - test('can be customized through setConflictHandler', async () => { + it('can register a custom conflict handler', async () => { const customConflictHandler = messages => messages.find(message => message.id === 'custom-handled'); mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); diff --git a/packages/notifications/src/common/eventListeners/index.ts b/packages/notifications/src/common/eventListeners/index.ts index becdd789d0e..5d255f064c7 100644 --- a/packages/notifications/src/common/eventListeners/index.ts +++ b/packages/notifications/src/common/eventListeners/index.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { EventListener, EventType } from './types'; +import { EventListener, EventListenerRemover, EventType } from './types'; const eventListeners: Record>> = {}; @@ -28,7 +28,7 @@ export const notifyEventListenersAndAwaitHandlers = ( export const addEventListener = ( type: EventType, handler: EventHandler -): EventListener => { +): EventListenerRemover => { // If there is no listener set for the event type, just create it if (!eventListeners[type]) { eventListeners[type] = new Set>(); @@ -40,5 +40,7 @@ export const addEventListener = ( }, }; eventListeners[type].add(listener); - return listener; + return { + remove: () => listener.remove(), + }; }; diff --git a/packages/notifications/src/common/eventListeners/types.ts b/packages/notifications/src/common/eventListeners/types.ts index 426a7192507..f9140ef63b0 100644 --- a/packages/notifications/src/common/eventListeners/types.ts +++ b/packages/notifications/src/common/eventListeners/types.ts @@ -10,3 +10,7 @@ export interface EventListener { } export type EventType = InAppMessageInteractionEvent | PushNotificationEvent; + +export type EventListenerRemover = { + remove: () => void; +}; diff --git a/packages/notifications/src/common/index.ts b/packages/notifications/src/common/index.ts index 1134bc52e51..3b8c9e1efd3 100644 --- a/packages/notifications/src/common/index.ts +++ b/packages/notifications/src/common/index.ts @@ -7,4 +7,8 @@ export { notifyEventListeners, notifyEventListenersAndAwaitHandlers, } from './eventListeners'; -export { EventListener, EventType } from './eventListeners/types'; +export { + EventListener, + EventType, + EventListenerRemover, +} from './eventListeners/types'; diff --git a/packages/notifications/src/inAppMessaging/index.ts b/packages/notifications/src/inAppMessaging/index.ts index 5bfbae5da51..21082666350 100644 --- a/packages/notifications/src/inAppMessaging/index.ts +++ b/packages/notifications/src/inAppMessaging/index.ts @@ -6,4 +6,9 @@ export { syncMessages, dispatchEvent, setConflictHandler, + onMessageReceived, + onMessageDisplayed, + onMessageDismissed, + onMessageActionTaken, + notifyMessageInteraction, } from './providers/pinpoint'; diff --git a/packages/notifications/src/inAppMessaging/providers/index.ts b/packages/notifications/src/inAppMessaging/providers/index.ts index 51aec634a0c..ce0a80edbff 100644 --- a/packages/notifications/src/inAppMessaging/providers/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/index.ts @@ -6,4 +6,9 @@ export { syncMessages, dispatchEvent, setConflictHandler, + onMessageReceived, + onMessageDisplayed, + onMessageDismissed, + onMessageActionTaken, + notifyMessageInteraction, } from './pinpoint/apis'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts index 212e8548139..7373ca2c055 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts @@ -19,14 +19,15 @@ import { conflictHandler, setConflictHandler } from './setConflictHandler'; * Triggers an In-App message to be displayed. Use this after your campaigns have been synced to the device using * {@link syncMessages}. Based on the messages synced and the event passed to this API, it triggers the display * of the In-App message that meets the criteria. - * To change the conflict handler, use the {@link setConflictHandler} API. * - * @param DispatchEventInput The input object that holds the event to be dispatched. + * @remark + * If an event would trigger multiple messages, the message closest to expiry will be chosen by default. + * To change this behavior, you can use the {@link setConflictHandler} API to provide + * your own logic for resolving message conflicts. * + * @param DispatchEventInput The input object that holds the event to be dispatched. * @throws service exceptions - Thrown when the underlying Pinpoint service returns an error. - * * @returns A promise that will resolve when the operation is complete. - * * @example * ```ts * // Sync message before disptaching an event diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts index a7208ba43a4..168bcc4065c 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts @@ -20,13 +20,10 @@ import { IdentifyUserInput } from '../types'; * * @param {IdentifyUserParameters} params The input object used to construct requests sent to Pinpoint's UpdateEndpoint * API. - * * @throws service: {@link UpdateEndpointException} - Thrown when the underlying Pinpoint service returns an error. * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library * configuration is incorrect. - * * @returns A promise that will resolve when the operation is complete. - * * @example * ```ts * // Identify a user with Pinpoint diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts index d25f562d165..46811a85823 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts @@ -5,3 +5,8 @@ export { identifyUser } from './identifyUser'; export { syncMessages } from './syncMessages'; export { dispatchEvent } from './dispatchEvent'; export { setConflictHandler } from './setConflictHandler'; +export { onMessageReceived } from './onMessageReceived'; +export { onMessageDismissed } from './onMessageDismissed'; +export { onMessageDisplayed } from './onMessageDisplayed'; +export { onMessageActionTaken } from './onMessageActionTaken'; +export { notifyMessageInteraction } from './notifyMessageInteraction'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts new file mode 100644 index 00000000000..ca37112d186 --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { notifyEventListeners } from '../../../../common'; +import { NotifyMessageInteractionInput } from '../types/inputs'; + +/** + * Notifies the respective listener of the specified type with the message given. + * + * @param {NotifyMessageInteractionInput} input - The input object that holds the type and message. + * @example + * ```ts + * onMessageRecieved((message) => { + * // Show end users the In-App message and notify event listeners + * notifyMessageInteraction({ type: 'messageDisplayed', message }); + * }); + * ``` + */ +export function notifyMessageInteraction({ + type, + message, +}: NotifyMessageInteractionInput): void { + notifyEventListeners(type, message); +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts new file mode 100644 index 00000000000..59af1ab308e --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { addEventListener } from '../../../../common'; +import { OnMessageActionTakenInput } from '../types/inputs'; +import { OnMessageActionTakenOutput } from '../types/outputs'; + +/** + * Registers a callback that will be invoked on `messageActionTaken` events. + * + * @param {OnMessageActionTakenInput} input - The input object that holds the callback handler. + * @returns {OnMessageActionTakenOutput} - An object that holds a remove method to stop listening to events. + * @example + * ```ts + * onMessageActionTaken((message) => { + * // use the message + * console.log(message.id); + * }); + * ``` + */ +export function onMessageActionTaken( + input: OnMessageActionTakenInput +): OnMessageActionTakenOutput { + return addEventListener('messageActionTaken', input); +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts new file mode 100644 index 00000000000..486925b5514 --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { addEventListener } from '../../../../common'; +import { OnMessageDismissedOutput } from '../types/outputs'; +import { OnMessageDismissedInput } from '../types/inputs'; + +/** + * Registers a callback that will be invoked on `messageDismissed` events. + * + * @param {OnMessageDismissedInput} input - The input object that holds the callback handler. + * @returns {OnMessageDismissedOutput} - An object that holds a remove method to stop listening to events. + * @example + * ```ts + * onMessageDismissed((message) => { + * // use the message + * console.log(message.id); + * }); + * ``` + */ +export function onMessageDismissed( + input: OnMessageDismissedInput +): OnMessageDismissedOutput { + return addEventListener('messageDismissed', input); +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts new file mode 100644 index 00000000000..f269c4c968f --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { addEventListener } from '../../../../common'; +import { OnMessageDisplayedOutput } from '../types/outputs'; +import { OnMessageDisplayedInput } from '../types/inputs'; + +/** + * Registers a callback that will be invoked on `messageDisplayed` events. + * + * @param {OnMessageDisplayedInput} input - The input object that holds the callback handler. + * @returns {OnMessageDismissedOutput} - An object that holds a remove method to stop listening to events. + * @example + * ```ts + * onMessageDisplayed((message) => { + * // use the message + * console.log(message.id); + * }); + * ``` + */ +export function onMessageDisplayed( + input: OnMessageDisplayedInput +): OnMessageDisplayedOutput { + return addEventListener('messageDisplayed', input); +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts new file mode 100644 index 00000000000..a655cf6675d --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { addEventListener } from '../../../../common'; +import { OnMessageReceivedInput } from '../types/inputs'; +import { OnMessageReceivedOutput } from '../types/outputs'; + +/** + * Registers a callback that will be invoked on `messageReceived` events. + * + * @param {OnMessageReceivedInput} input - The input object that holds the callback handler. + * @returns {OnMessageReceivedOutput} - An object that holds a remove method to stop listening to events. + * @example + * ```ts + * onMessageReceived((message) => { + * // use the message + * console.log(message.id); + * }); + * ``` + */ +export function onMessageReceived( + input: OnMessageReceivedInput +): OnMessageReceivedOutput { + return addEventListener('messageReceived', input); +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts index 052450551ff..9aab53f6771 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts @@ -10,13 +10,12 @@ export let conflictHandler: InAppMessageConflictHandler = /** * Set a conflict handler that will be used to resolve conflicts that may emerge * when matching events with synced messages. + * * @remark - * The conflict handler is not persisted between sessions - * and needs to be called before dispatching an event to have any effect. + * The conflict handler is not persisted across app restarts and so must be set again before dispatching an event for + * any custom handling to take effect. * * @param SetConflictHandlerInput: The input object that holds the conflict handler to be used. - * - * * @example * ```ts * // Sync messages before dispatching an event @@ -33,7 +32,7 @@ export let conflictHandler: InAppMessageConflictHandler = * setConflictHandler(myConflictHandler); * * // Dispatch an event - * await dispatchEvent({ name: "test_event" }); + * await dispatchEvent({ name: 'test_event' }); * ``` */ export function setConflictHandler(input: SetConflictHandlerInput): void { diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts index 4b72868d97e..4747238f97d 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts @@ -34,9 +34,7 @@ import { * @throws service exceptions - Thrown when the underlying Pinpoint service returns an error. * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library * configuration is incorrect. - * * @returns A promise that will resolve when the operation is complete. - * * @example * ```ts * // Sync InApp messages with Pinpoint and device. @@ -46,7 +44,7 @@ import { */ export async function syncMessages(): Promise { const messages = await fetchInAppMessages(); - if (messages.length === 0) { + if (!messages || messages.length === 0) { return; } try { @@ -95,7 +93,8 @@ async function fetchInAppMessages() { { credentials, region }, input ); - const { InAppMessageCampaigns: messages } = response.InAppMessagesResponse; + const { InAppMessageCampaigns: messages } = + response.InAppMessagesResponse ?? {}; return messages; } catch (error) { assertServiceError(error); diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts index 970ae16eb7b..087413bfa27 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts @@ -6,4 +6,9 @@ export { syncMessages, dispatchEvent, setConflictHandler, + onMessageReceived, + onMessageDisplayed, + onMessageDismissed, + onMessageActionTaken, + notifyMessageInteraction, } from './apis'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts index 80d6de78c80..a0ddff96523 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/index.ts @@ -15,4 +15,5 @@ export { InAppMessageCountMap, DailyInAppMessageCounter, InAppMessageConflictHandler, + OnMessageInteractionEventHandler, } from './types'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts index f2edad9b6dc..d98ac12d22b 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/inputs.ts @@ -1,8 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { IdentifyUserOptions, InAppMessageConflictHandler } from '.'; import { + IdentifyUserOptions, + InAppMessageConflictHandler, + OnMessageInteractionEventHandler, +} from '.'; +import { + InAppMessage, + InAppMessageInteractionEvent, InAppMessagingEvent, InAppMessagingIdentifyUserInput, } from '../../../types'; @@ -22,3 +28,31 @@ export type DispatchEventInput = InAppMessagingEvent; * Input type for Pinpoint SetConflictHandler API. */ export type SetConflictHandlerInput = InAppMessageConflictHandler; + +/** + * Input type for OnMessageReceived API. + */ +export type OnMessageReceivedInput = OnMessageInteractionEventHandler; + +/** + * Input type for OnMessageDisplayed API. + */ +export type OnMessageDisplayedInput = OnMessageInteractionEventHandler; + +/** + * Input type for OnMessageDismissed API. + */ +export type OnMessageDismissedInput = OnMessageInteractionEventHandler; + +/** + * Input type for OnMessageActionTaken API. + */ +export type OnMessageActionTakenInput = OnMessageInteractionEventHandler; + +/** + * Input type for NotifyMessageInteraction API. + */ +export type NotifyMessageInteractionInput = { + message: InAppMessage; + type: InAppMessageInteractionEvent; +}; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/outputs.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/outputs.ts new file mode 100644 index 00000000000..e729989aafe --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/outputs.ts @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { EventListenerRemover } from '../../../../common'; + +/** + * Output type for OnMessageReceived API. + */ +export type OnMessageReceivedOutput = EventListenerRemover; + +/** + * Output type for OnMessageDisplayed API. + */ +export type OnMessageDisplayedOutput = EventListenerRemover; + +/** + * Output type for OnMessageDismissed API. + */ +export type OnMessageDismissedOutput = EventListenerRemover; + +/** + * Output type for OnMessageActionTaken API. + */ +export type OnMessageActionTakenOutput = EventListenerRemover; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts index 6c964507666..89185f8fb87 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/types/types.ts @@ -30,3 +30,5 @@ export enum PinpointMessageEvent { export type InAppMessageConflictHandler = ( messages: InAppMessage[] ) => InAppMessage; + +export type OnMessageInteractionEventHandler = (message: InAppMessage) => void; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts index 58a625d41bd..a32353ea53a 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts @@ -30,20 +30,7 @@ let eventNameMemo = {}; let eventAttributesMemo = {}; let eventMetricsMemo = {}; -export const logger = new ConsoleLogger('InAppMessaging.Pinpoint'); - -export const dispatchInAppMessagingEvent = ( - event: string, - data: any, - message?: string -) => { - Hub.dispatch( - 'inAppMessaging', - { event, data, message }, - 'InAppMessaging', - AMPLIFY_SYMBOL - ); -}; +export const logger = new ConsoleLogger('InAppMessaging.Pinpoint.Utils'); export const recordAnalyticsEvent = ( event: PinpointMessageEvent, From 2bf18e7ea9d40b5ff1b637908d0bef9ecff1b43b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 11 Oct 2023 17:31:59 -0700 Subject: [PATCH 13/22] fix: remove void SignOutOutput type (#12257) fix: remove void AuthSignOutOutput type Co-authored-by: Ashwin Kumar --- packages/auth/src/index.ts | 1 - packages/auth/src/providers/cognito/apis/signOut.ts | 9 ++++----- packages/auth/src/providers/cognito/index.ts | 1 - packages/auth/src/providers/cognito/types/index.ts | 1 - packages/auth/src/providers/cognito/types/outputs.ts | 6 ------ packages/auth/src/types/index.ts | 1 - packages/auth/src/types/outputs.ts | 2 -- 7 files changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index bf7bb2f2758..6dcb5d93e78 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -62,7 +62,6 @@ export { ResetPasswordOutput, SetUpTOTPOutput, SignInOutput, - SignOutOutput, SignUpOutput, UpdateUserAttributesOutput, SendUserAttributeVerificationCodeOutput, diff --git a/packages/auth/src/providers/cognito/apis/signOut.ts b/packages/auth/src/providers/cognito/apis/signOut.ts index ed0fa321e85..3e2dbb345a6 100644 --- a/packages/auth/src/providers/cognito/apis/signOut.ts +++ b/packages/auth/src/providers/cognito/apis/signOut.ts @@ -9,7 +9,7 @@ import { defaultStorage, } from '@aws-amplify/core'; import { getAuthUserAgentValue, openAuthSession } from '../../../utils'; -import { SignOutInput, SignOutOutput } from '../types'; +import { SignOutInput } from '../types'; import { DefaultOAuthStore } from '../utils/signInWithRedirectStore'; import { tokenOrchestrator } from '../tokenProvider'; import { @@ -33,10 +33,9 @@ import { * Signs a user out * * @param input - The SignOutInput object - * @returns SignOutOutput * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function signOut(input?: SignOutInput): Promise { +export async function signOut(input?: SignOutInput): Promise { const cognitoConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(cognitoConfig); @@ -57,7 +56,7 @@ async function clientSignOut(cognitoConfig: CognitoUserPoolConfig) { await revokeToken( { region: getRegion(cognitoConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.SignOut) + userAgentValue: getAuthUserAgentValue(AuthAction.SignOut), }, { ClientId: cognitoConfig.userPoolClientId, @@ -83,7 +82,7 @@ async function globalSignOut(cognitoConfig: CognitoUserPoolConfig) { await globalSignOutClient( { region: getRegion(cognitoConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.SignOut) + userAgentValue: getAuthUserAgentValue(AuthAction.SignOut), }, { AccessToken: tokens.accessToken.toString(), diff --git a/packages/auth/src/providers/cognito/index.ts b/packages/auth/src/providers/cognito/index.ts index 6879dcd5f11..b3f26730ef2 100644 --- a/packages/auth/src/providers/cognito/index.ts +++ b/packages/auth/src/providers/cognito/index.ts @@ -60,7 +60,6 @@ export { ResetPasswordOutput, SetUpTOTPOutput, SignInOutput, - SignOutOutput, SignUpOutput, UpdateUserAttributesOutput, UpdateUserAttributeOutput, diff --git a/packages/auth/src/providers/cognito/types/index.ts b/packages/auth/src/providers/cognito/types/index.ts index e7d03aa9c8f..eb6581028e8 100644 --- a/packages/auth/src/providers/cognito/types/index.ts +++ b/packages/auth/src/providers/cognito/types/index.ts @@ -64,7 +64,6 @@ export { SignInWithSRPOutput, SignInWithUserPasswordOutput, SignInWithCustomSRPAuthOutput, - SignOutOutput, SignUpOutput, UpdateUserAttributesOutput, UpdateUserAttributeOutput, diff --git a/packages/auth/src/providers/cognito/types/outputs.ts b/packages/auth/src/providers/cognito/types/outputs.ts index 27d4f38fda9..80590f4691e 100644 --- a/packages/auth/src/providers/cognito/types/outputs.ts +++ b/packages/auth/src/providers/cognito/types/outputs.ts @@ -11,7 +11,6 @@ import { AuthSignInOutput, AuthSignUpOutput, AuthResetPasswordOutput, - AuthSignOutOutput, AuthUpdateUserAttributesOutput, AuthUpdateUserAttributeOutput, } from '../../../types'; @@ -86,11 +85,6 @@ export type SignInWithUserPasswordOutput = AuthSignInOutput; */ export type SignInWithCustomSRPAuthOutput = AuthSignInOutput; -/** - * Output type for Cognito signOut API. - */ -export type SignOutOutput = AuthSignOutOutput; - /** * Output type for Cognito signUp API. */ diff --git a/packages/auth/src/types/index.ts b/packages/auth/src/types/index.ts index 8b89219859e..57514140eb6 100644 --- a/packages/auth/src/types/index.ts +++ b/packages/auth/src/types/index.ts @@ -47,7 +47,6 @@ export { export { AuthSignUpOutput, AuthSignInOutput, - AuthSignOutOutput, AuthResetPasswordOutput, AuthUpdateUserAttributeOutput, AuthUpdateUserAttributesOutput, diff --git a/packages/auth/src/types/outputs.ts b/packages/auth/src/types/outputs.ts index 100d422105b..aa6a49c7546 100644 --- a/packages/auth/src/types/outputs.ts +++ b/packages/auth/src/types/outputs.ts @@ -16,8 +16,6 @@ export type AuthSignInOutput< nextStep: AuthNextSignInStep; }; -export type AuthSignOutOutput = void; - export type AuthSignUpOutput< UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey > = { From f56c84eedc97cf8200a32869bf4332a90741148f Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Thu, 12 Oct 2023 10:20:20 -0500 Subject: [PATCH 14/22] chore: Exported `AWSCredentials` for internal use (#12270) --- .../analytics/src/providers/kinesis-firehose/types/buffer.ts | 4 ++-- packages/analytics/src/providers/kinesis/types/buffer.ts | 4 ++-- packages/analytics/src/providers/personalize/types/buffer.ts | 4 ++-- packages/core/src/libraryUtils.ts | 1 + packages/core/src/providers/pinpoint/apis/flushEvents.ts | 4 ++-- packages/core/src/singleton/Auth/types.ts | 2 +- packages/storage/__tests__/providers/s3/apis/copy.test.ts | 4 ++-- .../storage/__tests__/providers/s3/apis/downloadData.test.ts | 4 ++-- .../__tests__/providers/s3/apis/getProperties.test.ts | 4 ++-- packages/storage/__tests__/providers/s3/apis/getUrl.test.ts | 4 ++-- packages/storage/__tests__/providers/s3/apis/list.test.ts | 4 ++-- packages/storage/__tests__/providers/s3/apis/remove.test.ts | 4 ++-- .../providers/s3/apis/uploadData/multipartHandlers.test.ts | 4 ++-- .../providers/s3/apis/uploadData/putObjectJob.test.ts | 4 ++-- packages/storage/src/providers/s3/types/options.ts | 5 ++--- 15 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts b/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts index 405005a02b7..6ce03a616e1 100644 --- a/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts +++ b/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts @@ -3,7 +3,7 @@ import { EventBufferConfig } from '../../../utils'; import { KinesisStream } from '../../../types'; -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; export type KinesisFirehoseBufferEvent = KinesisStream & { event: Uint8Array; @@ -13,7 +13,7 @@ export type KinesisFirehoseBufferEvent = KinesisStream & { export type KinesisFirehoseEventBufferConfig = EventBufferConfig & { region: string; - credentials: Credentials; + credentials: AWSCredentials; identityId?: string; resendLimit?: number; userAgentValue?: string; diff --git a/packages/analytics/src/providers/kinesis/types/buffer.ts b/packages/analytics/src/providers/kinesis/types/buffer.ts index b41e00cd587..57ab85ecb34 100644 --- a/packages/analytics/src/providers/kinesis/types/buffer.ts +++ b/packages/analytics/src/providers/kinesis/types/buffer.ts @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { EventBufferConfig } from '../../../utils'; -import { Credentials } from '@aws-sdk/types'; import { KinesisShard } from '../../../types'; export type KinesisBufferEvent = KinesisShard & { @@ -13,7 +13,7 @@ export type KinesisBufferEvent = KinesisShard & { export type KinesisEventBufferConfig = EventBufferConfig & { region: string; - credentials: Credentials; + credentials: AWSCredentials; identityId?: string; resendLimit?: number; userAgentValue?: string; diff --git a/packages/analytics/src/providers/personalize/types/buffer.ts b/packages/analytics/src/providers/personalize/types/buffer.ts index 369035873a9..85881d6981c 100644 --- a/packages/analytics/src/providers/personalize/types/buffer.ts +++ b/packages/analytics/src/providers/personalize/types/buffer.ts @@ -3,7 +3,7 @@ import { PersonalizeEvent } from './'; import { EventBufferConfig } from '../../../utils'; -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; export type PersonalizeBufferEvent = { trackingId: string; @@ -15,7 +15,7 @@ export type PersonalizeBufferEvent = { export type PersonalizeBufferConfig = EventBufferConfig & { region: string; - credentials: Credentials; + credentials: AWSCredentials; identityId?: string; userAgentValue?: string; }; diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 944fdee0396..16cfcc23de2 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -38,6 +38,7 @@ export { JwtPayload, AuthStandardAttributeKey, AuthVerifiableAttributeKey, + AWSCredentials } from './singleton/Auth/types'; // Logging utilities diff --git a/packages/core/src/providers/pinpoint/apis/flushEvents.ts b/packages/core/src/providers/pinpoint/apis/flushEvents.ts index bbf8e46c9ac..8d07b0591e9 100644 --- a/packages/core/src/providers/pinpoint/apis/flushEvents.ts +++ b/packages/core/src/providers/pinpoint/apis/flushEvents.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '../../../libraryUtils'; import { getEventBuffer } from '../utils/getEventBuffer'; import { BUFFER_SIZE, @@ -13,7 +13,7 @@ import { export const flushEvents = ( appId: string, region: string, - credentials: Credentials, + credentials: AWSCredentials, identityId?: string, userAgentValue?: string ) => { diff --git a/packages/core/src/singleton/Auth/types.ts b/packages/core/src/singleton/Auth/types.ts index 6dba247e925..13dbf435cd0 100644 --- a/packages/core/src/singleton/Auth/types.ts +++ b/packages/core/src/singleton/Auth/types.ts @@ -217,7 +217,7 @@ export type AWSCredentialsAndIdentityId = { identityId?: string; }; -type AWSCredentials = { +export type AWSCredentials = { accessKeyId: string; secretAccessKey: string; sessionToken?: string; diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index 1db4b9e9391..7c5fef91d2d 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { copyObject } from '../../../../src/providers/s3/utils/client'; import { copy } from '../../../../src/providers/s3/apis'; @@ -30,7 +30,7 @@ const region = 'region'; const targetIdentityId = 'targetIdentityId'; const defaultIdentityId = 'defaultIdentityId'; const copyResult = { key: destinationKey }; -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 5dc89d72df1..ebd66964167 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { getObject } from '../../../../src/providers/s3/utils/client'; import { downloadData } from '../../../../src/providers/s3'; @@ -18,7 +18,7 @@ jest.mock('@aws-amplify/core', () => ({ }, }, })); -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts index 1fa04f63e8b..9b315c6d62f 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -3,7 +3,7 @@ import { headObject } from '../../../../src/providers/s3/utils/client'; import { getProperties } from '../../../../src/providers/s3'; -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { GetPropertiesOptions } from '../../../../src/providers/s3/types'; @@ -22,7 +22,7 @@ const mockGetConfig = Amplify.getConfig as jest.Mock; const bucket = 'bucket'; const region = 'region'; -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts index 2064cc11a6e..27cc59ad1f5 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { getUrl } from '../../../../src/providers/s3/apis'; -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { getPresignedGetObjectUrl, @@ -24,7 +24,7 @@ const bucket = 'bucket'; const region = 'region'; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index 461d8612e5f..b80f01d0744 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { listObjectsV2 } from '../../../../src/providers/s3/utils/client'; import { list } from '../../../../src/providers/s3'; @@ -32,7 +32,7 @@ const defaultIdentityId = 'defaultIdentityId'; const eTag = 'eTag'; const lastModified = 'lastModified'; const size = 'size'; -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/__tests__/providers/s3/apis/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/remove.test.ts index 8ac7629e803..11e90dc915e 100644 --- a/packages/storage/__tests__/providers/s3/apis/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/remove.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { deleteObject } from '../../../../src/providers/s3/utils/client'; import { remove } from '../../../../src/providers/s3/apis'; @@ -24,7 +24,7 @@ const bucket = 'bucket'; const region = 'region'; const defaultIdentityId = 'defaultIdentityId'; const removeResult = { key }; -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts index ac0808f60ee..35786782aef 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify, defaultStorage } from '@aws-amplify/core'; import { createMultipartUpload, @@ -24,7 +24,7 @@ import { StorageOptions } from '../../../../../src/types'; jest.mock('@aws-amplify/core'); jest.mock('../../../../../src/providers/s3/utils/client'); -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts index b2b9e0096a3..401f2ebe260 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { putObject } from '../../../../../src/providers/s3/utils/client'; import { calculateContentMd5 } from '../../../../../src/providers/s3/utils'; @@ -24,7 +24,7 @@ jest.mock('@aws-amplify/core', () => ({ }, }, })); -const credentials: Credentials = { +const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 5082cfd4542..23aa77b846e 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { StorageAccessLevel } from '@aws-amplify/core'; -// TODO(ashwinkumar6) this uses V5 Credentials, update to V6. -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { TransferProgressEvent } from '../../../types'; import { @@ -123,7 +122,7 @@ export type CopyDestinationOptions = WriteOptions & { */ export type ResolvedS3Config = { region: string; - credentials: Credentials; + credentials: AWSCredentials; customEndpoint?: string; forcePathStyle?: boolean; useAccelerateEndpoint?: boolean; From 261f2a548bcabce405f80ae4c1746e67eaf922aa Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 12 Oct 2023 08:50:38 -0700 Subject: [PATCH 15/22] fix: remove deviceName from device management APIs (#12258) Co-authored-by: Ashwin Kumar Co-authored-by: israx <70438514+israx@users.noreply.github.com> --- .../providers/cognito/fetchDevices.test.ts | 32 ++----------------- .../providers/cognito/apis/fetchDevices.ts | 12 +++---- .../providers/cognito/apis/forgetDevice.ts | 9 ++++-- packages/auth/src/types/models.ts | 1 - 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts b/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts index cbaf2cc8b2a..8c9e358b293 100644 --- a/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts +++ b/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts @@ -62,43 +62,17 @@ describe('fetchDevices API happy path cases', () => { fetchAuthSessionsSpy.mockClear(); }); - it('should fetch devices and parse client response correctly with and without device name', async () => { - const deviceName = { - Name: 'device_name', - Value: 'test-device-name', - }; - + it('should fetch devices and parse client response correctly', async () => { const fetchDevicesClientSpy = jest .spyOn(clients, 'listDevices') .mockImplementationOnce(async () => { return { - Devices: [ - { - ...clientResponseDevice, - DeviceKey: 'DeviceKey1', - DeviceAttributes: [ - ...clientResponseDevice.DeviceAttributes, - deviceName, - ], - }, - { ...clientResponseDevice, DeviceKey: 'DeviceKey2' }, - ], + Devices: [clientResponseDevice], $metadata: {}, }; }); - expect(await fetchDevices()).toEqual([ - { - ...apiOutputDevice, - id: 'DeviceKey1', - name: deviceName.Value, - attributes: { - ...apiOutputDevice.attributes, - [deviceName.Name]: deviceName.Value, - }, - }, - { ...apiOutputDevice, id: 'DeviceKey2' }, - ]); + expect(await fetchDevices()).toEqual([apiOutputDevice]); expect(fetchDevicesClientSpy).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), expect.objectContaining({ diff --git a/packages/auth/src/providers/cognito/apis/fetchDevices.ts b/packages/auth/src/providers/cognito/apis/fetchDevices.ts index d44f4b4fd25..dbe22f19840 100644 --- a/packages/auth/src/providers/cognito/apis/fetchDevices.ts +++ b/packages/auth/src/providers/cognito/apis/fetchDevices.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, +} from '@aws-amplify/core/internals/utils'; import { fetchAuthSession } from '../../../'; import { FetchDevicesOutput } from '../types'; import { listDevices } from '../utils/clients/CognitoIdentityProvider'; @@ -32,9 +35,9 @@ export async function fetchDevices(): Promise { assertAuthTokens(tokens); const response = await listDevices( - { + { region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.FetchDevices) + userAgentValue: getAuthUserAgentValue(AuthAction.FetchDevices), }, { AccessToken: tokens.accessToken.toString(), @@ -55,11 +58,9 @@ const parseDevicesResponse = async ( DeviceLastModifiedDate, DeviceLastAuthenticatedDate, }) => { - let name: string | undefined; const attributes = DeviceAttributes.reduce( (attrs: any, { Name, Value }) => { if (Name && Value) { - if (Name === 'device_name') name = Value; attrs[Name] = Value; } return attrs; @@ -68,7 +69,6 @@ const parseDevicesResponse = async ( ); return { id, - name, attributes, createDate: DeviceCreateDate ? new Date(DeviceCreateDate * 1000) diff --git a/packages/auth/src/providers/cognito/apis/forgetDevice.ts b/packages/auth/src/providers/cognito/apis/forgetDevice.ts index 4a050acff84..05b1060fb03 100644 --- a/packages/auth/src/providers/cognito/apis/forgetDevice.ts +++ b/packages/auth/src/providers/cognito/apis/forgetDevice.ts @@ -4,7 +4,10 @@ import { forgetDevice as serviceForgetDevice } from '../utils/clients/CognitoIdentityProvider'; import { Amplify } from '@aws-amplify/core'; import { assertAuthTokens, assertDeviceMetadata } from '../utils/types'; -import { assertTokenProviderConfig, AuthAction } from '@aws-amplify/core/internals/utils'; +import { + assertTokenProviderConfig, + AuthAction, +} from '@aws-amplify/core/internals/utils'; import { fetchAuthSession } from '../../../'; import { getRegion } from '../utils/clients/CognitoIdentityProvider/utils'; import { tokenOrchestrator } from '../tokenProvider'; @@ -33,9 +36,9 @@ export async function forgetDevice(input?: ForgetDeviceInput): Promise { if (!externalDeviceKey) assertDeviceMetadata(deviceMetadata); await serviceForgetDevice( - { + { region: getRegion(authConfig.userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ForgetDevice) + userAgentValue: getAuthUserAgentValue(AuthAction.ForgetDevice), }, { AccessToken: tokens.accessToken.toString(), diff --git a/packages/auth/src/types/models.ts b/packages/auth/src/types/models.ts index 4db9c4a85b4..80124dd1586 100644 --- a/packages/auth/src/types/models.ts +++ b/packages/auth/src/types/models.ts @@ -273,5 +273,4 @@ export type AuthUser = { */ export type AuthDevice = { id: string; - name?: string; }; From 178f709c4fa7f920a6c14da5d0a5173c87fb0247 Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Thu, 12 Oct 2023 11:37:24 -0500 Subject: [PATCH 16/22] chore: Optimize getConfig (#12272) --- .../__tests__/singleton/Singleton.test.ts | 67 +++++++++++-------- packages/core/src/singleton/Amplify.ts | 18 ++--- packages/core/src/utils/deepFreeze.ts | 16 +++++ packages/core/src/utils/index.ts | 1 + 4 files changed, 66 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/utils/deepFreeze.ts diff --git a/packages/core/__tests__/singleton/Singleton.test.ts b/packages/core/__tests__/singleton/Singleton.test.ts index cf65ba0a156..2915b795f90 100644 --- a/packages/core/__tests__/singleton/Singleton.test.ts +++ b/packages/core/__tests__/singleton/Singleton.test.ts @@ -9,6 +9,14 @@ type ArgumentTypes = F extends (...args: infer A) => any ? A : never; +const MOCK_AUTH_CONFIG = { + Auth: { + Cognito: { + identityPoolId: 'us-east-1:bbbbb', + }, + }, +}; + describe('Amplify.configure() and Amplify.getConfig()', () => { it('should take the legacy CLI shaped config object for configuring and return it from getConfig()', () => { const mockLegacyConfig = { @@ -70,20 +78,10 @@ describe('Amplify.configure() and Amplify.getConfig()', () => { }); it('should take the v6 shaped config object for configuring and return it from getConfig()', () => { - const config: ArgumentTypes[0] = { - Auth: { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - identityPoolId: 'us-east-1:bbbbb', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - }; - - Amplify.configure(config); + Amplify.configure(MOCK_AUTH_CONFIG); const result = Amplify.getConfig(); - expect(result).toEqual(config); + expect(result).toEqual(MOCK_AUTH_CONFIG); }); it('should replace Cognito configuration set and get config', () => { @@ -97,26 +95,39 @@ describe('Amplify.configure() and Amplify.getConfig()', () => { }; Amplify.configure(config1); - - const config2: ArgumentTypes[0] = { - Auth: { - Cognito: { - identityPoolId: 'us-east-1:bbbbb', - }, - }, - }; - Amplify.configure(config2); + Amplify.configure(MOCK_AUTH_CONFIG); const result = Amplify.getConfig(); - expect(result).toEqual({ - Auth: { - Cognito: { - identityPoolId: 'us-east-1:bbbbb', - }, - }, - }); + expect(result).toEqual(MOCK_AUTH_CONFIG); }); + + it('should return memoized, immutable resource configuration objects', () => { + Amplify.configure(MOCK_AUTH_CONFIG); + + const config = Amplify.getConfig(); + const config2 = Amplify.getConfig(); + + const mutateConfig = () => { + config.Auth = MOCK_AUTH_CONFIG.Auth; + } + + // Config should be cached + expect(config).toEqual(MOCK_AUTH_CONFIG); + expect(config2).toBe(config); + + // Config should be immutable + expect(mutateConfig).toThrow(TypeError); + + // Config should be re-generated if it changes + Amplify.configure(MOCK_AUTH_CONFIG); + + const config3 = Amplify.getConfig(); + + expect(config3).toEqual(MOCK_AUTH_CONFIG); + expect(config3).not.toBe(config); + expect(config3).not.toBe(config2); + }) }); describe('Session tests', () => { diff --git a/packages/core/src/singleton/Amplify.ts b/packages/core/src/singleton/Amplify.ts index 8724ee48f5b..2e905bf330d 100644 --- a/packages/core/src/singleton/Amplify.ts +++ b/packages/core/src/singleton/Amplify.ts @@ -4,24 +4,23 @@ import { AuthClass } from './Auth'; import { Hub, AMPLIFY_SYMBOL } from '../Hub'; import { LegacyConfig, LibraryOptions, ResourcesConfig } from './types'; import { parseAWSExports } from '../parseAWSExports'; - -// TODO(v6): add default AuthTokenStore for each platform +import { deepFreeze } from '../utils'; export class AmplifyClass { resourcesConfig: ResourcesConfig; libraryOptions: LibraryOptions; + /** * Cross-category Auth utilities. * * @internal */ public readonly Auth: AuthClass; + constructor() { this.resourcesConfig = {}; - this.Auth = new AuthClass(); - - // TODO(v6): add default providers for getting started this.libraryOptions = {}; + this.Auth = new AuthClass(); } /** @@ -55,6 +54,9 @@ export class AmplifyClass { libraryOptions ); + // Make resource config immutable + this.resourcesConfig = deepFreeze(this.resourcesConfig); + this.Auth.configure(this.resourcesConfig.Auth!, this.libraryOptions.Auth); Hub.dispatch( @@ -71,10 +73,10 @@ export class AmplifyClass { /** * Provides access to the current back-end resource configuration for the Library. * - * @returns Returns the current back-end resource configuration. + * @returns Returns the immutable back-end resource configuration. */ - getConfig(): ResourcesConfig { - return JSON.parse(JSON.stringify(this.resourcesConfig)); + getConfig(): Readonly { + return this.resourcesConfig; } } diff --git a/packages/core/src/utils/deepFreeze.ts b/packages/core/src/utils/deepFreeze.ts new file mode 100644 index 00000000000..5298794dd54 --- /dev/null +++ b/packages/core/src/utils/deepFreeze.ts @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const deepFreeze = (object: any) => { + const propNames = Reflect.ownKeys(object); + + for (const name of propNames) { + const value = object[name]; + + if ((value && typeof value === "object") || typeof value === "function") { + deepFreeze(value); + } + } + + return Object.freeze(object); +}; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index dc5975c73ca..c49c1d2e0af 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -14,3 +14,4 @@ export { } from './retry'; export { urlSafeDecode } from './urlSafeDecode'; export { urlSafeEncode } from './urlSafeEncode'; +export { deepFreeze } from './deepFreeze'; From a4ad9e09d6098a53f4530d9fa9112c81b409a605 Mon Sep 17 00:00:00 2001 From: Hui Zhao Date: Thu, 12 Oct 2023 08:01:31 -0700 Subject: [PATCH 17/22] feat(core): add amplifyUuid and amplifyUrl with builtin polyfill loader for RN --- .../providers/personalize/utils/cachedSession.test.ts | 8 ++++++-- packages/analytics/package.json | 2 -- .../src/providers/personalize/utils/cachedSession.ts | 7 +++---- packages/api-graphql/package.json | 1 - .../src/Providers/AWSAppSyncRealTimeProvider/index.ts | 7 ++++--- .../api-graphql/src/internals/InternalGraphQLAPI.ts | 3 ++- packages/api-rest/src/utils/index.ts | 2 -- packages/api-rest/src/utils/resolveApiUrl.ts | 10 +++++++--- .../src/providers/cognito/apis/signInWithRedirect.ts | 7 ++++--- packages/auth/src/providers/cognito/index.ts | 2 -- .../utils/clients/CognitoIdentityProvider/base.ts | 9 ++++++--- .../auth/src/providers/cognito/utils/clients/base.ts | 9 +++++++-- .../auth/src/providers/cognito/utils/signInHelpers.ts | 3 ++- packages/core/src/Signer/Signer.ts | 5 +++-- packages/core/src/awsClients/cognitoIdentity/base.ts | 5 ++++- packages/core/src/awsClients/pinpoint/base.ts | 3 ++- .../core/src/awsClients/pinpoint/getInAppMessages.ts | 3 ++- packages/core/src/awsClients/pinpoint/putEvents.ts | 3 ++- .../core/src/awsClients/pinpoint/updateEndpoint.ts | 3 ++- .../middleware/signing/signer/signatureV4/index.ts | 2 -- .../signer/signatureV4/polyfills/index.native.ts | 5 ----- .../signing/signer/signatureV4/polyfills/index.ts | 4 ---- .../signing/signer/signatureV4/presignUrl.ts | 3 ++- packages/core/src/libraryUtils.ts | 2 ++ packages/core/src/providers/pinpoint/apis/record.ts | 6 +++--- .../src/providers/pinpoint/apis/updateEndpoint.ts | 6 +++--- packages/core/src/utils/amplifyUrl/index.ts | 9 +++++++++ .../src/utils/amplifyUrl/polyfill.native.ts} | 1 + .../src/utils/amplifyUrl/polyfill.ts} | 0 .../polyfills => core/src/utils/amplifyUuid}/index.ts | 7 ++++++- .../src/utils/amplifyUuid/polyfill.native.ts} | 5 +++-- .../src/utils/amplifyUuid/polyfill.ts} | 0 packages/datastore/package.json | 2 -- packages/datastore/src/datastore/datastore.ts | 7 ++++--- packages/datastore/src/util.ts | 8 ++++---- packages/notifications/package.json | 3 +-- .../src/common/AWSPinpointProviderCommon/index.ts | 8 ++++---- packages/pubsub/package.json | 1 - packages/pubsub/src/Providers/MqttOverWSProvider.ts | 4 ++-- .../react-native/src/moduleLoaders/loadUrlPolyfill.ts | 2 +- .../providers/s3/utils/client/abortMultipartUpload.ts | 8 ++++++-- .../storage/src/providers/s3/utils/client/base.ts | 11 +++++++---- .../s3/utils/client/completeMultipartUpload.ts | 10 ++++++++-- .../src/providers/s3/utils/client/copyObject.ts | 3 ++- .../s3/utils/client/createMultipartUpload.ts | 3 ++- .../src/providers/s3/utils/client/deleteObject.ts | 3 ++- .../src/providers/s3/utils/client/getObject.ts | 3 ++- .../src/providers/s3/utils/client/headObject.ts | 3 ++- .../storage/src/providers/s3/utils/client/index.ts | 2 -- .../src/providers/s3/utils/client/listObjectsV2.ts | 8 ++++++-- .../src/providers/s3/utils/client/listParts.ts | 8 ++++++-- .../s3/utils/client/polyfills/index.native.ts | 5 ----- .../src/providers/s3/utils/client/putObject.ts | 3 ++- .../src/providers/s3/utils/client/uploadPart.ts | 8 ++++++-- 54 files changed, 152 insertions(+), 103 deletions(-) delete mode 100644 packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.native.ts delete mode 100644 packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.ts create mode 100644 packages/core/src/utils/amplifyUrl/index.ts rename packages/{api-rest/src/utils/polyfills/index.native.ts => core/src/utils/amplifyUrl/polyfill.native.ts} (99%) rename packages/{api-rest/src/utils/polyfills/index.ts => core/src/utils/amplifyUrl/polyfill.ts} (100%) rename packages/{storage/src/providers/s3/utils/client/polyfills => core/src/utils/amplifyUuid}/index.ts (52%) rename packages/{auth/src/providers/cognito/polyfills/index.native.ts => core/src/utils/amplifyUuid/polyfill.native.ts} (55%) rename packages/{auth/src/providers/cognito/polyfills/index.ts => core/src/utils/amplifyUuid/polyfill.ts} (100%) delete mode 100644 packages/storage/src/providers/s3/utils/client/polyfills/index.native.ts diff --git a/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts b/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts index fbc7516fa18..b72e641f2e9 100644 --- a/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts +++ b/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Cache, BrowserStorageCache } from '@aws-amplify/core'; -import { isBrowser } from '@aws-amplify/core/internals/utils'; +import { isBrowser, amplifyUuid } from '@aws-amplify/core/internals/utils'; import { resolveCachedSession, updateCachedSession, @@ -11,11 +11,14 @@ import { jest.mock('@aws-amplify/core'); jest.mock('@aws-amplify/core/internals/utils'); +const mockAmplifyUuid = amplifyUuid as jest.Mock; + describe('Analytics service provider Personalize utils: cachedSession', () => { const sessionIdCacheKey = '_awsct_sid.personalize'; const userIdCacheKey = '_awsct_uid.personalize'; const mockCache = Cache as jest.Mocked; const mockIsBrowser = isBrowser as jest.Mock; + const mockUuid = 'b2bd676e-bc6b-40f4-bd86-1e31a07f7d10'; const mockSession = { sessionId: 'sessionId0', @@ -30,6 +33,7 @@ describe('Analytics service provider Personalize utils: cachedSession', () => { beforeEach(() => { mockCache.getItem.mockImplementation(key => mockCachedStorage[key]); mockIsBrowser.mockReturnValue(false); + mockAmplifyUuid.mockReturnValue(mockUuid); }); afterEach(() => { @@ -47,7 +51,7 @@ describe('Analytics service provider Personalize utils: cachedSession', () => { mockCache.getItem.mockImplementation(() => undefined); const result = resolveCachedSession('trackingId0'); expect(result.sessionId).not.toBe(mockSession.sessionId); - expect(result.sessionId.length).toBeGreaterThan(0); + expect(result.sessionId).toEqual(mockUuid); expect(result.userId).toBe(undefined); }); diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 6d6c0e72a47..35f5f23ea0a 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -93,7 +93,6 @@ ], "dependencies": { "tslib": "^2.5.0", - "uuid": "^9.0.0", "@aws-sdk/client-kinesis": "3.398.0", "@aws-sdk/client-firehose": "3.398.0", "@aws-sdk/client-personalize-events": "3.398.0", @@ -105,7 +104,6 @@ "devDependencies": { "@aws-amplify/core": "6.0.0", "@aws-sdk/types": "3.398.0", - "@types/uuid": "^9.0.0", "typescript": "5.0.2" }, "jest": { diff --git a/packages/analytics/src/providers/personalize/utils/cachedSession.ts b/packages/analytics/src/providers/personalize/utils/cachedSession.ts index 8b844f92a3d..c92550b2939 100644 --- a/packages/analytics/src/providers/personalize/utils/cachedSession.ts +++ b/packages/analytics/src/providers/personalize/utils/cachedSession.ts @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Cache } from '@aws-amplify/core'; -import { isBrowser } from '@aws-amplify/core/internals/utils'; -import { v4 as uuid } from 'uuid'; +import { isBrowser, amplifyUuid } from '@aws-amplify/core/internals/utils'; const PERSONALIZE_CACHE_USERID = '_awsct_uid'; const PERSONALIZE_CACHE_SESSIONID = '_awsct_sid'; @@ -30,7 +29,7 @@ const setCache = (key: string, value: unknown) => { export const resolveCachedSession = (trackingId: string) => { let sessionId: string | undefined = getCache(PERSONALIZE_CACHE_SESSIONID); if (!sessionId) { - sessionId = uuid(); + sessionId = amplifyUuid(); setCache(PERSONALIZE_CACHE_SESSIONID, sessionId); } @@ -58,7 +57,7 @@ export const updateCachedSession = ( !!currentSessionId && !currentUserId && !!newUserId; if (isRequireNewSession) { - const newSessionId = uuid(); + const newSessionId = amplifyUuid(); setCache(PERSONALIZE_CACHE_SESSIONID, newSessionId); setCache(PERSONALIZE_CACHE_USERID, newUserId); } else if (isRequireUpdateSession) { diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index b63e9b30fdb..1a512f6174e 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -56,7 +56,6 @@ "graphql": "15.8.0", "tslib": "^1.8.0", "url": "0.11.0", - "uuid": "^3.2.1", "rxjs": "^7.8.1" }, "size-limit": [ diff --git a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts index 542c84b5e66..fd43de88b9b 100644 --- a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts @@ -3,7 +3,6 @@ import { Observable, SubscriptionLike } from 'rxjs'; import { GraphQLError } from 'graphql'; import * as url from 'url'; -import { v4 as uuid } from 'uuid'; import { Buffer } from 'buffer'; import { Hub, fetchAuthSession } from '@aws-amplify/core'; import { signRequest } from '@aws-amplify/core/internals/aws-client-utils'; @@ -17,6 +16,8 @@ import { isNonRetryableError, jitteredExponentialRetry, DocumentType, + amplifyUuid, + AmplifyUrl, } from '@aws-amplify/core/internals/utils'; import { @@ -210,7 +211,7 @@ export class AWSAppSyncRealTimeProvider { observer.complete(); } else { let subscriptionStartActive = false; - const subscriptionId = uuid(); + const subscriptionId = amplifyUuid(); const startSubscription = () => { if (!subscriptionStartActive) { subscriptionStartActive = true; @@ -968,7 +969,7 @@ export class AWSAppSyncRealTimeProvider { { headers: request.headers, method: request.method, - url: new URL(request.url), + url: new AmplifyUrl(request.url), body: request.data, }, { diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index ec60f965fbb..eb1bd781c55 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -15,6 +15,7 @@ import { CustomUserAgentDetails, ConsoleLogger as Logger, getAmplifyUserAgent, + AmplifyUrl, } from '@aws-amplify/core/internals/utils'; import { GraphQLAuthError, @@ -304,7 +305,7 @@ export class InternalGraphQLAPIClass { let response; try { const { body: responseBody } = await this._api.post({ - url: new URL(endpoint), + url: new AmplifyUrl(endpoint), options: { headers, body, diff --git a/packages/api-rest/src/utils/index.ts b/packages/api-rest/src/utils/index.ts index 2fc96b25063..2e72b5bf24b 100644 --- a/packages/api-rest/src/utils/index.ts +++ b/packages/api-rest/src/utils/index.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import './polyfills'; - export { createCancellableOperation } from './createCancellableOperation'; export { resolveCredentials } from './resolveCredentials'; export { parseSigningInfo } from './parseSigningInfo'; diff --git a/packages/api-rest/src/utils/resolveApiUrl.ts b/packages/api-rest/src/utils/resolveApiUrl.ts index 48eda61e024..f0cce97062a 100644 --- a/packages/api-rest/src/utils/resolveApiUrl.ts +++ b/packages/api-rest/src/utils/resolveApiUrl.ts @@ -2,6 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { AmplifyClassV6 } from '@aws-amplify/core'; +import { + AmplifyUrl, + AmplifyUrlSearchParams, +} from '@aws-amplify/core/internals/utils'; import { RestApiError, RestApiValidationErrorCode, @@ -28,13 +32,13 @@ export const resolveApiUrl = ( const urlStr = amplify.getConfig()?.API?.REST?.[apiName]?.endpoint; assertValidationError(!!urlStr, RestApiValidationErrorCode.InvalidApiName); try { - const url = new URL(urlStr + path); + const url = new AmplifyUrl(urlStr + path); if (queryParams) { - const mergedQueryParams = new URLSearchParams(url.searchParams); + const mergedQueryParams = new AmplifyUrlSearchParams(url.searchParams); Object.entries(queryParams).forEach(([key, value]) => { mergedQueryParams.set(key, value); }); - url.search = new URLSearchParams(mergedQueryParams).toString(); + url.search = new AmplifyUrlSearchParams(mergedQueryParams).toString(); } return url; } catch (error) { diff --git a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts index 8ba9d466f90..9d7568ef910 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts @@ -12,6 +12,7 @@ import { USER_AGENT_HEADER, urlSafeDecode, decodeJWT, + AmplifyUrl, } from '@aws-amplify/core/internals/utils'; import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { CognitoUserPoolsTokenProvider } from '../tokenProvider'; @@ -148,7 +149,7 @@ async function handleCodeFlow({ }) { /* Convert URL into an object with parameters as keys { redirect_uri: 'http://localhost:3000/', response_type: 'code', ...} */ - const url = new URL(currentUrl); + const url = new AmplifyUrl(currentUrl); let validatedState: string; try { validatedState = await validateStateFromURL(url); @@ -242,7 +243,7 @@ async function handleImplicitFlow({ }) { // hash is `null` if `#` doesn't exist on URL - const url = new URL(currentUrl); + const url = new AmplifyUrl(currentUrl); const { idToken, accessToken, state, tokenType, expiresIn } = ( url.hash ?? '#' @@ -330,7 +331,7 @@ async function handleAuthResponse({ preferPrivateSession?: boolean; }) { try { - const urlParams = new URL(currentUrl); + const urlParams = new AmplifyUrl(currentUrl); const error = urlParams.searchParams.get('error'); const errorMessage = urlParams.searchParams.get('error_description'); diff --git a/packages/auth/src/providers/cognito/index.ts b/packages/auth/src/providers/cognito/index.ts index b3f26730ef2..223c73b9a83 100644 --- a/packages/auth/src/providers/cognito/index.ts +++ b/packages/auth/src/providers/cognito/index.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import './polyfills'; - export { signUp } from './apis/signUp'; export { resetPassword } from './apis/resetPassword'; export { confirmResetPassword } from './apis/confirmResetPassword'; diff --git a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts index d053d052b6c..1a06002762a 100644 --- a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts +++ b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/base.ts @@ -15,7 +15,10 @@ import { getRetryDecider, jitteredBackoff, } from '@aws-amplify/core/internals/aws-client-utils'; -import { getAmplifyUserAgent } from '@aws-amplify/core/internals/utils'; +import { + getAmplifyUserAgent, + AmplifyUrl, +} from '@aws-amplify/core/internals/utils'; import { composeTransferHandler } from '@aws-amplify/core/internals/aws-client-utils/composers'; /** @@ -29,12 +32,12 @@ const SERVICE_NAME = 'cognito-idp'; const endpointResolver = ({ region }: EndpointResolverOptions) => { const authConfig = Amplify.getConfig().Auth?.Cognito; const customURL = authConfig?.endpoint; - const defaultURL = new URL( + const defaultURL = new AmplifyUrl( `https://${SERVICE_NAME}.${region}.${getDnsSuffix(region)}` ); return { - url: customURL ? new URL(customURL) : defaultURL, + url: customURL ? new AmplifyUrl(customURL) : defaultURL, }; }; diff --git a/packages/auth/src/providers/cognito/utils/clients/base.ts b/packages/auth/src/providers/cognito/utils/clients/base.ts index ea8e4170f39..0b71aa38c21 100644 --- a/packages/auth/src/providers/cognito/utils/clients/base.ts +++ b/packages/auth/src/providers/cognito/utils/clients/base.ts @@ -14,7 +14,10 @@ import { getRetryDecider, jitteredBackoff, } from '@aws-amplify/core/internals/aws-client-utils'; -import { getAmplifyUserAgent } from '@aws-amplify/core/internals/utils'; +import { + getAmplifyUserAgent, + AmplifyUrl, +} from '@aws-amplify/core/internals/utils'; import { composeTransferHandler } from '@aws-amplify/core/internals/aws-client-utils/composers'; /** @@ -26,7 +29,9 @@ const SERVICE_NAME = 'cognito-idp'; * The endpoint resolver function that returns the endpoint URL for a given region. */ const endpointResolver = ({ region }: EndpointResolverOptions) => ({ - url: new URL(`https://${SERVICE_NAME}.${region}.${getDnsSuffix(region)}`), + url: new AmplifyUrl( + `https://${SERVICE_NAME}.${region}.${getDnsSuffix(region)}` + ), }); /** diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 3aaea797a32..d7ab64336ef 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -6,6 +6,7 @@ import { AuthAction, assertTokenProviderConfig, base64Encoder, + AmplifyUrl, } from '@aws-amplify/core/internals/utils'; import { AuthenticationHelper } from './srp/AuthenticationHelper'; import { BigInteger } from './srp/BigInteger'; @@ -840,7 +841,7 @@ export function getTOTPSetupDetails( accountName ?? username }?secret=${secretCode}&issuer=${appName}`; - return new URL(totpUri); + return new AmplifyUrl(totpUri); }, }; } diff --git a/packages/core/src/Signer/Signer.ts b/packages/core/src/Signer/Signer.ts index 4fce58b72f9..4a8ede41d9e 100644 --- a/packages/core/src/Signer/Signer.ts +++ b/packages/core/src/Signer/Signer.ts @@ -7,6 +7,7 @@ import { signRequest, TOKEN_QUERY_PARAM, } from '../clients/middleware/signing/signer/signatureV4'; +import { AmplifyUrl } from '../utils/amplifyUrl'; const IOT_SERVICE_NAME = 'iotdevicegateway'; // Best practice regex to parse the service and region from an AWS endpoint @@ -75,7 +76,7 @@ export class Signer { const requestToSign = { ...request, body: request.data, - url: new URL(request.url), + url: new AmplifyUrl(request.url), }; const options = getOptions(requestToSign, accessInfo, serviceInfo); @@ -121,7 +122,7 @@ export class Signer { const presignable = { body, method, - url: new URL(urlToSign), + url: new AmplifyUrl(urlToSign), }; const options = getOptions( diff --git a/packages/core/src/awsClients/cognitoIdentity/base.ts b/packages/core/src/awsClients/cognitoIdentity/base.ts index 20a1a876c70..41584c36960 100644 --- a/packages/core/src/awsClients/cognitoIdentity/base.ts +++ b/packages/core/src/awsClients/cognitoIdentity/base.ts @@ -19,6 +19,7 @@ import { } from '../../clients/middleware/retry'; import { getAmplifyUserAgent } from '../../Platform'; import { observeFrameworkChanges } from '../../Platform/detectFramework'; +import { AmplifyUrl } from '../../utils/amplifyUrl'; /** * The service name used to sign requests if the API requires authentication. @@ -29,7 +30,9 @@ const SERVICE_NAME = 'cognito-identity'; * The endpoint resolver function that returns the endpoint URL for a given region. */ const endpointResolver = ({ region }: EndpointResolverOptions) => ({ - url: new URL(`https://cognito-identity.${region}.${getDnsSuffix(region)}`), + url: new AmplifyUrl( + `https://cognito-identity.${region}.${getDnsSuffix(region)}` + ), }); /** diff --git a/packages/core/src/awsClients/pinpoint/base.ts b/packages/core/src/awsClients/pinpoint/base.ts index 2c6030d0463..ffca0f847d6 100644 --- a/packages/core/src/awsClients/pinpoint/base.ts +++ b/packages/core/src/awsClients/pinpoint/base.ts @@ -9,6 +9,7 @@ import { import { parseJsonError } from '../../clients/serde/json'; import type { EndpointResolverOptions, Headers } from '../../clients/types'; import { getAmplifyUserAgent } from '../../Platform'; +import { AmplifyUrl } from '../../utils/amplifyUrl'; /** * The service name used to sign requests if the API requires authentication. @@ -19,7 +20,7 @@ const SERVICE_NAME = 'mobiletargeting'; * The endpoint resolver function that returns the endpoint URL for a given region. */ const endpointResolver = ({ region }: EndpointResolverOptions) => ({ - url: new URL(`https://pinpoint.${region}.${getDnsSuffix(region)}`), + url: new AmplifyUrl(`https://pinpoint.${region}.${getDnsSuffix(region)}`), }); /** diff --git a/packages/core/src/awsClients/pinpoint/getInAppMessages.ts b/packages/core/src/awsClients/pinpoint/getInAppMessages.ts index a2f0bfb6194..83969ad0f44 100644 --- a/packages/core/src/awsClients/pinpoint/getInAppMessages.ts +++ b/packages/core/src/awsClients/pinpoint/getInAppMessages.ts @@ -15,6 +15,7 @@ import type { GetInAppMessagesCommandInput as GetInAppMessagesInput, GetInAppMessagesCommandOutput as GetInAppMessagesOutput, } from './types'; +import { AmplifyUrl } from '../../utils/amplifyUrl'; export type { GetInAppMessagesInput, GetInAppMessagesOutput }; @@ -23,7 +24,7 @@ const getInAppMessagesSerializer = ( endpoint: Endpoint ): HttpRequest => { const headers = getSharedHeaders(); - const url = new URL(endpoint.url); + const url = new AmplifyUrl(endpoint.url); url.pathname = `v1/apps/${extendedEncodeURIComponent( ApplicationId )}/endpoints/${extendedEncodeURIComponent(EndpointId)}/inappmessages`; diff --git a/packages/core/src/awsClients/pinpoint/putEvents.ts b/packages/core/src/awsClients/pinpoint/putEvents.ts index ac73dac7ff4..5a2be6bfcc8 100644 --- a/packages/core/src/awsClients/pinpoint/putEvents.ts +++ b/packages/core/src/awsClients/pinpoint/putEvents.ts @@ -16,6 +16,7 @@ import type { PutEventsCommandInput as PutEventsInput, PutEventsCommandOutput as PutEventsOutput, } from './types'; +import { AmplifyUrl } from '../../utils/amplifyUrl'; export type { PutEventsInput, PutEventsOutput }; @@ -25,7 +26,7 @@ const putEventsSerializer = ( ): HttpRequest => { assert(!!ApplicationId, PinpointValidationErrorCode.NoAppId); const headers = getSharedHeaders(); - const url = new URL(endpoint.url); + const url = new AmplifyUrl(endpoint.url); url.pathname = `v1/apps/${extendedEncodeURIComponent(ApplicationId)}/events`; const body = JSON.stringify(EventsRequest ?? {}); return { method: 'POST', headers, url, body }; diff --git a/packages/core/src/awsClients/pinpoint/updateEndpoint.ts b/packages/core/src/awsClients/pinpoint/updateEndpoint.ts index 10ce67224c2..0676abeaa04 100644 --- a/packages/core/src/awsClients/pinpoint/updateEndpoint.ts +++ b/packages/core/src/awsClients/pinpoint/updateEndpoint.ts @@ -15,6 +15,7 @@ import type { UpdateEndpointCommandInput as UpdateEndpointInput, UpdateEndpointCommandOutput as UpdateEndpointOutput, } from './types'; +import { AmplifyUrl } from '../../utils/amplifyUrl'; export type { UpdateEndpointInput, UpdateEndpointOutput }; @@ -23,7 +24,7 @@ const updateEndpointSerializer = ( endpoint: Endpoint ): HttpRequest => { const headers = getSharedHeaders(); - const url = new URL(endpoint.url); + const url = new AmplifyUrl(endpoint.url); url.pathname = `v1/apps/${extendedEncodeURIComponent( ApplicationId )}/endpoints/${extendedEncodeURIComponent(EndpointId)}`; diff --git a/packages/core/src/clients/middleware/signing/signer/signatureV4/index.ts b/packages/core/src/clients/middleware/signing/signer/signatureV4/index.ts index 2365096e44a..e8ef35a57eb 100644 --- a/packages/core/src/clients/middleware/signing/signer/signatureV4/index.ts +++ b/packages/core/src/clients/middleware/signing/signer/signatureV4/index.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import './polyfills'; - // TODO: V6 replace Signer export { signRequest } from './signRequest'; export { presignUrl } from './presignUrl'; diff --git a/packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.native.ts b/packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.native.ts deleted file mode 100644 index 065c0fecb15..00000000000 --- a/packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.native.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { loadUrlPolyfill } from '@aws-amplify/react-native'; - -loadUrlPolyfill(); diff --git a/packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.ts b/packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.ts deleted file mode 100644 index f0daa8d350d..00000000000 --- a/packages/core/src/clients/middleware/signing/signer/signatureV4/polyfills/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// noop - polyfills not required on platform diff --git a/packages/core/src/clients/middleware/signing/signer/signatureV4/presignUrl.ts b/packages/core/src/clients/middleware/signing/signer/signatureV4/presignUrl.ts index 1009b14d2dd..0b459e24ebe 100644 --- a/packages/core/src/clients/middleware/signing/signer/signatureV4/presignUrl.ts +++ b/packages/core/src/clients/middleware/signing/signer/signatureV4/presignUrl.ts @@ -15,6 +15,7 @@ import { } from './constants'; import { getSigningValues } from './utils/getSigningValues'; import { getSignature } from './utils/getSignature'; +import { AmplifyUrl } from '../../../../../utils/amplifyUrl'; /** * Given a `Presignable` object, returns a Signature Version 4 presigned `URL` object. @@ -33,7 +34,7 @@ export const presignUrl = ( // create the request to sign // @ts-ignore URL constructor accepts a URL object - const presignedUrl = new URL(url); + const presignedUrl = new AmplifyUrl(url); Object.entries({ [ALGORITHM_QUERY_PARAM]: SHA256_ALGORITHM_IDENTIFIER, [CREDENTIAL_QUERY_PARAM]: `${accessKeyId}/${credentialScope}`, diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 16cfcc23de2..0be251bbb5d 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -20,6 +20,8 @@ export { } from './utils'; export { parseAWSExports } from './parseAWSExports'; export { LegacyConfig } from './singleton/types'; +export { amplifyUuid } from './utils/amplifyUuid'; +export { AmplifyUrl, AmplifyUrlSearchParams } from './utils/amplifyUrl'; // Auth utilities export { diff --git a/packages/core/src/providers/pinpoint/apis/record.ts b/packages/core/src/providers/pinpoint/apis/record.ts index cd1c33cedc9..57f7d919a71 100644 --- a/packages/core/src/providers/pinpoint/apis/record.ts +++ b/packages/core/src/providers/pinpoint/apis/record.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { v4 as uuid } from 'uuid'; +import { amplifyUuid } from '../../../utils/amplifyUuid'; import { PinpointRecordInput, PinpointSession } from '../types'; import { getEndpointId } from '../utils'; import { @@ -30,7 +30,7 @@ export const record = async ({ userAgentValue, }: PinpointRecordInput): Promise => { const timestampISOString = new Date().toISOString(); - const eventId = uuid(); + const eventId = amplifyUuid(); let endpointId = await getEndpointId(appId, category); // Prepare event buffer if required @@ -70,7 +70,7 @@ export const record = async ({ // Generate session if required if (!session) { - const sessionId = uuid(); + const sessionId = amplifyUuid(); session = { Id: sessionId, diff --git a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts index 63c38785b4a..01f1e21dea4 100644 --- a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts +++ b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { v4 as uuidv4 } from 'uuid'; +import { amplifyUuid } from '../../../utils/amplifyUuid'; import { getClientInfo } from '../../../utils/getClientInfo'; import { updateEndpoint as clientUpdateEndpoint, @@ -29,7 +29,7 @@ export const updateEndpoint = async ({ }: PinpointUpdateEndpointInput): Promise => { const endpointId = await getEndpointId(appId, category); // only generate a new endpoint id if one was not found in cache - const createdEndpointId = !endpointId ? uuidv4() : undefined; + const createdEndpointId = !endpointId ? amplifyUuid() : undefined; const { customProperties, demographic, @@ -59,7 +59,7 @@ export const updateEndpoint = async ({ ApplicationId: appId, EndpointId: endpointId ?? createdEndpointId, EndpointRequest: { - RequestId: uuidv4(), + RequestId: amplifyUuid(), EffectiveDate: new Date().toISOString(), ChannelType: channelType, Address: address, diff --git a/packages/core/src/utils/amplifyUrl/index.ts b/packages/core/src/utils/amplifyUrl/index.ts new file mode 100644 index 00000000000..3de929beee5 --- /dev/null +++ b/packages/core/src/utils/amplifyUrl/index.ts @@ -0,0 +1,9 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import './polyfill'; + +const AmplifyUrl = URL; +const AmplifyUrlSearchParams = URLSearchParams; + +export { AmplifyUrl, AmplifyUrlSearchParams }; diff --git a/packages/api-rest/src/utils/polyfills/index.native.ts b/packages/core/src/utils/amplifyUrl/polyfill.native.ts similarity index 99% rename from packages/api-rest/src/utils/polyfills/index.native.ts rename to packages/core/src/utils/amplifyUrl/polyfill.native.ts index 065c0fecb15..4e06280116a 100644 --- a/packages/api-rest/src/utils/polyfills/index.native.ts +++ b/packages/core/src/utils/amplifyUrl/polyfill.native.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import { loadUrlPolyfill } from '@aws-amplify/react-native'; loadUrlPolyfill(); diff --git a/packages/api-rest/src/utils/polyfills/index.ts b/packages/core/src/utils/amplifyUrl/polyfill.ts similarity index 100% rename from packages/api-rest/src/utils/polyfills/index.ts rename to packages/core/src/utils/amplifyUrl/polyfill.ts diff --git a/packages/storage/src/providers/s3/utils/client/polyfills/index.ts b/packages/core/src/utils/amplifyUuid/index.ts similarity index 52% rename from packages/storage/src/providers/s3/utils/client/polyfills/index.ts rename to packages/core/src/utils/amplifyUuid/index.ts index f0daa8d350d..3a492a5fbec 100644 --- a/packages/storage/src/providers/s3/utils/client/polyfills/index.ts +++ b/packages/core/src/utils/amplifyUuid/index.ts @@ -1,4 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// noop - polyfills not required on platform +import './polyfill'; +import { v4 } from 'uuid'; + +const amplifyUuid = v4; + +export { amplifyUuid }; diff --git a/packages/auth/src/providers/cognito/polyfills/index.native.ts b/packages/core/src/utils/amplifyUuid/polyfill.native.ts similarity index 55% rename from packages/auth/src/providers/cognito/polyfills/index.native.ts rename to packages/core/src/utils/amplifyUuid/polyfill.native.ts index 065c0fecb15..82f9ccf7dc3 100644 --- a/packages/auth/src/providers/cognito/polyfills/index.native.ts +++ b/packages/core/src/utils/amplifyUuid/polyfill.native.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { loadUrlPolyfill } from '@aws-amplify/react-native'; -loadUrlPolyfill(); +import { loadGetRandomValues } from '@aws-amplify/react-native'; + +loadGetRandomValues(); diff --git a/packages/auth/src/providers/cognito/polyfills/index.ts b/packages/core/src/utils/amplifyUuid/polyfill.ts similarity index 100% rename from packages/auth/src/providers/cognito/polyfills/index.ts rename to packages/core/src/utils/amplifyUuid/polyfill.ts diff --git a/packages/datastore/package.json b/packages/datastore/package.json index 0a22b56292f..b49771d1cac 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -53,7 +53,6 @@ "idb": "5.0.6", "immer": "9.0.6", "ulid": "^2.3.0", - "uuid": "^9.0.0", "rxjs": "^7.8.1" }, "peerDependencies": { @@ -62,7 +61,6 @@ "devDependencies": { "@aws-amplify/core": "6.0.0", "@aws-amplify/react-native": "^1.0.0", - "@types/uuid": "^9.0.0", "@types/uuid-validate": "^0.0.1", "dexie": "3.2.2", "dexie-export-import": "1.0.3", diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index f182a7ac4ab..3b5c6105b0a 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -11,7 +11,7 @@ import { enablePatches, Patch, } from 'immer'; -import { v4 as uuid4 } from 'uuid'; +import { amplifyUuid } from '@aws-amplify/core/internals/utils'; import { Observable, SubscriptionLike, filter } from 'rxjs'; import { defaultAuthStrategy, multiAuthStrategy } from '../authModeStrategies'; import { @@ -838,13 +838,14 @@ const createModelClass = ( const id = isInternalModel ? _id : modelDefinition.syncable - ? uuid4() + ? amplifyUuid() : ulid(); ((draft)).id = id; } else if (isIdOptionallyManaged(modelDefinition)) { // only auto-populate if the id was not provided - ((draft)).id = draft.id || uuid4(); + ((draft)).id = + draft.id || amplifyUuid(); } if (!isInternallyInitialized) { diff --git a/packages/datastore/src/util.ts b/packages/datastore/src/util.ts index fe04984a62e..1b8c549fa73 100644 --- a/packages/datastore/src/util.ts +++ b/packages/datastore/src/util.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { monotonicFactory, ULID } from 'ulid'; -import { v4 as uuid } from 'uuid'; +import { amplifyUuid, AmplifyUrl } from '@aws-amplify/core/internals/utils'; import { produce, applyPatches, Patch } from 'immer'; import { ModelInstanceCreator } from './datastore/datastore'; import { @@ -246,7 +246,7 @@ let privateModeCheckResult; export const isPrivateMode = () => { return new Promise(resolve => { - const dbname = uuid(); + const dbname = amplifyUuid(); let db; const isPrivate = () => { @@ -297,7 +297,7 @@ let safariCompatabilityModeResult; */ export const isSafariCompatabilityMode: () => Promise = async () => { try { - const dbName = uuid(); + const dbName = amplifyUuid(); const storeName = 'indexedDBFeatureProbeStore'; const indexName = 'idx'; @@ -642,7 +642,7 @@ export const isAWSJSON = (val: string): boolean => { export const isAWSURL = (val: string): boolean => { try { - return !!new URL(val); + return !!new AmplifyUrl(val); } catch { return false; } diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 01fda45dc36..c80c4c0d306 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -92,8 +92,7 @@ "src" ], "dependencies": { - "lodash": "^4.17.21", - "uuid": "^9.0.0" + "lodash": "^4.17.21" }, "peerDependencies": { "@aws-amplify/core": "^6.0.0" diff --git a/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts b/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts index 52189c78569..5ee8b556d13 100644 --- a/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts +++ b/packages/notifications/src/common/AWSPinpointProviderCommon/index.ts @@ -8,6 +8,7 @@ import { getAmplifyUserAgent, InAppMessagingAction, PushNotificationAction, + amplifyUuid, } from '@aws-amplify/core/internals/utils'; import { Cache, fetchAuthSession } from '@aws-amplify/core'; @@ -18,7 +19,6 @@ import { updateEndpoint, UpdateEndpointInput, } from '@aws-amplify/core/internals/aws-clients/pinpoint'; -import { v4 as uuid } from 'uuid'; import { NotificationsCategory, @@ -136,7 +136,7 @@ export default abstract class AWSPinpointProviderCommon [endpointId]: { Endpoint: {}, Events: { - [uuid()]: event, + [amplifyUuid()]: event, }, }, }, @@ -187,7 +187,7 @@ export default abstract class AWSPinpointProviderCommon ApplicationId: appId, EndpointId: endpointId, EndpointRequest: { - RequestId: uuid(), + RequestId: amplifyUuid(), EffectiveDate: new Date().toISOString(), ChannelType: endpointInfo.channelType, Address: address ?? endpointInfo.address, @@ -247,7 +247,7 @@ export default abstract class AWSPinpointProviderCommon return cachedEndpointId; } // Otherwise, generate a new ID and store it in long-lived cache before returning it - const endpointId = uuid(); + const endpointId = amplifyUuid(); // Set a longer TTL to avoid endpoint id being deleted after the default TTL (3 days) // Also set its priority to the highest to reduce its chance of being deleted when cache is full const ttl = 1000 * 60 * 60 * 24 * 365 * 100; // 100 years diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 0f84bbb2b03..4cedc55500c 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -53,7 +53,6 @@ "graphql": "15.8.0", "tslib": "^2.5.0", "url": "0.11.0", - "uuid": "^9.0.0", "zen-observable-ts": "0.8.19" }, "peerDependencies": { diff --git a/packages/pubsub/src/Providers/MqttOverWSProvider.ts b/packages/pubsub/src/Providers/MqttOverWSProvider.ts index 2564df94d05..9c4e8221ac5 100644 --- a/packages/pubsub/src/Providers/MqttOverWSProvider.ts +++ b/packages/pubsub/src/Providers/MqttOverWSProvider.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import * as Paho from '../vendor/paho-mqtt'; -import { v4 as uuid } from 'uuid'; +import { amplifyUuid } from '@aws-amplify/core/internals/utils'; import Observable, { ZenObservable } from 'zen-observable-ts'; import { AbstractPubSubProvider } from './PubSubProvider'; @@ -102,7 +102,7 @@ export class MqttOverWSProvider extends AbstractPubSubProvider { // another module and that causes error const message = (e as Error).message.replace( /undefined/g, - '@react-native-community/netinfo' + 'react-native-url-polyfill' ); throw new Error(message); } diff --git a/packages/storage/src/providers/s3/utils/client/abortMultipartUpload.ts b/packages/storage/src/providers/s3/utils/client/abortMultipartUpload.ts index db826aaa461..301092613ac 100644 --- a/packages/storage/src/providers/s3/utils/client/abortMultipartUpload.ts +++ b/packages/storage/src/providers/s3/utils/client/abortMultipartUpload.ts @@ -8,6 +8,10 @@ import { parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; +import { + AmplifyUrl, + AmplifyUrlSearchParams, +} from '@aws-amplify/core/internals/utils'; import { MetadataBearer } from '@aws-sdk/types'; import type { AbortMultipartUploadCommandInput } from './types'; @@ -31,11 +35,11 @@ const abortMultipartUploadSerializer = ( input: AbortMultipartUploadInput, endpoint: Endpoint ): HttpRequest => { - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); validateS3RequiredParameter(!!input.UploadId, 'UploadId'); - url.search = new URLSearchParams({ + url.search = new AmplifyUrlSearchParams({ uploadId: input.UploadId, }).toString(); return { diff --git a/packages/storage/src/providers/s3/utils/client/base.ts b/packages/storage/src/providers/s3/utils/client/base.ts index 509b634f0cf..5638402d351 100644 --- a/packages/storage/src/providers/s3/utils/client/base.ts +++ b/packages/storage/src/providers/s3/utils/client/base.ts @@ -1,7 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getAmplifyUserAgent } from '@aws-amplify/core/internals/utils'; +import { + getAmplifyUserAgent, + AmplifyUrl, +} from '@aws-amplify/core/internals/utils'; import { getDnsSuffix, jitteredBackoff, @@ -56,16 +59,16 @@ const endpointResolver = ( let endpoint: URL; // 1. get base endpoint if (customEndpoint) { - endpoint = new URL(customEndpoint); + endpoint = new AmplifyUrl(customEndpoint); } else if (useAccelerateEndpoint) { if (forcePathStyle) { throw new Error( 'Path style URLs are not supported with S3 Transfer Acceleration.' ); } - endpoint = new URL(`https://s3-accelerate.${getDnsSuffix(region)}`); + endpoint = new AmplifyUrl(`https://s3-accelerate.${getDnsSuffix(region)}`); } else { - endpoint = new URL(`https://s3.${region}.${getDnsSuffix(region)}`); + endpoint = new AmplifyUrl(`https://s3.${region}.${getDnsSuffix(region)}`); } // 2. inject bucket name if (apiInput?.Bucket) { diff --git a/packages/storage/src/providers/s3/utils/client/completeMultipartUpload.ts b/packages/storage/src/providers/s3/utils/client/completeMultipartUpload.ts index 12859d12993..55e52fbcb7d 100644 --- a/packages/storage/src/providers/s3/utils/client/completeMultipartUpload.ts +++ b/packages/storage/src/providers/s3/utils/client/completeMultipartUpload.ts @@ -7,6 +7,10 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { + AmplifyUrl, + AmplifyUrlSearchParams, +} from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import type { CompleteMultipartUploadCommandInput, @@ -45,11 +49,13 @@ const completeMultipartUploadSerializer = async ( const headers = { 'content-type': 'application/xml', }; - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); validateS3RequiredParameter(!!input.UploadId, 'UploadId'); - url.search = new URLSearchParams({ uploadId: input.UploadId }).toString(); + url.search = new AmplifyUrlSearchParams({ + uploadId: input.UploadId, + }).toString(); validateS3RequiredParameter(!!input.MultipartUpload, 'MultipartUpload'); return { method: 'POST', diff --git a/packages/storage/src/providers/s3/utils/client/copyObject.ts b/packages/storage/src/providers/s3/utils/client/copyObject.ts index 1730e564406..6a79bfb6d9f 100644 --- a/packages/storage/src/providers/s3/utils/client/copyObject.ts +++ b/packages/storage/src/providers/s3/utils/client/copyObject.ts @@ -7,6 +7,7 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import type { CopyObjectCommandInput, CopyObjectCommandOutput } from './types'; import { defaultConfig } from './base'; @@ -50,7 +51,7 @@ const copyObjectSerializer = async ( 'x-amz-metadata-directive': input.MetadataDirective, }), }; - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); return { diff --git a/packages/storage/src/providers/s3/utils/client/createMultipartUpload.ts b/packages/storage/src/providers/s3/utils/client/createMultipartUpload.ts index 9818f0a9374..62c72d6e94c 100644 --- a/packages/storage/src/providers/s3/utils/client/createMultipartUpload.ts +++ b/packages/storage/src/providers/s3/utils/client/createMultipartUpload.ts @@ -7,6 +7,7 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import type { CreateMultipartUploadCommandInput, @@ -41,7 +42,7 @@ const createMultipartUploadSerializer = async ( endpoint: Endpoint ): Promise => { const headers = await serializeObjectConfigsToHeaders(input); - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); url.search = 'uploads'; diff --git a/packages/storage/src/providers/s3/utils/client/deleteObject.ts b/packages/storage/src/providers/s3/utils/client/deleteObject.ts index e4e0fc11d33..8ba2ea864d7 100644 --- a/packages/storage/src/providers/s3/utils/client/deleteObject.ts +++ b/packages/storage/src/providers/s3/utils/client/deleteObject.ts @@ -7,6 +7,7 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import type { DeleteObjectCommandInput, @@ -36,7 +37,7 @@ const deleteObjectSerializer = ( input: DeleteObjectInput, endpoint: Endpoint ): HttpRequest => { - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); return { diff --git a/packages/storage/src/providers/s3/utils/client/getObject.ts b/packages/storage/src/providers/s3/utils/client/getObject.ts index 57b1692973e..02f730080aa 100644 --- a/packages/storage/src/providers/s3/utils/client/getObject.ts +++ b/packages/storage/src/providers/s3/utils/client/getObject.ts @@ -11,6 +11,7 @@ import { EMPTY_SHA256_HASH, HttpResponse, } from '@aws-amplify/core/internals/aws-client-utils'; +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import { S3EndpointResolverOptions, defaultConfig } from './base'; @@ -43,7 +44,7 @@ const getObjectSerializer = async ( input: GetObjectInput, endpoint: Endpoint ): Promise => { - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); return { diff --git a/packages/storage/src/providers/s3/utils/client/headObject.ts b/packages/storage/src/providers/s3/utils/client/headObject.ts index 04c3905b7e2..580162edff7 100644 --- a/packages/storage/src/providers/s3/utils/client/headObject.ts +++ b/packages/storage/src/providers/s3/utils/client/headObject.ts @@ -7,6 +7,7 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import { defaultConfig } from './base'; import type { HeadObjectCommandInput, HeadObjectCommandOutput } from './types'; @@ -41,7 +42,7 @@ const headObjectSerializer = async ( input: HeadObjectInput, endpoint: Endpoint ): Promise => { - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); return { diff --git a/packages/storage/src/providers/s3/utils/client/index.ts b/packages/storage/src/providers/s3/utils/client/index.ts index ee3f022a3a2..d32be9658ae 100644 --- a/packages/storage/src/providers/s3/utils/client/index.ts +++ b/packages/storage/src/providers/s3/utils/client/index.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import './polyfills'; - export { SERVICE_NAME } from './base'; export { getObject, diff --git a/packages/storage/src/providers/s3/utils/client/listObjectsV2.ts b/packages/storage/src/providers/s3/utils/client/listObjectsV2.ts index 0c8a73f33bd..ab2e5d2ebf6 100644 --- a/packages/storage/src/providers/s3/utils/client/listObjectsV2.ts +++ b/packages/storage/src/providers/s3/utils/client/listObjectsV2.ts @@ -7,6 +7,10 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { + AmplifyUrl, + AmplifyUrlSearchParams, +} from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import type { ListObjectsV2CommandInput, @@ -48,8 +52,8 @@ const listObjectsV2Serializer = ( prefix: input.Prefix, 'start-after': input.StartAfter, }); - const url = new URL(endpoint.url.toString()); - url.search = new URLSearchParams(query).toString(); + const url = new AmplifyUrl(endpoint.url.toString()); + url.search = new AmplifyUrlSearchParams(query).toString(); return { method: 'GET', headers, diff --git a/packages/storage/src/providers/s3/utils/client/listParts.ts b/packages/storage/src/providers/s3/utils/client/listParts.ts index cbddf7c09c2..ca369f8f0c6 100644 --- a/packages/storage/src/providers/s3/utils/client/listParts.ts +++ b/packages/storage/src/providers/s3/utils/client/listParts.ts @@ -7,6 +7,10 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { + AmplifyUrl, + AmplifyUrlSearchParams, +} from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import type { ListPartsCommandInput, @@ -41,11 +45,11 @@ const listPartsSerializer = async ( endpoint: Endpoint ): Promise => { const headers = {}; - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); validateS3RequiredParameter(!!input.UploadId, 'UploadId'); - url.search = new URLSearchParams({ + url.search = new AmplifyUrlSearchParams({ uploadId: input.UploadId, }).toString(); return { diff --git a/packages/storage/src/providers/s3/utils/client/polyfills/index.native.ts b/packages/storage/src/providers/s3/utils/client/polyfills/index.native.ts deleted file mode 100644 index 065c0fecb15..00000000000 --- a/packages/storage/src/providers/s3/utils/client/polyfills/index.native.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { loadUrlPolyfill } from '@aws-amplify/react-native'; - -loadUrlPolyfill(); diff --git a/packages/storage/src/providers/s3/utils/client/putObject.ts b/packages/storage/src/providers/s3/utils/client/putObject.ts index 0a74a2df6bb..f7d5a1b6218 100644 --- a/packages/storage/src/providers/s3/utils/client/putObject.ts +++ b/packages/storage/src/providers/s3/utils/client/putObject.ts @@ -7,6 +7,7 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import { defaultConfig } from './base'; @@ -56,7 +57,7 @@ const putObjectSerializer = async ( })), ...assignStringVariables({ 'content-md5': input.ContentMD5 }), }; - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); return { diff --git a/packages/storage/src/providers/s3/utils/client/uploadPart.ts b/packages/storage/src/providers/s3/utils/client/uploadPart.ts index 4785e457610..e02ba2c57a4 100644 --- a/packages/storage/src/providers/s3/utils/client/uploadPart.ts +++ b/packages/storage/src/providers/s3/utils/client/uploadPart.ts @@ -7,6 +7,10 @@ import { HttpResponse, parseMetadata, } from '@aws-amplify/core/internals/aws-client-utils'; +import { + AmplifyUrl, + AmplifyUrlSearchParams, +} from '@aws-amplify/core/internals/utils'; import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; import { defaultConfig } from './base'; @@ -41,12 +45,12 @@ const uploadPartSerializer = async ( ...assignStringVariables({ 'content-md5': input.ContentMD5 }), }; headers['content-type'] = 'application/octet-stream'; - const url = new URL(endpoint.url.toString()); + const url = new AmplifyUrl(endpoint.url.toString()); validateS3RequiredParameter(!!input.Key, 'Key'); url.pathname = serializePathnameObjectKey(url, input.Key); validateS3RequiredParameter(!!input.PartNumber, 'PartNumber'); validateS3RequiredParameter(!!input.UploadId, 'UploadId'); - url.search = new URLSearchParams({ + url.search = new AmplifyUrlSearchParams({ partNumber: input.PartNumber + '', uploadId: input.UploadId, }).toString(); From 3bbc475021a6879fadff4a5d220314566a798bb1 Mon Sep 17 00:00:00 2001 From: David McAfee Date: Thu, 12 Oct 2023 12:41:22 -0700 Subject: [PATCH 18/22] feat(data): add observer override to core Reachability util (#12279) --- packages/core/src/Reachability/Reachability.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/core/src/Reachability/Reachability.ts b/packages/core/src/Reachability/Reachability.ts index f98e258df13..1489a95c2b1 100644 --- a/packages/core/src/Reachability/Reachability.ts +++ b/packages/core/src/Reachability/Reachability.ts @@ -32,4 +32,18 @@ export class Reachability { }; }); } + + // expose observers to simulate offline mode for integration testing + private static _observerOverride(status: NetworkStatus): void { + for (const observer of this._observers) { + if (observer.closed) { + this._observers = this._observers.filter( + _observer => _observer !== observer + ); + continue; + } + + observer?.next && observer.next(status); + } + } } From bf9096c96df5ec2f2b3291215cff0cc02871550f Mon Sep 17 00:00:00 2001 From: israx <70438514+israx@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:47:23 -0400 Subject: [PATCH 19/22] fix(auth): returns refreshToken from login (#12284) * fix: return refreshToken from loging * chore: fix variable assignment * chore: return refreshTokenString --- .../providers/cognito/refreshToken.test.ts | 19 +++++++++++++------ .../cognito/utils/refreshAuthTokens.ts | 3 +-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/refreshToken.test.ts b/packages/auth/__tests__/providers/cognito/refreshToken.test.ts index 30e523ab134..95d597e017c 100644 --- a/packages/auth/__tests__/providers/cognito/refreshToken.test.ts +++ b/packages/auth/__tests__/providers/cognito/refreshToken.test.ts @@ -8,6 +8,8 @@ import * as clients from '../../../src/providers/cognito/utils/clients/CognitoId jest.mock('@aws-amplify/core/lib/clients/handlers/fetch'); describe('refresh token tests', () => { + const mockedUsername = 'mockedUsername'; + const mockedRefreshToken = 'mockedRefreshToken'; test('Default Cognito Token Refresh Handler', async () => { const succeedResponse = { status: 200, @@ -38,10 +40,11 @@ describe('refresh token tests', () => { idToken: decodeJWT( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0' ), - metadata: { - refreshToken: 'refreshtoken', - }, + + refreshToken: mockedRefreshToken, + clockDrift: 0, + username: mockedUsername, }; const expectedRequest = { url: new URL('https://cognito-idp.us-east-1.amazonaws.com/'), @@ -56,7 +59,7 @@ describe('refresh token tests', () => { ClientId: 'aaaaaaaaaaaa', AuthFlow: 'REFRESH_TOKEN_AUTH', AuthParameters: { - REFRESH_TOKEN: 'refreshtoken', + REFRESH_TOKEN: mockedRefreshToken, }, }), }; @@ -70,7 +73,8 @@ describe('refresh token tests', () => { payload: {}, }, clockDrift: 0, - refreshToken: 'refreshtoken', + refreshToken: mockedRefreshToken, + username: mockedUsername, }, authConfig: { Cognito: { @@ -78,11 +82,15 @@ describe('refresh token tests', () => { userPoolClientId: 'aaaaaaaaaaaa', }, }, + username: mockedUsername, }); expect(response.accessToken.toString()).toEqual( expectedOutput.accessToken.toString() ); + + expect(response.refreshToken).toEqual(expectedOutput.refreshToken); + expect(fetchTransferHandler).toBeCalledWith( expectedRequest, expect.anything() @@ -92,7 +100,6 @@ describe('refresh token tests', () => { describe('Cognito ASF', () => { let initiateAuthSpy; - let tokenProviderSpy; afterAll(() => { jest.restoreAllMocks(); }); diff --git a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts index 929f9e4a50a..4ab194f2d63 100644 --- a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts +++ b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts @@ -63,13 +63,12 @@ export const refreshAuthTokens: TokenRefresher = async ({ }); } const clockDrift = iat * 1000 - new Date().getTime(); - const refreshToken = AuthenticationResult?.RefreshToken; return { accessToken, idToken, clockDrift, - refreshToken, + refreshToken: refreshTokenString, username: `${accessToken.payload.username}`, }; }; From 87a4e3765135daed555fd78c33abc00cd60e8815 Mon Sep 17 00:00:00 2001 From: Chris F <5827964+cshfang@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:07:53 -0700 Subject: [PATCH 20/22] chore: Clean up Cache to use share code and use native extension. (#12264) --- .../personalize/utils/cachedSession.test.ts | 14 +- .../src/providers/personalize/apis/record.ts | 6 +- .../personalize/utils/cachedSession.ts | 8 +- packages/aws-amplify/typeDoc.js | 2 +- .../Cache/BrowserStorageCache.test.ts | 444 ------ .../Cache/InMemoryStorageCache.test.ts | 375 ----- .../core/__tests__/Cache/StorageCache.test.ts | 225 ++- .../Cache/StorageCacheCommon.test.ts | 593 ++++++++ .../CacheList.test.ts} | 4 +- .../Cache/{Utils => utils}/cacheUtils.test.ts | 6 +- packages/core/package.json | 15 +- packages/core/src/Cache/AsyncStorageCache.ts | 495 ------- .../core/src/Cache/BrowserStorageCache.ts | 504 ------- packages/core/src/Cache/CHANGELOG.md | 1239 ----------------- packages/core/src/Cache/InMemoryCache.ts | 347 ----- .../core/src/Cache/StorageCache.native.ts | 65 + packages/core/src/Cache/StorageCache.ts | 219 +-- packages/core/src/Cache/StorageCacheCommon.ts | 581 ++++++++ packages/core/src/Cache/Utils/CacheUtils.ts | 90 -- packages/core/src/Cache/constants.ts | 18 + packages/core/src/Cache/index.ts | 6 + packages/core/src/Cache/reactnative.ts | 9 - packages/core/src/Cache/types/Cache.ts | 75 - packages/core/src/Cache/types/cache.ts | 51 + packages/core/src/Cache/types/index.ts | 2 +- .../src/Cache/{Utils => utils}/CacheList.ts | 370 ++--- packages/core/src/Cache/utils/cacheHelpers.ts | 52 + .../Cache/{Utils => utils}/errorHelpers.ts | 4 - .../core/src/Cache/{Utils => utils}/index.ts | 9 +- packages/core/src/index.ts | 6 +- .../providers/pinpoint/apis/updateEndpoint.ts | 2 +- packages/core/src/singleton/Cache/types.ts | 27 + packages/core/src/singleton/types.ts | 4 +- packages/core/src/storage/DefaultStorage.ts | 4 +- packages/core/src/storage/utils.ts | 2 +- 35 files changed, 1788 insertions(+), 4085 deletions(-) delete mode 100644 packages/core/__tests__/Cache/BrowserStorageCache.test.ts delete mode 100644 packages/core/__tests__/Cache/InMemoryStorageCache.test.ts create mode 100644 packages/core/__tests__/Cache/StorageCacheCommon.test.ts rename packages/core/__tests__/Cache/{Utils/cacheList.test.ts => utils/CacheList.test.ts} (97%) rename packages/core/__tests__/Cache/{Utils => utils}/cacheUtils.test.ts (67%) delete mode 100644 packages/core/src/Cache/AsyncStorageCache.ts delete mode 100644 packages/core/src/Cache/BrowserStorageCache.ts delete mode 100644 packages/core/src/Cache/CHANGELOG.md delete mode 100644 packages/core/src/Cache/InMemoryCache.ts create mode 100644 packages/core/src/Cache/StorageCache.native.ts create mode 100644 packages/core/src/Cache/StorageCacheCommon.ts delete mode 100644 packages/core/src/Cache/Utils/CacheUtils.ts create mode 100644 packages/core/src/Cache/constants.ts create mode 100644 packages/core/src/Cache/index.ts delete mode 100644 packages/core/src/Cache/reactnative.ts delete mode 100644 packages/core/src/Cache/types/Cache.ts create mode 100644 packages/core/src/Cache/types/cache.ts rename packages/core/src/Cache/{Utils => utils}/CacheList.ts (95%) create mode 100644 packages/core/src/Cache/utils/cacheHelpers.ts rename packages/core/src/Cache/{Utils => utils}/errorHelpers.ts (85%) rename packages/core/src/Cache/{Utils => utils}/index.ts (55%) create mode 100644 packages/core/src/singleton/Cache/types.ts diff --git a/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts b/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts index b72e641f2e9..ec6bc5aa1da 100644 --- a/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts +++ b/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Cache, BrowserStorageCache } from '@aws-amplify/core'; +import { Cache } from '@aws-amplify/core'; import { isBrowser, amplifyUuid } from '@aws-amplify/core/internals/utils'; import { resolveCachedSession, @@ -16,7 +16,7 @@ const mockAmplifyUuid = amplifyUuid as jest.Mock; describe('Analytics service provider Personalize utils: cachedSession', () => { const sessionIdCacheKey = '_awsct_sid.personalize'; const userIdCacheKey = '_awsct_uid.personalize'; - const mockCache = Cache as jest.Mocked; + const mockCache = Cache as jest.Mocked; const mockIsBrowser = isBrowser as jest.Mock; const mockUuid = 'b2bd676e-bc6b-40f4-bd86-1e31a07f7d10'; @@ -42,14 +42,14 @@ describe('Analytics service provider Personalize utils: cachedSession', () => { mockCache.setItem.mockReset(); }); - it('resolve cached session from Cache', () => { - const result = resolveCachedSession('trackingId0'); + it('resolve cached session from Cache', async () => { + const result = await resolveCachedSession(); expect(result).toStrictEqual(mockSession); }); - it('create a new session if there is no cache', () => { - mockCache.getItem.mockImplementation(() => undefined); - const result = resolveCachedSession('trackingId0'); + it('create a new session if there is no cache', async () => { + mockCache.getItem.mockImplementation(async () => undefined); + const result = await resolveCachedSession(); expect(result.sessionId).not.toBe(mockSession.sessionId); expect(result.sessionId).toEqual(mockUuid); expect(result.userId).toBe(undefined); diff --git a/packages/analytics/src/providers/personalize/apis/record.ts b/packages/analytics/src/providers/personalize/apis/record.ts index 2d7dfa07b32..12c69f1a185 100644 --- a/packages/analytics/src/providers/personalize/apis/record.ts +++ b/packages/analytics/src/providers/personalize/apis/record.ts @@ -39,10 +39,10 @@ export const record = ({ const { region, trackingId, bufferSize, flushSize, flushInterval } = resolveConfig(); resolveCredentials() - .then(({ credentials, identityId }) => { + .then(async ({ credentials, identityId }) => { const timestamp = Date.now(); const { sessionId: cachedSessionId, userId: cachedUserId } = - resolveCachedSession(trackingId); + await resolveCachedSession(); if (eventType === IDENTIFY_EVENT_TYPE) { updateCachedSession( typeof properties.userId === 'string' ? properties.userId : '', @@ -54,7 +54,7 @@ export const record = ({ } const { sessionId: updatedSessionId, userId: updatedUserId } = - resolveCachedSession(trackingId); + await resolveCachedSession(); const eventBuffer = getEventBuffer({ region, diff --git a/packages/analytics/src/providers/personalize/utils/cachedSession.ts b/packages/analytics/src/providers/personalize/utils/cachedSession.ts index c92550b2939..e791fc5691f 100644 --- a/packages/analytics/src/providers/personalize/utils/cachedSession.ts +++ b/packages/analytics/src/providers/personalize/utils/cachedSession.ts @@ -26,14 +26,16 @@ const setCache = (key: string, value: unknown) => { }); }; -export const resolveCachedSession = (trackingId: string) => { - let sessionId: string | undefined = getCache(PERSONALIZE_CACHE_SESSIONID); +export const resolveCachedSession = async () => { + let sessionId: string | undefined = await getCache( + PERSONALIZE_CACHE_SESSIONID + ); if (!sessionId) { sessionId = amplifyUuid(); setCache(PERSONALIZE_CACHE_SESSIONID, sessionId); } - const userId: string | undefined = getCache(PERSONALIZE_CACHE_USERID); + const userId: string | undefined = await getCache(PERSONALIZE_CACHE_USERID); return { sessionId, diff --git a/packages/aws-amplify/typeDoc.js b/packages/aws-amplify/typeDoc.js index 52c34666f69..c22a8c022d7 100644 --- a/packages/aws-amplify/typeDoc.js +++ b/packages/aws-amplify/typeDoc.js @@ -3,7 +3,7 @@ module.exports = { readme: '../../README.md', media: '../../media', - exclude: '**/*+(InMemoryCache|ErrorUtils|CacheUtils|cacheList|index).ts', + exclude: '**/*+(ErrorUtils|cacheHelpers|CacheList|index).ts', excludeExternals: true, excludeNotExported: true, excludePrivate: true, diff --git a/packages/core/__tests__/Cache/BrowserStorageCache.test.ts b/packages/core/__tests__/Cache/BrowserStorageCache.test.ts deleted file mode 100644 index 88ad83c2cca..00000000000 --- a/packages/core/__tests__/Cache/BrowserStorageCache.test.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { CacheConfig, CacheItem } from '../../src/Cache/types/Cache'; -import { defaultConfig, getByteLength } from '../../src/Cache/Utils/CacheUtils'; -import { - BrowserStorageCache as cache, - BrowserStorageCacheClass, -} from '../../src/Cache/BrowserStorageCache'; - -const config: CacheConfig = { - capacityInBytes: 3000, - itemMaxSize: 800, - defaultTTL: 3000000, - defaultPriority: 5, - warningThreshold: 0.8, - storage: window.localStorage, -}; - -cache.configure(config); - -function getItemSize(value: string): number { - const currTime: Date = new Date(); - const ret: CacheItem = { - key: defaultConfig.keyPrefix + 'a', - data: value, - timestamp: currTime.getTime(), - visitedTime: currTime.getTime(), - priority: 5, - expires: currTime.getTime(), - type: typeof value, - byteSize: 0, - }; - ret.byteSize = getByteLength(JSON.stringify(ret)); - ret.byteSize = getByteLength(JSON.stringify(ret)); - return ret.byteSize; -} - -describe('BrowserStorageCache', () => { - const cache_size = config.capacityInBytes || defaultConfig.capacityInBytes; - const default_ttl = config.defaultTTL || defaultConfig.defaultTTL; - const item_max_size = config.itemMaxSize || defaultConfig.itemMaxSize; - const warningThreshold = - config.warningThreshold || defaultConfig.warningThreshold; - - let regularItem: string = ''; - for (let i = 0; i < item_max_size / 2; i++) { - regularItem += 'a'; - } - - const regularItemSize: number = getItemSize(regularItem); - - const maxItemNum: number = Math.floor(cache_size / regularItemSize); - const itemsNeedToPop: number = Math.ceil( - (cache_size * (1 - warningThreshold)) / regularItemSize - ); - - if (maxItemNum > default_ttl) { - console.error('incorrect paratmeter for test!'); - } - - const spyonConsoleWarn = jest.spyOn(console, 'warn'); - - describe('setItem test', () => { - test('put string, happy case', () => { - const key: string = 'a'; - cache.setItem(key, regularItem); - const ret = cache.getItem(key); - - expect(ret).toBe(regularItem); - cache.clear(); - }); - - test('put object, happy case', () => { - const key: string = 'a'; - const item: object = { abc: 123, edf: 456 }; - - cache.setItem(key, item); - const ret = cache.getItem(key); - - expect(ret).toEqual(item); - - cache.clear(); - }); - - test('put number, happy case', () => { - const key: string = 'a'; - const item: number = 1234; - cache.setItem(key, item); - const ret = cache.getItem(key); - - expect(ret).toBe(item); - - cache.clear(); - }); - - test('put boolean, happy case', () => { - const key: string = 'a'; - const item: boolean = true; - cache.setItem(key, item); - const ret = cache.getItem(key); - - expect(ret).toBe(item); - - cache.clear(); - }); - - test('abort and output console warning when trying to put one big item', () => { - let value: string; - let key: string = 'b'; - for (let i = 0; i < item_max_size * 2; i++) { - value += 'a'; - } - - cache.setItem(key, value); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem(key)).toBe(null); - spyonConsoleWarn.mockReset(); - }); - - test('abort and output console warning when invalid keys', () => { - cache.setItem('', 'abc'); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - - cache.setItem('CurSize', 'abc'); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - }); - - test('abort and output console warning if wrong CacheConfigOptions', () => { - cache.setItem('a', 'abc', { priority: 0 }); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem('a')).toBeNull(); - spyonConsoleWarn.mockReset(); - - cache.setItem('a', 'abc', { priority: 6 }); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem('a')).toBeNull(); - spyonConsoleWarn.mockReset(); - }); - - test('abort and output console warning if value is undefined', () => { - cache.setItem('a', undefined); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem('a')).toBeNull(); - spyonConsoleWarn.mockReset(); - }); - - test('update the cache content if same key in the cache when setItem invoked', () => { - const key: string = 'a'; - const val1: string = 'abc'; - const val2: string = 'cbaabc'; - - cache.setItem(key, val1); - const cacheSizeBefore: number = cache.getCacheCurSize(); - const ret1 = cache.getItem(key); - expect(ret1).toBe(val1); - - cache.setItem(key, val2); - const cacheSizeAfter: number = cache.getCacheCurSize(); - const ret2 = cache.getItem(key); - expect(ret2).toBe(val2); - - expect(cacheSizeAfter - cacheSizeBefore).toBe( - getItemSize(val2) - getItemSize(val1) - ); - cache.clear(); - }); - - test('pop low priority items when cache is full', () => { - let key: string; - const ttl: number = 300; - // default priority is 5 - const priority: number = 4; - - let keysPoped: string[] = []; - // fill the cache - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - if (i < itemsNeedToPop) { - cache.setItem(key, regularItem); - keysPoped.push(key); - } else cache.setItem(key, regularItem, { priority: priority }); - } - - for (let i = 0; i < keysPoped.length; i++) { - expect(cache.getItem(keysPoped[i])).not.toBeNull(); - } - - key = maxItemNum.toString(); - cache.setItem(key, regularItem); - - for (let i = 0; i <= maxItemNum; i++) { - if (i < keysPoped.length) { - expect(cache.getItem(keysPoped[i])).toBeNull(); - } else { - expect(cache.getItem(i.toString())).not.toBeNull(); - } - } - - cache.clear(); - }); - - test('pop last visited items when same priority', () => { - let key: string = 'a'; - const dateSpy = jest.spyOn(Date.prototype, 'getTime'); - let keysPoped: string[] = []; - - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - dateSpy.mockImplementation(() => { - return 1434319925275 + i; - }); - if (i < itemsNeedToPop) { - keysPoped.push(key); - } - cache.setItem(key, regularItem); - } - - key = maxItemNum.toString(); - cache.setItem(key, regularItem); - - for (let i = 0; i <= maxItemNum; i++) { - if (i < keysPoped.length) { - expect(cache.getItem(keysPoped[i])).toBeNull(); - } else { - expect(cache.getItem(i.toString())).not.toBeNull(); - } - } - - cache.clear(); - dateSpy.mockRestore(); - }); - - test('wipe out expired items when cache is full and after that cache has enough room for the item', () => { - let key: string = 'a'; - const dateSpy = jest.spyOn(Date.prototype, 'getTime'); - - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - dateSpy.mockImplementation(() => { - return 1434319925275 + i; - }); - // set ttl to maxItemNum/2 - cache.setItem(key, regularItem, { - expires: 1434319925275 + i + maxItemNum / 2, - }); - } - - dateSpy.mockImplementation(() => { - return 1434319925275 + maxItemNum; - }); - - key = maxItemNum.toString(); - cache.setItem(key, regularItem); - - for (let i = 0; i < maxItemNum; i++) { - if (i < (maxItemNum % 2 ? maxItemNum / 2 : maxItemNum / 2 + 1)) { - expect(cache.getItem(i.toString())).toBeNull(); - } else { - expect(cache.getItem(i.toString())).not.toBeNull(); - } - } - cache.clear(); - dateSpy.mockRestore(); - }); - }); - - describe('getItem test', () => { - test('get item, happy case', () => { - let key: string = 'a'; - - cache.setItem(key, regularItem); - const ret = cache.getItem(key); - - cache.clear(); - }); - - test('item get cleaned if it expires when trying to get it', () => { - const timeSpy = jest.spyOn(Date.prototype, 'getTime'); - let key: string = 'a'; - - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - timeSpy.mockImplementation(() => { - return 1434319925275 + i * default_ttl; - }); - cache.setItem(key, regularItem); - } - - expect(cache.getItem('0')).toBeNull(); - - timeSpy.mockRestore(); - cache.clear(); - }); - - test('return null when no such key in the cache', () => { - expect(cache.getItem('abc')).toBeNull(); - }); - - test('item get refreshed when fetched from cache', () => { - let key: string = 'a'; - let keysPoped: string[] = []; - const timeSpy = jest.spyOn(Date.prototype, 'getTime'); - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - timeSpy.mockImplementation(() => { - // to ensure no item is expired - return 1434319925275 + i * (default_ttl / (maxItemNum * 2)); - }); - if (i < itemsNeedToPop) { - keysPoped.push(key); - } - cache.setItem(key, regularItem); - } - - // refreshed - cache.getItem(keysPoped[0]); - key = maxItemNum.toString(); - cache.setItem(key, regularItem); - - for (let i = 0; i < keysPoped.length; i++) { - if (i == 0) { - expect(cache.getItem(keysPoped[0])).not.toBeNull(); - } else { - expect(cache.getItem(keysPoped[i])).toBeNull(); - } - } - - cache.clear(); - timeSpy.mockRestore(); - }); - - test('execute function if specified when no such key in cache', () => { - const execFunc: Function = data => { - return data * 5; - }; - - expect( - cache.getItem('a', { - callback: () => { - return execFunc(5); - }, - }) - ).toBe(25); - - expect(cache.getItem('a')).toBe(25); - - cache.clear(); - }); - - test('output a console warning and return null if invalid keys', () => { - cache.getItem(''); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - - cache.getItem('CurSize'); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - }); - }); - - describe('removeItem test', () => { - test('remove cache, happy case', () => { - let key: string = 'a'; - cache.setItem(key, regularItem); - expect(cache.getItem(key)).not.toBeNull(); - - cache.removeItem(key); - expect(cache.getItem(key)).toBeNull(); - - cache.clear(); - }); - }); - - describe('clear test', () => { - test('clear the cache, including the CurSize key', () => { - let key: string = 'a'; - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - cache.setItem(key, regularItem); - } - - key = 'a'; - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - expect(cache.getItem(key)).not.toBeNull(); - } - expect(cache.getCacheCurSize()).not.toBe(0); - - cache.clear(); - - key = 'a'; - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - expect(cache.getItem(key)).toBeNull(); - } - expect(cache.getCacheCurSize()).toBe(0); - }); - - test("will not remove other users' item", () => { - window.sessionStorage.setItem('others-testb', 'abc'); - - cache.setItem('a', 'abc'); - cache.clear(); - - expect(cache.getItem('a')).toBeNull(); - expect(window.sessionStorage.getItem('others-testb')).not.toBeNull(); - }); - }); - - describe('getCacheCurSize test', () => { - test('return 0 if cache is empty', () => { - expect(cache.getCacheCurSize()).toBe(0); - }); - - test('return cache currrent size if not empty', () => { - for (let i = 0; i < maxItemNum; i++) { - let key = i.toString(); - cache.setItem(key, regularItem); - } - - expect(cache.getCacheCurSize()).toBe(regularItemSize * maxItemNum); - cache.clear(); - }); - }); - - describe('getAllKeys test', () => { - test('happy case', () => { - cache.setItem('a', 123); - cache.setItem('b', 'abc'); - cache.setItem('c', { abc: 123 }); - - expect(cache.getAllKeys()).toEqual(['a', 'b', 'c']); - cache.clear(); - }); - }); - - describe('createInstance', () => { - test('happy case, return new instance', () => { - expect(cache.createInstance({ keyPrefix: 'abc' })).toBeInstanceOf( - BrowserStorageCacheClass - ); - }); - }); -}); diff --git a/packages/core/__tests__/Cache/InMemoryStorageCache.test.ts b/packages/core/__tests__/Cache/InMemoryStorageCache.test.ts deleted file mode 100644 index 3b11961e8cc..00000000000 --- a/packages/core/__tests__/Cache/InMemoryStorageCache.test.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { - InMemoryCache as cache, - InMemoryCacheClass, -} from '../../src/Cache/InMemoryCache'; -import { defaultConfig, getByteLength } from '../../src/Cache/Utils/CacheUtils'; -import { CacheConfig, CacheItem } from '../../src/Cache/types/Cache'; - -function getItemSize(value: string): number { - const currTime: Date = new Date(); - const ret: CacheItem = { - key: defaultConfig.keyPrefix + 'a', - data: value, - timestamp: currTime.getTime(), - visitedTime: currTime.getTime(), - priority: 5, - expires: currTime.getTime(), - type: typeof value, - byteSize: 0, - }; - ret.byteSize = getByteLength(JSON.stringify(ret)); - ret.byteSize = getByteLength(JSON.stringify(ret)); - return ret.byteSize; -} - -const config: CacheConfig = { - capacityInBytes: 3000, - itemMaxSize: 600, - defaultTTL: 3000000, - defaultPriority: 5, - warningThreshold: 0.8, - storage: window.localStorage, -}; - -cache.configure(config); - -describe('InMemoryCache', () => { - const cache_size = config['capacityInBytes'] || defaultConfig.capacityInBytes; - const default_ttl = config['defaultTTL'] || defaultConfig.defaultTTL; - const item_max_size = config['itemMaxSize'] || defaultConfig.itemMaxSize; - - let regularItem: string = ''; - // item size would be 339 Byte - for (let i = 0; i < item_max_size / 2; i++) { - regularItem += 'a'; - } - - const regularItemSize: number = getItemSize(regularItem); - const maxItemNum: number = Math.floor(cache_size / regularItemSize); - - if (maxItemNum > default_ttl) { - console.error('incorrect paratmeter for test!'); - } - - const spyonConsoleWarn = jest.spyOn(console, 'warn'); - - describe('setItem test', () => { - test('put string, happy case', () => { - let key: string = 'a'; - cache.setItem(key, regularItem); - expect(cache.getItem(key)).toBe(regularItem); - - cache.clear(); - }); - - test('put object, happy case', () => { - let key: string = 'a'; - const item: object = { abc: 123, edf: 456 }; - cache.setItem(key, item); - expect(cache.getItem(key)).toEqual(item); - - cache.clear(); - }); - - test('put number, happy case', () => { - let key: string = 'a'; - const item: number = 12345; - cache.setItem(key, item); - expect(cache.getItem(key)).toBe(item); - - cache.clear(); - }); - - test('put boolean, happy case', () => { - let key: string = 'a'; - const item: boolean = false; - cache.setItem(key, item); - expect(cache.getItem(key)).toBe(item); - - cache.clear(); - }); - - test('abort and output console warning when trying to put one big item', () => { - let value: string; - let key: string = 'b'; - for (let i = 0; i < item_max_size * 2; i++) { - value += 'a'; - } - - cache.setItem(key, value); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem(key)).toBe(null); - spyonConsoleWarn.mockReset(); - }); - - test('abort and output console warning when invalid keys', () => { - cache.setItem('', 'abc'); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - - cache.setItem('CurSize', 'abc'); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - }); - - test('abort and output console warning if wrong CacheConfigOptions', () => { - cache.setItem('a', 'abc', { priority: 0 }); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem('a')).toBeNull(); - spyonConsoleWarn.mockReset(); - - cache.setItem('a', 'abc', { priority: 6 }); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem('a')).toBeNull(); - spyonConsoleWarn.mockReset(); - }); - - test('abort and output console warning if value is undefined', () => { - cache.setItem('a', undefined); - expect(spyonConsoleWarn).toBeCalled(); - expect(cache.getItem('a')).toBeNull(); - spyonConsoleWarn.mockReset(); - }); - - test('update the cache content if same key in the cache when setItem invoked', () => { - const key: string = 'a'; - const val1: string = 'abc'; - const val2: string = 'cbaabc'; - - cache.setItem(key, val1); - const cacheSizeBefore = cache.getCacheCurSize(); - expect(cache.getItem(key)).toBe(val1); - cache.setItem(key, val2); - const cacheSizeAfter = cache.getCacheCurSize(); - expect(cache.getItem(key)).toBe(val2); - - expect(cacheSizeAfter - cacheSizeBefore).toBe( - getItemSize(val2) - getItemSize(val1) - ); - cache.clear(); - }); - - test('pop low priority items when cache is full', () => { - let key: string; - const ttl: number = 300; - // default priority is 5 - const priority: number = 4; - - let keysPoped: string[] = []; - // fill the cache - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - if (i == 0) { - cache.setItem(key, regularItem, { priority: 4 }); - keysPoped.push(key); - } else cache.setItem(key, regularItem, { priority: 3 }); - } - - for (let i = 0; i < keysPoped.length; i++) { - expect(cache.getItem(keysPoped[i])).not.toBeNull(); - } - - key = maxItemNum.toString(); - cache.setItem(key, regularItem); - - for (let i = 0; i <= maxItemNum; i++) { - if (i < keysPoped.length) { - expect(cache.getItem(keysPoped[i])).toBeNull(); - } else { - expect(cache.getItem(i.toString())).not.toBeNull(); - } - } - - cache.clear(); - }); - - test('pop last visited items when same priority', () => { - let key: string = 'a'; - const dateSpy = jest.spyOn(Date.prototype, 'getTime'); - let keysPoped: string[] = []; - - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - dateSpy.mockImplementation(() => { - return 1434319925275 + i; - }); - if (i == 0) { - keysPoped.push(key); - } - cache.setItem(key, regularItem); - } - - key = maxItemNum.toString(); - cache.setItem(key, regularItem); - - for (let i = 0; i < keysPoped.length; i++) { - expect(cache.getItem(keysPoped[i])).toBeNull(); - } - - cache.clear(); - dateSpy.mockRestore(); - }); - }); - - describe('getItem test', () => { - test('get item, happy case', () => { - let key: string = 'a'; - - cache.setItem(key, regularItem); - expect(cache.getItem(key)).toBe(regularItem); - cache.clear(); - }); - - test('item get cleaned if expired when trying to get it', () => { - const timeSpy = jest.spyOn(Date.prototype, 'getTime'); - let key: string = 'a'; - - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - timeSpy.mockImplementation(() => { - return 1434319925275 + i * default_ttl; - }); - cache.setItem(key, regularItem); - } - - expect(cache.getItem('0')).toBeNull(); - timeSpy.mockRestore(); - cache.clear(); - }); - - test('return null when no such key in the cache', () => { - expect(cache.getItem('abc')).toBeNull(); - }); - - test('item get refreshed when fetched from cache', () => { - let key: string = 'a'; - - const timeSpy = jest.spyOn(Date.prototype, 'getTime'); - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - timeSpy.mockImplementation(() => { - // to ensure no item is expired - return 1434319925275 + i * (default_ttl / (maxItemNum * 2)); - }); - cache.setItem(key, regularItem); - } - - // refreshed - cache.getItem('0'); - - key = maxItemNum.toString(); - cache.setItem(key, regularItem); - - expect(cache.getItem('0')).not.toBeNull(); - expect(cache.getItem('1')).toBeNull(); - //myHandler.showTheList(); - cache.clear(); - timeSpy.mockRestore(); - }); - - test('execute function if specified when no such key in cache', () => { - const execFunc: Function = data => { - return data * 5; - }; - - expect( - cache.getItem('a', { - callback: () => { - return execFunc(5); - }, - }) - ).toBe(25); - - expect(cache.getItem('a')).toBe(25); - - cache.clear(); - //expect(cache.getItem('a', callback)).toBe(5); - }); - - test('output a console warning and return null if invalid keys', () => { - cache.getItem(''); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - - cache.getItem('CurSize'); - expect(spyonConsoleWarn).toBeCalled(); - spyonConsoleWarn.mockReset(); - }); - }); - - describe('removeItem test', () => { - test('remove cache, happy case', () => { - let key: string = 'a'; - cache.setItem(key, regularItem); - expect(cache.getItem(key)).not.toBeNull(); - - cache.removeItem(key); - expect(cache.getItem(key)).toBeNull(); - - cache.clear(); - }); - }); - - describe('clear test', () => { - test('clear the cache', () => { - let key: string = 'a'; - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - cache.setItem(key, regularItem); - } - - key = 'a'; - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - expect(cache.getItem(key)).not.toBeNull(); - } - - cache.clear(); - - key = 'a'; - for (let i = 0; i < maxItemNum / 2; i++) { - key = i.toString(); - expect(cache.getItem(key)).toBeNull(); - } - }); - }); - - describe('getItemCursize test', () => { - test('return 0 if cache is empty', () => { - expect(cache.getCacheCurSize()).toBe(0); - }); - - test('return cache currrent size if not empty', () => { - let key: string = 'a'; - for (let i = 0; i < maxItemNum; i++) { - key = i.toString(); - cache.setItem(key, regularItem); - } - - expect(cache.getCacheCurSize()).toBe(regularItemSize * maxItemNum); - - cache.clear(); - console.log(String.fromCharCode(0xdc33)); - }); - }); - - describe('getAllKeys test', () => { - test('happy case', async () => { - await cache.setItem('a', 123); - await cache.setItem('b', 'abc'); - await cache.setItem('c', { abc: 123 }); - - expect.assertions(1); - expect(await cache.getAllKeys()).toEqual(['a', 'b', 'c']); - await cache.clear(); - }); - }); - - describe('createInstance', () => { - test('happy case, return new instance', () => { - expect(cache.createInstance({ keyPrefix: 'abc' })).toBeInstanceOf( - InMemoryCacheClass - ); - }); - }); -}); diff --git a/packages/core/__tests__/Cache/StorageCache.test.ts b/packages/core/__tests__/Cache/StorageCache.test.ts index ec44e407871..e8c502cf454 100644 --- a/packages/core/__tests__/Cache/StorageCache.test.ts +++ b/packages/core/__tests__/Cache/StorageCache.test.ts @@ -1,141 +1,138 @@ import { CacheConfig } from '../../src/Cache/types/Cache'; +import { defaultConfig } from '../../src/Cache/constants'; import { StorageCache } from '../../src/Cache/StorageCache'; -import { defaultConfig } from '../../src/Cache/Utils'; -import { ConsoleLogger as Logger } from '../../src/Logger'; - -const config: CacheConfig = { - keyPrefix: 'aws-amplify#$#', - capacityInBytes: 3000, - itemMaxSize: 600, - defaultTTL: 3000000, - defaultPriority: 5, - warningThreshold: 0.8, -}; +import { getCurrentSizeKey } from '../../src/Cache/utils'; +import { getLocalStorageWithFallback } from '../../src/storage/utils'; + +jest.mock('../../src/Cache/utils'); +jest.mock('../../src/storage/utils'); describe('StorageCache', () => { + const keyPrefix = 'key-prefix-'; + const currentSizeKey = `${keyPrefix}current-size-key`; + const config: CacheConfig = { + keyPrefix, + capacityInBytes: 3000, + itemMaxSize: 600, + defaultTTL: 3000000, + defaultPriority: 5, + warningThreshold: 0.8, + }; + // create mocks + const mockGetLocalStorageWithFallback = + getLocalStorageWithFallback as jest.Mock; + const mockGetCurrentSizeKey = getCurrentSizeKey as jest.Mock; + const mockStorageSetItem = jest.fn(); + const mockStorageGetItem = jest.fn(); + const mockStorageRemoveItem = jest.fn(); + const mockStorageClear = jest.fn(); + const mockStorageKey = jest.fn(); + const mockStorage: Storage = { + setItem: mockStorageSetItem, + getItem: mockStorageGetItem, + removeItem: mockStorageRemoveItem, + clear: mockStorageClear, + key: mockStorageKey, + length: 0, + }; + // extend class for testing + class StorageCacheTest extends StorageCache { + testGetConfig() { + return this.config; + } + + testGetAllCacheKeys(options?: { omitSizeKey?: boolean }) { + return this.getAllCacheKeys(options); + } + } + // create test helpers + const getStorageCache = (config?: CacheConfig) => + new StorageCacheTest(config); + + beforeAll(() => { + mockGetCurrentSizeKey.mockReturnValue(currentSizeKey); + }); + + beforeEach(() => { + mockGetLocalStorageWithFallback.mockReturnValue(mockStorage); + }); + + afterEach(() => { + mockGetLocalStorageWithFallback.mockReset(); + mockStorageKey.mockReset(); + }); + describe('constructor', () => { - test('set to default if config capacityInBytes is not integer', () => { - const tmp = config.capacityInBytes; - config.capacityInBytes = 1048576; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().capacityInBytes).toBe( - defaultConfig.capacityInBytes - ); - config.capacityInBytes = tmp; + it('can be constructed with default configurations', () => { + const cache = getStorageCache(); + expect(mockGetLocalStorageWithFallback).toBeCalled(); + expect(cache.testGetConfig()).toStrictEqual(defaultConfig); }); - test('set to default if config capacityInBytes is not integer', () => { - const tmp = config.capacityInBytes; - config.capacityInBytes = 1048576; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().capacityInBytes).toBe( - defaultConfig.capacityInBytes - ); - config.capacityInBytes = tmp; + it('can be constructed with custom configurations', () => { + const cache = getStorageCache(config); + expect(mockGetLocalStorageWithFallback).toBeCalled(); + expect(cache.testGetConfig()).toStrictEqual(config); }); + }); - test('set to default if config capacityInBytes is not integer', () => { - const tmp = config.capacityInBytes; - config.capacityInBytes = 1048576; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().capacityInBytes).toBe( - defaultConfig.capacityInBytes - ); - config.capacityInBytes = tmp; - }); + describe('getAllCacheKeys()', () => { + const keys = ['current-size-key', 'key-1', 'key-2']; + const cachedKeys = keys.map(key => `${keyPrefix}${key}`); - test('set to default if config itemMaxSize is not integer', () => { - const tmp = config.itemMaxSize; - config.itemMaxSize = 210000; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().itemMaxSize).toBe(defaultConfig.itemMaxSize); - config.itemMaxSize = tmp; + beforeEach(() => { + mockStorageKey.mockImplementation((index: number) => cachedKeys[index]); + mockGetLocalStorageWithFallback.mockReturnValue({ + ...mockStorage, + length: cachedKeys.length, + }); }); - test('set to default if config defaultTTL is not integer', () => { - const tmp = config.defaultTTL; - config.defaultTTL = 259200000; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().defaultTTL).toBe(defaultConfig.defaultTTL); - config.defaultTTL = tmp; - }); + it('returns all cache keys', async () => { + const cache = getStorageCache(config); - test('set to default if config defaultPriority is not integer', () => { - const tmp = config.defaultPriority; - config.defaultPriority = 5; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().defaultPriority).toBe( - defaultConfig.defaultPriority - ); - config.defaultPriority = tmp; + expect(await cache.testGetAllCacheKeys()).toStrictEqual(keys); }); - test('set to default if itemMaxSize is bigger then capacityInBytes', () => { - const tmp = config.itemMaxSize; - config.itemMaxSize = config.capacityInBytes * 2; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().itemMaxSize).toBe(defaultConfig.itemMaxSize); - config.itemMaxSize = tmp; - }); + it('can omit the size key', async () => { + const cache = getStorageCache(config); - test('set to default if defaultPriority is out of range', () => { - const tmp = config.defaultPriority; - config.defaultPriority = 0; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().defaultPriority).toBe( - defaultConfig.defaultPriority - ); - config.defaultPriority = 6; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().defaultPriority).toBe( - defaultConfig.defaultPriority - ); - config.defaultPriority = tmp; + expect( + await cache.testGetAllCacheKeys({ omitSizeKey: true }) + ).toStrictEqual(keys.slice(1)); }); - test('set to default if warningThreshold is out of range', () => { - const tmp = config.warningThreshold; - config.warningThreshold = Math.random() + 1; - const storage: StorageCache = new StorageCache(config); - expect(storage.configure().warningThreshold).toBe( - defaultConfig.warningThreshold + it('only returns keys that it owns', async () => { + const extendedCachedKeys = [ + ...keys.map(key => `${keyPrefix}${key}`), + 'some-other-prefixed-key', + ]; + mockStorageKey.mockImplementation( + (index: number) => extendedCachedKeys[index] ); - config.warningThreshold = tmp; + mockGetLocalStorageWithFallback.mockReturnValue({ + ...mockStorage, + length: extendedCachedKeys.length, + }); + const cache = getStorageCache(config); + + expect( + await cache.testGetAllCacheKeys({ omitSizeKey: true }) + ).toStrictEqual(keys.slice(1)); }); }); - describe('config test', () => { - test('happy case', () => { - const storage: StorageCache = new StorageCache(config); + describe('createInstance()', () => { + const cache = getStorageCache(config); - const customizedConfig: CacheConfig = { - itemMaxSize: 1000, + it('returns a new instance', () => { + const newConfig: CacheConfig = { + ...config, + keyPrefix: 'foo-', }; - - const verifiedConfig = storage.configure(customizedConfig); - - expect(verifiedConfig.itemMaxSize).toBe(1000); - expect(verifiedConfig).toEqual({ - capacityInBytes: 3000, - defaultPriority: 5, - defaultTTL: 3000000, - itemMaxSize: 1000, - keyPrefix: 'aws-amplify#$#', - storage: expect.any(Storage), - warningThreshold: 0.8, - }); - }); - - test('give a error message if config has the keyPrefix', () => { - const spyon = jest.spyOn(Logger.prototype, 'warn'); - const storage: StorageCache = new StorageCache(config); - - const customizedConfig = { - keyPrefix: 'abcc', - } as Omit; - const new_config = storage.configure(customizedConfig); - - expect(spyon).toBeCalled(); + const instance = cache.createInstance(newConfig); + expect(instance).toBeInstanceOf(StorageCache); + expect(instance).not.toBe(cache); }); }); }); diff --git a/packages/core/__tests__/Cache/StorageCacheCommon.test.ts b/packages/core/__tests__/Cache/StorageCacheCommon.test.ts new file mode 100644 index 00000000000..dd63e141e81 --- /dev/null +++ b/packages/core/__tests__/Cache/StorageCacheCommon.test.ts @@ -0,0 +1,593 @@ +import { CacheConfig } from '../../src/Cache/types/Cache'; +import { defaultConfig } from '../../src/Cache/constants'; +import { StorageCacheCommon } from '../../src/Cache/StorageCacheCommon'; +import { KeyValueStorageInterface } from '../../src/types'; +import { ConsoleLogger } from '../../src/Logger'; +import { + getByteLength, + getCurrentSizeKey, + getCurrentTime, +} from '../../src/Cache/utils'; + +jest.mock('../../src/Cache/utils'); + +describe('StorageCacheCommon', () => { + const keyPrefix = 'key-prefix-'; + const currentSizeKey = `${keyPrefix}current-size-key`; + const key = 'key'; + const prefixedKey = `${keyPrefix}${key}`; + const currentTime = 1600412400000; + const config: CacheConfig = { + keyPrefix, + capacityInBytes: 3000, + itemMaxSize: 600, + defaultTTL: 3000000, + defaultPriority: 5, + warningThreshold: 0.8, + }; + // create spies + const loggerSpy = { + debug: jest.spyOn(ConsoleLogger.prototype, 'debug'), + error: jest.spyOn(ConsoleLogger.prototype, 'error'), + warn: jest.spyOn(ConsoleLogger.prototype, 'warn'), + }; + // create mocks + const mockGetByteLength = getByteLength as jest.Mock; + const mockGetCurrentSizeKey = getCurrentSizeKey as jest.Mock; + const mockGetCurrentTime = getCurrentTime as jest.Mock; + const mockKeyValueStorageSetItem = jest.fn(); + const mockKeyValueStorageGetItem = jest.fn(); + const mockKeyValueStorageRemoveItem = jest.fn(); + const mockKeyValueStorageClear = jest.fn(); + const mockGetAllCacheKeys = jest.fn(); + const mockKeyValueStorage: KeyValueStorageInterface = { + setItem: mockKeyValueStorageSetItem, + getItem: mockKeyValueStorageGetItem, + removeItem: mockKeyValueStorageRemoveItem, + clear: mockKeyValueStorageClear, + }; + // extend class for testing + class StorageCacheCommonTest extends StorageCacheCommon { + getAllCacheKeys() { + return mockGetAllCacheKeys(); + } + + testGetConfig() { + return this.config; + } + } + // create test helpers + const getStorageCache = (config?: CacheConfig) => + new StorageCacheCommonTest({ + config, + keyValueStorage: mockKeyValueStorage, + }); + + beforeAll(() => { + // suppress console log + for (const level in loggerSpy) { + loggerSpy[level].mockImplementation(jest.fn); + } + mockGetCurrentSizeKey.mockReturnValue(currentSizeKey); + }); + + beforeEach(() => { + mockGetCurrentTime.mockReturnValue(currentTime); + }); + + afterEach(() => { + // clear mocks + for (const level in loggerSpy) { + loggerSpy[level].mockClear(); + } + mockKeyValueStorageSetItem.mockClear(); + mockKeyValueStorageRemoveItem.mockClear(); + mockKeyValueStorageClear.mockClear(); + // reset mocks + mockGetByteLength.mockReset(); + mockKeyValueStorageGetItem.mockReset(); + mockGetAllCacheKeys.mockReset(); + }); + + describe('constructor', () => { + it('reverts to default itemMaxSize if it is set larger than capacityInBytes', () => { + const cache = getStorageCache({ + ...config, + itemMaxSize: config.capacityInBytes * 2, + }); + expect(cache.testGetConfig().itemMaxSize).toBe(defaultConfig.itemMaxSize); + expect(loggerSpy.error).toBeCalled(); + }); + + it('reverts to default defaultPriority if it is set below minimum range', () => { + const cache = getStorageCache({ + ...config, + defaultPriority: 0, + }); + expect(cache.testGetConfig().defaultPriority).toBe( + defaultConfig.defaultPriority + ); + expect(loggerSpy.error).toBeCalled(); + }); + + it('reverts to default defaultPriority if it is set above maximum range', () => { + const cache = getStorageCache({ + ...config, + defaultPriority: 6, + }); + expect(cache.testGetConfig().defaultPriority).toBe( + defaultConfig.defaultPriority + ); + expect(loggerSpy.error).toBeCalled(); + }); + + it('reverts to default warningThreshold if it is set below minimum range', () => { + const cache = getStorageCache({ + ...config, + warningThreshold: -1, + }); + expect(cache.testGetConfig().warningThreshold).toBe( + defaultConfig.warningThreshold + ); + expect(loggerSpy.error).toBeCalled(); + }); + + it('reverts to default warningThreshold if it is set above maximum range', () => { + const cache = getStorageCache({ + ...config, + warningThreshold: 2, + }); + expect(cache.testGetConfig().warningThreshold).toBe( + defaultConfig.warningThreshold + ); + expect(loggerSpy.error).toBeCalled(); + }); + + it('reverts to default capacityInBytes if it is set larger than 5MB limit', () => { + const cacheLimit = 5 * 1024 * 1024; // 5MB limit + const cache = getStorageCache({ + ...config, + capacityInBytes: cacheLimit + 1, + }); + expect(cache.testGetConfig().capacityInBytes).toBe( + defaultConfig.capacityInBytes + ); + expect(loggerSpy.error).toBeCalled(); + }); + }); + + describe('configure()', () => { + it('re-configures an instance', () => { + const cache = getStorageCache(config); + const updatedConfig = { + capacityInBytes: 4000, + itemMaxSize: 700, + defaultTTL: 4000000, + defaultPriority: 4, + warningThreshold: 0.7, + }; + + expect(cache.configure(updatedConfig)).toStrictEqual({ + keyPrefix: config.keyPrefix, + ...updatedConfig, + }); + }); + + it('logs a warning if re-configured with keyPrefix', () => { + const cache = getStorageCache(config); + cache.configure(config); + + expect(loggerSpy.warn).toBeCalled(); + }); + }); + + describe('getCurrentCacheSize()', () => { + const cache = getStorageCache(config); + + it('returns the current cache size', async () => { + mockKeyValueStorageGetItem.mockResolvedValue('10'); + expect(await cache.getCurrentCacheSize()).toBe(10); + expect(mockKeyValueStorageGetItem).toBeCalledWith(currentSizeKey); + }); + + it('returns zero if size set to zero', async () => { + mockKeyValueStorageGetItem.mockResolvedValue('0'); + expect(await cache.getCurrentCacheSize()).toBe(0); + expect(mockKeyValueStorageSetItem).not.toBeCalled(); + }); + + it('returns zero if size it not set', async () => { + mockKeyValueStorageGetItem.mockResolvedValue(null); + expect(await cache.getCurrentCacheSize()).toBe(0); + expect(mockKeyValueStorageSetItem).toBeCalledWith(currentSizeKey, '0'); + }); + }); + + describe('setItem()', () => { + const cache = getStorageCache(config); + + beforeEach(() => { + mockKeyValueStorageGetItem.mockReturnValue(null); + }); + + afterEach(() => { + mockKeyValueStorageGetItem.mockReset(); + }); + + it.each([ + ['string', 'x'.repeat(300)], + ['object', { abc: 123, edf: 456 }], + ['number', 1234], + ['boolean', true], + ])('sets an item if it does not exist (%s}', async (_, value: any) => { + await cache.setItem(key, value); + expect(loggerSpy.debug).toBeCalledWith( + expect.stringContaining(`Set item: key is ${key}`) + ); + expect(mockKeyValueStorageSetItem).toBeCalledWith( + prefixedKey, + expect.stringContaining(JSON.stringify(value)) + ); + }); + + it('aborts on empty key', async () => { + await cache.setItem('', 'abc'); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid key') + ); + expect(mockKeyValueStorageSetItem).not.toBeCalled(); + }); + + it('aborts on reserved key', async () => { + await cache.setItem('CurSize', 'abc'); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid key') + ); + expect(mockKeyValueStorageSetItem).not.toBeCalled(); + }); + + it('aborts on undefined value', async () => { + await cache.setItem(key, undefined); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('should not be undefined') + ); + expect(mockKeyValueStorageGetItem).not.toBeCalled(); + }); + + it('aborts if priority is below minimum', async () => { + await cache.setItem(key, 'abc', { priority: 0 }); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid parameter') + ); + expect(mockKeyValueStorageGetItem).not.toBeCalled(); + }); + + it('aborts if priority is above maximum', async () => { + await cache.setItem(key, 'abc', { priority: 6 }); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid parameter') + ); + expect(mockKeyValueStorageGetItem).not.toBeCalled(); + }); + + it('aborts if item size is above maximum', async () => { + mockGetByteLength.mockImplementation( + jest.requireActual('../../src/Cache/utils').getByteLength + ); + const value = 'x'.repeat(config.itemMaxSize * 2); + await cache.setItem(key, value); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('is too big') + ); + expect(mockKeyValueStorageGetItem).not.toBeCalled(); + }); + + it('updates existing cache content with the same key', async () => { + const value = 'value'; + const currentItem = { byteSize: 10 }; + mockGetByteLength.mockReturnValue(20); + mockKeyValueStorageGetItem + .mockReturnValueOnce(JSON.stringify(currentItem)) // check for item + .mockReturnValueOnce(JSON.stringify(currentItem)) // check before remove item + .mockReturnValueOnce(25) // get current cache size (decrease) + .mockReturnValueOnce(15) // get current cache size (full check) + .mockReturnValueOnce(15); // get current cache size (increase) + await cache.setItem(key, value); + + expect(mockKeyValueStorageGetItem).toBeCalledTimes(5); + expect(mockKeyValueStorageSetItem).toBeCalledWith(currentSizeKey, '15'); // 25 - 10 + expect(mockKeyValueStorageRemoveItem).toBeCalledTimes(1); + expect(mockKeyValueStorageSetItem).toBeCalledWith(currentSizeKey, '35'); // 15 + 20 + expect(mockKeyValueStorageSetItem).toBeCalledWith( + prefixedKey, + JSON.stringify({ + key: prefixedKey, + data: value, + timestamp: currentTime, + visitedTime: currentTime, + priority: 5, + expires: currentTime + config.defaultTTL, + type: 'string', + byteSize: 20, + }) + ); + }); + + it('tries to clear invalid keys when cache is full', async () => { + const value = 'value'; + const expiredItem = { expires: currentTime - 10, byteSize: 10 }; + const validItem = { expires: currentTime + 10, byteSize: 10 }; + mockGetByteLength.mockReturnValue(20); + mockKeyValueStorageGetItem + .mockReturnValueOnce(null) // check for item + .mockReturnValueOnce(3000) // get current cache size (full check) + .mockReturnValueOnce(JSON.stringify(expiredItem)) // first expired check + .mockReturnValueOnce(JSON.stringify(expiredItem)) // check before removing item + .mockReturnValueOnce(25) // get current cache size (decrease) + .mockReturnValueOnce(JSON.stringify(validItem)) // second expired check + .mockReturnValueOnce(2000); // get current cache size (second full check) + mockGetAllCacheKeys.mockReturnValue([ + `${keyPrefix}expired-key`, + `${keyPrefix}valid-key`, + ]); + await cache.setItem(key, value); + expect(mockKeyValueStorageRemoveItem).toBeCalledTimes(1); + expect(mockKeyValueStorageSetItem).toBeCalledWith( + prefixedKey, + expect.stringContaining(value) + ); + }); + + it('pops lower priority items when cache is full', async () => { + const value = 'value'; + const baseItem = { + expires: currentTime + 10, + byteSize: 400, + }; + const lowPriorityItem = { + ...baseItem, + key: 'low-priority-key', + priority: 4, + }; + const mediumPriorityItem = { + ...baseItem, + key: 'medium-priority-key', + priority: 3, + }; + const highPriorityItem = { + ...baseItem, + key: 'high-priority-key', + priority: 2, + }; + mockGetByteLength.mockReturnValue(500); + mockKeyValueStorageGetItem + .mockReturnValueOnce(null) // check for item + .mockReturnValueOnce(3000) // get current cache size (full check) + .mockReturnValueOnce(JSON.stringify(mediumPriorityItem)) // first expired check + .mockReturnValueOnce(JSON.stringify(highPriorityItem)) // second expired check + .mockReturnValueOnce(JSON.stringify(lowPriorityItem)) // third expired check + .mockReturnValueOnce(3000) // get current cache size (second full check) + .mockReturnValueOnce(3000) // get current cache size (checking space for pop) + .mockReturnValueOnce(JSON.stringify(mediumPriorityItem)) // first item check for pop + .mockReturnValueOnce(JSON.stringify(highPriorityItem)) // second item check for pop + .mockReturnValueOnce(JSON.stringify(lowPriorityItem)) // third item check for pop + .mockReturnValueOnce(JSON.stringify(lowPriorityItem)) // check before removing item + .mockReturnValueOnce(2900) // get current cache size (decrease) + .mockReturnValueOnce(JSON.stringify(mediumPriorityItem)) // check before removing item + .mockReturnValueOnce(2800); // get current cache size (decrease) + mockGetAllCacheKeys.mockReturnValue([ + mediumPriorityItem.key, + highPriorityItem.key, + lowPriorityItem.key, + ]); + await cache.setItem(key, value); + expect(mockKeyValueStorageRemoveItem).toBeCalledTimes(2); + expect(mockKeyValueStorageRemoveItem).toBeCalledWith(lowPriorityItem.key); + expect(mockKeyValueStorageRemoveItem).toBeCalledWith( + mediumPriorityItem.key + ); + expect(mockKeyValueStorageSetItem).toBeCalledWith( + prefixedKey, + expect.stringContaining(value) + ); + }); + + it('pops last visited items if priorities are the same when cache is full', async () => { + const value = 'value'; + const baseItem = { + expires: currentTime + 10, + byteSize: 400, + priority: 3, + }; + const lastVisitedItem = { + ...baseItem, + key: `${keyPrefix}last-visited-key`, + visitedTime: currentTime - 3000, + }; + const recentlyVistedItem = { + ...baseItem, + key: `${keyPrefix}recently-visited-key`, + visitedTime: currentTime - 2000, + }; + const mostRecentlyVisitedItem = { + ...baseItem, + key: `${keyPrefix}most-recently-visited-key`, + visitedTime: currentTime - 1000, + }; + mockGetByteLength.mockReturnValue(300); + mockKeyValueStorageGetItem + .mockReturnValueOnce(null) // check for item + .mockReturnValueOnce(3000) // get current cache size (full check) + .mockReturnValueOnce(JSON.stringify(recentlyVistedItem)) // first expired check + .mockReturnValueOnce(JSON.stringify(mostRecentlyVisitedItem)) // second expired check + .mockReturnValueOnce(JSON.stringify(lastVisitedItem)) // third expired check + .mockReturnValueOnce(3000) // get current cache size (second full check) + .mockReturnValueOnce(3000) // get current cache size (checking space for pop) + .mockReturnValueOnce(JSON.stringify(recentlyVistedItem)) // first item check for pop + .mockReturnValueOnce(JSON.stringify(mostRecentlyVisitedItem)) // second item check for pop + .mockReturnValueOnce(JSON.stringify(lastVisitedItem)) // third item check for pop + .mockReturnValueOnce(JSON.stringify(lastVisitedItem)) // check before removing item + .mockReturnValueOnce(2900) // get current cache size (decrease) + .mockReturnValueOnce(JSON.stringify(recentlyVistedItem)) // check before removing item + .mockReturnValueOnce(2800); // get current cache size (decrease) + mockGetAllCacheKeys.mockReturnValue([ + recentlyVistedItem.key, + mostRecentlyVisitedItem.key, + lastVisitedItem.key, + ]); + await cache.setItem(key, value); + expect(mockKeyValueStorageRemoveItem).toBeCalledTimes(2); + expect(mockKeyValueStorageRemoveItem).toBeCalledWith(lastVisitedItem.key); + expect(mockKeyValueStorageRemoveItem).toBeCalledWith( + recentlyVistedItem.key + ); + expect(mockKeyValueStorageSetItem).toBeCalledWith( + prefixedKey, + expect.stringContaining(value) + ); + }); + }); + + describe('getItem()', () => { + const value = 'value'; + const cache = getStorageCache(config); + const key = 'key'; + const prefixedKey = `${keyPrefix}${key}`; + + beforeEach(() => { + mockKeyValueStorageGetItem.mockReturnValue(null); + }); + + it('gets an item', async () => { + mockKeyValueStorageGetItem.mockReturnValue( + JSON.stringify({ data: value }) + ); + + expect(await cache.getItem(key)).toBe(value); + expect(loggerSpy.debug).toBeCalledWith( + expect.stringContaining(`Get item: key is ${key}`) + ); + expect(mockKeyValueStorageGetItem).toBeCalledWith(prefixedKey); + }); + + it('aborts on empty key', async () => { + expect(await cache.getItem('')).toBeNull(); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid key') + ); + expect(mockKeyValueStorageGetItem).not.toBeCalled(); + }); + + it('aborts on reserved key', async () => { + expect(await cache.getItem('CurSize')).toBeNull(); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid key') + ); + expect(mockKeyValueStorageGetItem).not.toBeCalled(); + }); + + it('clears item if it expires when trying to get it', async () => { + mockKeyValueStorageGetItem.mockReturnValue( + JSON.stringify({ + data: value, + byteSize: 10, + expires: currentTime - 10, + }) + ); + + expect(await cache.getItem(key)).toBeNull(); + expect(mockKeyValueStorageRemoveItem).toBeCalledWith(prefixedKey); + }); + + it('returns null if not in cache', async () => { + expect(await cache.getItem(key)).toBeNull(); + }); + + it('updates item visitedTime when fetched from cache', async () => { + const item = { data: value }; + mockKeyValueStorageGetItem.mockReturnValue(JSON.stringify(item)); + + expect(await cache.getItem(key)).toBe(value); + expect(mockKeyValueStorageGetItem).toBeCalledWith(prefixedKey); + expect(mockKeyValueStorageSetItem).toBeCalledWith( + prefixedKey, + JSON.stringify({ ...item, visitedTime: currentTime }) + ); + }); + + it('execute a callback if specified when key not found in cache', async () => { + mockGetByteLength.mockReturnValue(20); + const callback = jest.fn(() => value); + expect(await cache.getItem(key, { callback })).toBe(value); + expect(callback).toBeCalled(); + expect(mockKeyValueStorageSetItem).toBeCalled(); + }); + }); + + describe('removeItem()', () => { + const cache = getStorageCache(config); + const key = 'key'; + const prefixedKey = `${keyPrefix}${key}`; + + beforeEach(() => { + mockKeyValueStorageGetItem.mockReturnValue( + JSON.stringify({ byteSize: 10 }) + ); + }); + + it('removes an item', async () => { + await cache.removeItem(key); + expect(loggerSpy.debug).toBeCalledWith( + expect.stringContaining(`Remove item: key is ${key}`) + ); + expect(mockKeyValueStorageRemoveItem).toBeCalledWith(prefixedKey); + }); + + it('aborts on empty key', async () => { + await cache.removeItem(''); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid key') + ); + expect(mockKeyValueStorageRemoveItem).not.toBeCalled(); + }); + + it('aborts on reserved key', async () => { + await cache.removeItem('CurSize'); + expect(loggerSpy.warn).toBeCalledWith( + expect.stringContaining('Invalid key') + ); + expect(mockKeyValueStorageRemoveItem).not.toBeCalled(); + }); + + it('does nothing if item not found', async () => { + mockKeyValueStorageGetItem.mockReturnValue(null); + await cache.removeItem(key); + expect(mockKeyValueStorageRemoveItem).not.toBeCalled(); + }); + }); + + describe('clear()', () => { + const cache = getStorageCache(config); + + it('clears the cache, including the currentSizeKey', async () => { + mockGetAllCacheKeys.mockReturnValue([ + currentSizeKey, + `${keyPrefix}some-key`, + ]); + await cache.clear(); + expect(loggerSpy.debug).toBeCalledWith('Clear Cache'); + expect(mockKeyValueStorageRemoveItem).toBeCalledTimes(2); + }); + }); + + describe('getAllKeys()', () => { + const cache = getStorageCache(config); + + it('returns all cache keys', async () => { + const keys = ['current-size-key', 'key-1', 'key-2']; + mockGetAllCacheKeys.mockReturnValue(keys); + + expect(await cache.getAllKeys()).toStrictEqual(keys); + }); + }); +}); diff --git a/packages/core/__tests__/Cache/Utils/cacheList.test.ts b/packages/core/__tests__/Cache/utils/CacheList.test.ts similarity index 97% rename from packages/core/__tests__/Cache/Utils/cacheList.test.ts rename to packages/core/__tests__/Cache/utils/CacheList.test.ts index b4cd431e7d1..a8016a56c09 100644 --- a/packages/core/__tests__/Cache/Utils/cacheList.test.ts +++ b/packages/core/__tests__/Cache/utils/CacheList.test.ts @@ -1,6 +1,6 @@ -import CacheList from '../../../src/Cache/Utils/CacheList'; +import { CacheList } from '../../../src/Cache/utils'; -describe('cacheList', () => { +describe('CacheList', () => { describe('isEmpty', () => { test('return true if list is empty', () => { const list: CacheList = new CacheList(); diff --git a/packages/core/__tests__/Cache/Utils/cacheUtils.test.ts b/packages/core/__tests__/Cache/utils/cacheUtils.test.ts similarity index 67% rename from packages/core/__tests__/Cache/Utils/cacheUtils.test.ts rename to packages/core/__tests__/Cache/utils/cacheUtils.test.ts index cea9bd020f5..4c99645a352 100644 --- a/packages/core/__tests__/Cache/Utils/cacheUtils.test.ts +++ b/packages/core/__tests__/Cache/utils/cacheUtils.test.ts @@ -1,7 +1,7 @@ -import { getByteLength } from '../../../src/Cache/Utils/CacheUtils'; +import { getByteLength } from '../../../src/Cache/utils/cacheHelpers'; -describe('CacheUtils', () => { - describe('getByteLength test', () => { +describe('cacheHelpers', () => { + describe('getByteLength()', () => { test('happy case', () => { const str: string = 'abc'; expect(getByteLength(str)).toBe(3); diff --git a/packages/core/package.json b/packages/core/package.json index cbddaf77a2d..cadfab100f0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,10 +13,8 @@ "./lib/Credentials.js", "./lib-esm/I18n/index.js", "./lib-esm/Credentials.js", - "./lib/Cache/BrowserStorageCache.js", - "./lib/Cache/AsyncStorageCache.js", - "./lib-esm/Cache/BrowserStorageCache.js", - "./lib-esm/Cache/AsyncStorageCache.js" + "./lib/Cache/index.js", + "./lib-esm/Cache/index.js" ], "scripts": { "test": "npm run lint && jest -w 1 --coverage", @@ -36,8 +34,7 @@ "ts-coverage": "typescript-coverage-report -p ./tsconfig.json -t 92.36" }, "react-native": { - "./lib/index": "./lib-esm/index.js", - "./lib-esm/Cache": "./lib-esm/Cache/reactnative.js" + "./lib/index": "./lib-esm/index.js" }, "repository": { "type": "git", @@ -128,12 +125,6 @@ "path": "./lib-esm/index.js", "import": "{ Cache }", "limit": "4.13 kB" - }, - { - "name": "Cache (in-memory)", - "path": "./lib-esm/index.js", - "import": "{ InMemoryCache }", - "limit": "4.15 kB" } ], "jest": { diff --git a/packages/core/src/Cache/AsyncStorageCache.ts b/packages/core/src/Cache/AsyncStorageCache.ts deleted file mode 100644 index 0d3b3f9eb2d..00000000000 --- a/packages/core/src/Cache/AsyncStorageCache.ts +++ /dev/null @@ -1,495 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { loadAsyncStorage } from '@aws-amplify/react-native'; -import { ConsoleLogger as Logger } from '../Logger'; -import { StorageCache } from './StorageCache'; -import { defaultConfig, getCurrTime } from './Utils'; -import { CacheConfig, CacheItem, CacheItemOptions, ICache } from './types'; -import { getCurrSizeKey } from './Utils/CacheUtils'; -import { assert, CacheErrorCode } from './Utils/errorHelpers'; - -const logger = new Logger('AsyncStorageCache'); -const AsyncStorage = loadAsyncStorage(); - -/* - * Customized cache which based on the AsyncStorage with LRU implemented - */ -export class AsyncStorageCache extends StorageCache implements ICache { - /** - * initialize the cache - * - * @param {Object} config - the configuration of the cache - */ - constructor(config?: CacheConfig) { - super(config); - - this.getItem = this.getItem.bind(this); - this.setItem = this.setItem.bind(this); - this.removeItem = this.removeItem.bind(this); - - logger.debug('Using AsyncStorageCache'); - } - - /** - * decrease current size of the cache - * @private - * @param amount - the amount of the cache size which needs to be decreased - */ - async _decreaseCurSizeInBytes(amount: number) { - const curSize = await this.getCacheCurSize(); - await AsyncStorage.setItem( - getCurrSizeKey(this.cacheConfig.keyPrefix), - (curSize - amount).toString() - ); - } - - /** - * increase current size of the cache - * @private - * @param amount - the amount of the cache szie which need to be increased - */ - async _increaseCurSizeInBytes(amount: number) { - const curSize = await this.getCacheCurSize(); - await AsyncStorage.setItem( - getCurrSizeKey(this.cacheConfig.keyPrefix), - (curSize + amount).toString() - ); - } - - /** - * update the visited time if item has been visited - * @private - * @param item - the item which need to be refreshed - * @param prefixedKey - the key of the item - * - * @return the refreshed item - */ - async _refreshItem(item: CacheItem, prefixedKey: string) { - item.visitedTime = getCurrTime(); - await AsyncStorage.setItem(prefixedKey, JSON.stringify(item)); - return item; - } - - /** - * check wether item is expired - * @private - * @param key - the key of the item - * - * @return true if the item is expired. - */ - async _isExpired(key: string) { - const text = await AsyncStorage.getItem(key); - assert(text !== null, CacheErrorCode.NoCacheItem, `Key: ${key}`); - const item = JSON.parse(text); - if (getCurrTime() >= item.expires) { - return true; - } - return false; - } - - /** - * delete item from cache - * @private - * @param prefixedKey - the key of the item - * @param size - optional, the byte size of the item - */ - async _removeItem(prefixedKey: string, size?: number) { - const config = await AsyncStorage.getItem(prefixedKey); - assert(!!config, CacheErrorCode.NoCacheItem, `Key: ${prefixedKey}`); - const itemSize = size ?? JSON.parse(config).byteSize; - // first try to update the current size of the cache - await this._decreaseCurSizeInBytes(itemSize); - - // try to remove the item from cache - try { - await AsyncStorage.removeItem(prefixedKey); - } catch (removeItemError) { - // if some error happened, we need to rollback the current size - await this._increaseCurSizeInBytes(itemSize); - logger.error(`Failed to remove item: ${removeItemError}`); - } - } - - /** - * put item into cache - * @private - * @param prefixedKey - the key of the item - * @param itemData - the value of the item - * @param itemSizeInBytes - the byte size of the item - */ - async _setItem(prefixedKey: string, item: any) { - // first try to update the current size of the cache. - await this._increaseCurSizeInBytes(item.byteSize); - - // try to add the item into cache - try { - await AsyncStorage.setItem(prefixedKey, JSON.stringify(item)); - } catch (setItemErr) { - // if some error happened, we need to rollback the current size - await this._decreaseCurSizeInBytes(item.byteSize); - logger.error(`Failed to set item ${setItemErr}`); - } - } - - /** - * total space needed when poping out items - * @private - * @param itemSize - * - * @return total space needed - */ - async _sizeToPop(itemSize: number) { - const spaceItemNeed = - (await this.getCacheCurSize()) + - itemSize - - this.cacheConfig.capacityInBytes; - const cacheThresholdSpace = - (1 - this.cacheConfig.warningThreshold) * - this.cacheConfig.capacityInBytes; - return spaceItemNeed > cacheThresholdSpace - ? spaceItemNeed - : cacheThresholdSpace; - } - - /** - * see whether cache is full - * @private - * @param itemSize - * - * @return true if cache is full - */ - async _isCacheFull(itemSize: number) { - return ( - itemSize + (await this.getCacheCurSize()) > - this.cacheConfig.capacityInBytes - ); - } - - /** - * scan the storage and find out all the keys owned by this cache - * also clean the expired keys while scanning - * @private - * @return array of keys - */ - async _findValidKeys() { - const keys: string[] = []; - let keyInCache: Readonly = []; - - keyInCache = await AsyncStorage.getAllKeys(); - - for (let i = 0; i < keyInCache.length; i += 1) { - const key = keyInCache[i]; - if ( - key.indexOf(this.cacheConfig.keyPrefix) === 0 && - key !== getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - if (await this._isExpired(key)) { - await this._removeItem(key); - } else { - keys.push(key); - } - } - } - return keys; - } - - /** - * get all the items we have, sort them by their priority, - * if priority is same, sort them by their last visited time - * pop out items from the low priority (5 is the lowest) - * @private - * @param keys - all the keys in this cache - * @param sizeToPop - the total size of the items which needed to be poped out - */ - async _popOutItems(keys: string[], sizeToPop: number) { - const items: any[] = []; - let remainedSize = sizeToPop; - for (let i = 0; i < keys.length; i += 1) { - const val = await AsyncStorage.getItem(keys[i]); - if (val != null) { - const item = JSON.parse(val); - items.push(item); - } - } - - // first compare priority - // then compare visited time - items.sort((a, b) => { - if (a.priority > b.priority) { - return -1; - } else if (a.priority < b.priority) { - return 1; - } else { - if (a.visitedTime < b.visitedTime) { - return -1; - } else return 1; - } - }); - - for (let i = 0; i < items.length; i += 1) { - // pop out items until we have enough room for new item - await this._removeItem(items[i].key, items[i].byteSize); - remainedSize -= items[i].byteSize; - if (remainedSize <= 0) { - return; - } - } - } - - /** - * Set item into cache. You can put number, string, boolean or object. - * The cache will first check whether has the same key. - * If it has, it will delete the old item and then put the new item in - * The cache will pop out items if it is full - * You can specify the cache item options. The cache will abort and output a warning: - * If the key is invalid - * If the size of the item exceeds itemMaxSize. - * If the value is undefined - * If incorrect cache item configuration - * If error happened with browser storage - * - * @param {String} key - the key of the item - * @param {Object} value - the value of the item - * @param {Object} [options] - optional, the specified meta-data - * @return {Promise} - */ - async setItem(key: string, value: any, options: Record) { - logger.debug( - `Set item: key is ${key}, value is ${value} with options: ${options}` - ); - const prefixedKey = this.cacheConfig.keyPrefix + key; - // invalid keys - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - logger.warn(`Invalid key: should not be empty or 'CurSize'`); - return; - } - - if (typeof value === 'undefined') { - logger.warn(`The value of item should not be undefined!`); - return; - } - - const cacheItemOptions = { - priority: - options && options.priority !== undefined - ? options.priority - : this.cacheConfig.defaultPriority, - expires: - options && options.expires !== undefined - ? options.expires - : this.cacheConfig.defaultTTL + getCurrTime(), - }; - - if (cacheItemOptions.priority < 1 || cacheItemOptions.priority > 5) { - logger.warn( - `Invalid parameter: priority due to out or range. It should be within 1 and 5.` - ); - return; - } - - const item = this.fillCacheItem(prefixedKey, value, cacheItemOptions); - - // check wether this item is too big; - if (item.byteSize > this.cacheConfig.itemMaxSize) { - logger.warn( - `Item with key: ${key} you are trying to put into is too big!` - ); - return; - } - - try { - // first look into the storage, if it exists, delete it. - const val = await AsyncStorage.getItem(prefixedKey); - if (val) { - await this._removeItem(prefixedKey, JSON.parse(val).byteSize); - } - - // check whether the cache is full - if (await this._isCacheFull(item.byteSize)) { - const validKeys = await this._findValidKeys(); - if (await this._isCacheFull(item.byteSize)) { - const sizeToPop = await this._sizeToPop(item.byteSize); - await this._popOutItems(validKeys, sizeToPop); - } - } - - // put item in the cache - await this._setItem(prefixedKey, item); - } catch (e) { - logger.warn(`setItem failed! ${e}`); - } - } - - /** - * Get item from cache. It will return null if item doesn’t exist or it has been expired. - * If you specified callback function in the options, - * then the function will be executed if no such item in the cache - * and finally put the return value into cache. - * Please make sure the callback function will return the value you want to put into the cache. - * The cache will abort output a warning: - * If the key is invalid - * If error happened with AsyncStorage - * - * @param {String} key - the key of the item - * @param {Object} [options] - the options of callback function - * @return {Promise} - return a promise resolves to be the value of the item - */ - async getItem(key: string, options: CacheItemOptions) { - logger.debug(`Get item: key is ${key} with options ${options}`); - let ret = null; - const prefixedKey = this.cacheConfig.keyPrefix + key; - - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - logger.warn(`Invalid key: should not be empty or 'CurSize'`); - return null; - } - - try { - ret = await AsyncStorage.getItem(prefixedKey); - if (ret != null) { - if (await this._isExpired(prefixedKey)) { - // if expired, remove that item and return null - await this._removeItem(prefixedKey, JSON.parse(ret).byteSize); - } else { - // if not expired, great, return the value and refresh it - let item = JSON.parse(ret); - item = await this._refreshItem(item, prefixedKey); - return item.data; - } - } - - if (options && options.callback !== undefined) { - const val = options.callback(); - if (val !== null) { - this.setItem(key, val, options); - } - return val; - } - return null; - } catch (e) { - logger.warn(`getItem failed! ${e}`); - return null; - } - } - - /** - * remove item from the cache - * The cache will abort output a warning: - * If error happened with AsyncStorage - * @param {String} key - the key of the item - * @return {Promise} - */ - async removeItem(key: string) { - logger.debug(`Remove item: key is ${key}`); - const prefixedKey = this.cacheConfig.keyPrefix + key; - - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - return; - } - - try { - const val = await AsyncStorage.getItem(prefixedKey); - if (val) { - await this._removeItem(prefixedKey, JSON.parse(val).byteSize); - } - } catch (e) { - logger.warn(`removeItem failed! ${e}`); - } - } - - /** - * clear the entire cache - * The cache will abort output a warning: - * If error happened with AsyncStorage - * @return {Promise} - */ - async clear() { - logger.debug(`Clear Cache`); - try { - const keys = await AsyncStorage.getAllKeys(); - - const keysToRemove: string[] = []; - for (let i = 0; i < keys.length; i += 1) { - if (keys[i].indexOf(this.cacheConfig.keyPrefix) === 0) { - keysToRemove.push(keys[i]); - } - } - - // can be improved - for (let i = 0; i < keysToRemove.length; i += 1) { - await AsyncStorage.removeItem(keysToRemove[i]); - } - } catch (e) { - logger.warn(`clear failed! ${e}`); - } - } - - /** - * return the current size of the cache - * @return {Promise} - */ - async getCacheCurSize() { - let ret = await AsyncStorage.getItem( - getCurrSizeKey(this.cacheConfig.keyPrefix) - ); - if (!ret) { - await AsyncStorage.setItem( - getCurrSizeKey(this.cacheConfig.keyPrefix), - '0' - ); - ret = '0'; - } - return Number(ret); - } - - /** - * Return all the keys in the cache. - * Will return an empty array if error happend. - * @return {Promise} - */ - async getAllKeys() { - try { - const keys = await AsyncStorage.getAllKeys(); - - const retKeys: string[] = []; - for (let i = 0; i < keys.length; i += 1) { - if ( - keys[i].indexOf(this.cacheConfig.keyPrefix) === 0 && - keys[i] !== getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - retKeys.push(keys[i].substring(this.cacheConfig.keyPrefix.length)); - } - } - return retKeys; - } catch (e) { - logger.warn(`getALlkeys failed! ${e}`); - return []; - } - } - - /** - * Return a new instance of cache with customized configuration. - * @param {Object} config - the customized configuration - * @return {Object} - the new instance of Cache - */ - createInstance(config: CacheConfig): ICache { - if (config.keyPrefix === defaultConfig.keyPrefix) { - logger.error('invalid keyPrefix, setting keyPrefix with timeStamp'); - config.keyPrefix = getCurrTime.toString(); - } - return new AsyncStorageCache(config); - } -} - -const instance: ICache = new AsyncStorageCache(); -export { AsyncStorage, instance as Cache }; diff --git a/packages/core/src/Cache/BrowserStorageCache.ts b/packages/core/src/Cache/BrowserStorageCache.ts deleted file mode 100644 index 373bf44856f..00000000000 --- a/packages/core/src/Cache/BrowserStorageCache.ts +++ /dev/null @@ -1,504 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { ConsoleLogger as Logger } from '../Logger'; -import { defaultConfig, getCurrTime } from './Utils'; -import { StorageCache } from './StorageCache'; -import { ICache, CacheConfig, CacheItem, CacheItemOptions } from './types'; -import { getCurrSizeKey } from './Utils/CacheUtils'; -import { assert, CacheErrorCode } from './Utils/errorHelpers'; - -const logger = new Logger('Cache'); - -/** - * Customized storage based on the SessionStorage or LocalStorage with LRU implemented - */ -export class BrowserStorageCacheClass extends StorageCache implements ICache { - /** - * initialize the cache - * @param config - the configuration of the cache - */ - constructor(config?: CacheConfig) { - super(config); - - assert(!!this.cacheConfig.storage, CacheErrorCode.NoCacheStorage); - this.cacheConfig.storage = this.cacheConfig.storage; - this.getItem = this.getItem.bind(this); - this.setItem = this.setItem.bind(this); - this.removeItem = this.removeItem.bind(this); - } - private getStorage(): Storage { - assert(!!this.cacheConfig.storage, CacheErrorCode.NoCacheStorage); - return this.cacheConfig.storage; - } - /** - * decrease current size of the cache - * - * @private - * @param amount - the amount of the cache size which needs to be decreased - */ - private _decreaseCurSizeInBytes(amount: number): void { - const curSize: number = this.getCacheCurSize(); - this.getStorage().setItem( - getCurrSizeKey(this.cacheConfig.keyPrefix), - (curSize - amount).toString() - ); - } - - /** - * increase current size of the cache - * - * @private - * @param amount - the amount of the cache szie which need to be increased - */ - private _increaseCurSizeInBytes(amount: number): void { - const curSize: number = this.getCacheCurSize(); - this.getStorage().setItem( - getCurrSizeKey(this.cacheConfig.keyPrefix), - (curSize + amount).toString() - ); - } - - /** - * update the visited time if item has been visited - * - * @private - * @param item - the item which need to be refreshed - * @param prefixedKey - the key of the item - * - * @return the refreshed item - */ - private _refreshItem(item: CacheItem, prefixedKey: string): CacheItem { - item.visitedTime = getCurrTime(); - this.getStorage().setItem(prefixedKey, JSON.stringify(item)); - return item; - } - - /** - * check wether item is expired - * - * @private - * @param key - the key of the item - * - * @return true if the item is expired. - */ - private _isExpired(key: string): boolean { - const text: string | null = this.getStorage().getItem(key); - assert(text !== null, CacheErrorCode.NoCacheItem, `Key: ${key}`); - const item: CacheItem = JSON.parse(text); - if (getCurrTime() >= item.expires) { - return true; - } - return false; - } - - /** - * delete item from cache - * - * @private - * @param prefixedKey - the key of the item - * @param size - optional, the byte size of the item - */ - private _removeItem(prefixedKey: string, size?: number): void { - const item = this.getStorage().getItem(prefixedKey); - assert(item !== null, CacheErrorCode.NoCacheItem, `Key: ${prefixedKey}`); - const itemSize: number = size ?? JSON.parse(item).byteSize; - this._decreaseCurSizeInBytes(itemSize); - // remove the cache item - this.getStorage().removeItem(prefixedKey); - } - - /** - * put item into cache - * - * @private - * @param prefixedKey - the key of the item - * @param itemData - the value of the item - * @param itemSizeInBytes - the byte size of the item - */ - private _setItem(prefixedKey: string, item: CacheItem): void { - // update the cache size - this._increaseCurSizeInBytes(item.byteSize); - - try { - this.getStorage().setItem(prefixedKey, JSON.stringify(item)); - } catch (setItemErr) { - // if failed, we need to rollback the cache size - this._decreaseCurSizeInBytes(item.byteSize); - logger.error(`Failed to set item ${setItemErr}`); - } - } - - /** - * total space needed when poping out items - * - * @private - * @param itemSize - * - * @return total space needed - */ - private _sizeToPop(itemSize: number): number { - const spaceItemNeed = - this.getCacheCurSize() + itemSize - this.cacheConfig.capacityInBytes; - const cacheThresholdSpace = - (1 - this.cacheConfig.warningThreshold) * - this.cacheConfig.capacityInBytes; - return spaceItemNeed > cacheThresholdSpace - ? spaceItemNeed - : cacheThresholdSpace; - } - - /** - * see whether cache is full - * - * @private - * @param itemSize - * - * @return true if cache is full - */ - private _isCacheFull(itemSize: number): boolean { - return itemSize + this.getCacheCurSize() > this.cacheConfig.capacityInBytes; - } - - /** - * scan the storage and find out all the keys owned by this cache - * also clean the expired keys while scanning - * - * @private - * - * @return array of keys - */ - private _findValidKeys(): string[] { - const keys: string[] = []; - const keyInCache: string[] = []; - // get all keys in Storage - for (let i = 0; i < this.getStorage().length; i += 1) { - const key = this.getStorage().key(i); - if (key) { - keyInCache.push(key); - } - } - - // find those items which belong to our cache and also clean those expired items - for (let i = 0; i < keyInCache.length; i += 1) { - const key: string = keyInCache[i]; - if ( - key.indexOf(this.cacheConfig.keyPrefix) === 0 && - key !== getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - if (this._isExpired(key)) { - this._removeItem(key); - } else { - keys.push(key); - } - } - } - return keys; - } - - /** - * get all the items we have, sort them by their priority, - * if priority is same, sort them by their last visited time - * pop out items from the low priority (5 is the lowest) - * - * @private - * @param keys - all the keys in this cache - * @param sizeToPop - the total size of the items which needed to be poped out - */ - private _popOutItems(keys: string[], sizeToPop: number): void { - const items: CacheItem[] = []; - let remainedSize: number = sizeToPop; - // get the items from Storage - for (let i = 0; i < keys.length; i += 1) { - const val: string | null = this.getStorage().getItem(keys[i]); - if (val != null) { - const item: CacheItem = JSON.parse(val); - items.push(item); - } - } - - // first compare priority - // then compare visited time - items.sort((a, b) => { - if (a.priority > b.priority) { - return -1; - } else if (a.priority < b.priority) { - return 1; - } else { - if (a.visitedTime < b.visitedTime) { - return -1; - } else return 1; - } - }); - - for (let i = 0; i < items.length; i += 1) { - // pop out items until we have enough room for new item - this._removeItem(items[i].key, items[i].byteSize); - remainedSize -= items[i].byteSize; - if (remainedSize <= 0) { - return; - } - } - } - - /** - * Set item into cache. You can put number, string, boolean or object. - * The cache will first check whether has the same key. - * If it has, it will delete the old item and then put the new item in - * The cache will pop out items if it is full - * You can specify the cache item options. The cache will abort and output a warning: - * If the key is invalid - * If the size of the item exceeds itemMaxSize. - * If the value is undefined - * If incorrect cache item configuration - * If error happened with browser storage - * - * @param key - the key of the item - * @param value - the value of the item - * @param {Object} [options] - optional, the specified meta-data - */ - public setItem( - key: string, - value: object | number | string | boolean, - options?: CacheItemOptions - ): void { - logger.log( - `Set item: key is ${key}, value is ${value} with options: ${options}` - ); - const prefixedKey: string = this.cacheConfig.keyPrefix + key; - // invalid keys - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - logger.warn(`Invalid key: should not be empty or 'CurSize'`); - return; - } - - if (typeof value === 'undefined') { - logger.warn(`The value of item should not be undefined!`); - return; - } - - const cacheItemOptions = { - priority: - options && options.priority !== undefined - ? options.priority - : this.cacheConfig.defaultPriority, - expires: - options && options.expires !== undefined - ? options.expires - : this.cacheConfig.defaultTTL + getCurrTime(), - }; - - if (cacheItemOptions.priority < 1 || cacheItemOptions.priority > 5) { - logger.warn( - `Invalid parameter: priority due to out or range. It should be within 1 and 5.` - ); - return; - } - - const item: CacheItem = this.fillCacheItem( - prefixedKey, - value, - cacheItemOptions - ); - - // check wether this item is too big; - if (item.byteSize > this.cacheConfig.itemMaxSize) { - logger.warn( - `Item with key: ${key} you are trying to put into is too big!` - ); - return; - } - - try { - // first look into the storage, if it exists, delete it. - const val: string | null = this.getStorage().getItem(prefixedKey); - if (val) { - this._removeItem(prefixedKey, JSON.parse(val).byteSize); - } - - // check whether the cache is full - if (this._isCacheFull(item.byteSize)) { - const validKeys: string[] = this._findValidKeys(); - // check again and then pop out items - if (this._isCacheFull(item.byteSize)) { - const sizeToPop: number = this._sizeToPop(item.byteSize); - this._popOutItems(validKeys, sizeToPop); - } - } - - // put item in the cache - // may failed due to storage full - this._setItem(prefixedKey, item); - } catch (e) { - logger.warn(`setItem failed! ${e}`); - } - } - - /** - * Get item from cache. It will return null if item doesn’t exist or it has been expired. - * If you specified callback function in the options, - * then the function will be executed if no such item in the cache - * and finally put the return value into cache. - * Please make sure the callback function will return the value you want to put into the cache. - * The cache will abort output a warning: - * If the key is invalid - * If error happened with browser storage - * - * @param key - the key of the item - * @param {Object} [options] - the options of callback function - * - * @return - return the value of the item - */ - public getItem(key: string, options?: CacheItemOptions): any { - logger.log(`Get item: key is ${key} with options ${options}`); - let ret: string | null = null; - const prefixedKey: string = this.cacheConfig.keyPrefix + key; - - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - logger.warn(`Invalid key: should not be empty or 'CurSize'`); - return null; - } - - try { - ret = this.getStorage().getItem(prefixedKey); - if (ret != null) { - if (this._isExpired(prefixedKey)) { - // if expired, remove that item and return null - this._removeItem(prefixedKey, JSON.parse(ret).byteSize); - ret = null; - } else { - // if not expired, great, return the value and refresh it - let item: CacheItem = JSON.parse(ret); - item = this._refreshItem(item, prefixedKey); - return item.data; - } - } - - if (options && options.callback !== undefined) { - const val: object | string | number | boolean = options.callback(); - if (val !== null) { - this.setItem(key, val, options); - } - return val; - } - return null; - } catch (e) { - logger.warn(`getItem failed! ${e}`); - return null; - } - } - - /** - * remove item from the cache - * The cache will abort output a warning: - * If error happened with browser storage - * @param key - the key of the item - */ - public removeItem(key: string): void { - logger.log(`Remove item: key is ${key}`); - const prefixedKey: string = this.cacheConfig.keyPrefix + key; - - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - return; - } - - try { - const val: string | null = this.getStorage().getItem(prefixedKey); - if (val) { - this._removeItem(prefixedKey, JSON.parse(val).byteSize); - } - } catch (e) { - logger.warn(`removeItem failed! ${e}`); - } - } - - /** - * clear the entire cache - * The cache will abort output a warning: - * If error happened with browser storage - */ - public clear(): void { - logger.log(`Clear Cache`); - const keysToRemove: string[] = []; - - for (let i = 0; i < this.getStorage().length; i += 1) { - const key = this.getStorage().key(i); - if (key?.indexOf(this.cacheConfig.keyPrefix) === 0) { - keysToRemove.push(key); - } - } - - try { - for (let i = 0; i < keysToRemove.length; i += 1) { - this.getStorage().removeItem(keysToRemove[i]); - } - } catch (e) { - logger.warn(`clear failed! ${e}`); - } - } - - /** - * Return all the keys in the cache. - * - * @return - all keys in the cache - */ - public getAllKeys(): string[] { - const keys: string[] = []; - for (let i = 0; i < this.getStorage().length; i += 1) { - const key = this.getStorage().key(i); - if ( - key && - key.indexOf(this.cacheConfig.keyPrefix) === 0 && - key !== getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - keys.push(key.substring(this.cacheConfig.keyPrefix.length)); - } - } - return keys; - } - - /** - * return the current size of the cache - * - * @return - current size of the cache - */ - public getCacheCurSize(): number { - let ret: string | null = this.getStorage().getItem( - getCurrSizeKey(this.cacheConfig.keyPrefix) - ); - if (!ret) { - this.getStorage().setItem( - getCurrSizeKey(this.cacheConfig.keyPrefix), - '0' - ); - ret = '0'; - } - return Number(ret); - } - - /** - * Return a new instance of cache with customized configuration. - * @param config - the customized configuration - * - * @return - new instance of Cache - */ - public createInstance(config: CacheConfig): ICache { - if (!config.keyPrefix || config.keyPrefix === defaultConfig.keyPrefix) { - logger.error('invalid keyPrefix, setting keyPrefix with timeStamp'); - config.keyPrefix = getCurrTime.toString(); - } - - return new BrowserStorageCacheClass(config); - } -} - -export const BrowserStorageCache: ICache = new BrowserStorageCacheClass(); diff --git a/packages/core/src/Cache/CHANGELOG.md b/packages/core/src/Cache/CHANGELOG.md deleted file mode 100644 index 68804641bea..00000000000 --- a/packages/core/src/Cache/CHANGELOG.md +++ /dev/null @@ -1,1239 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## 5.1.10 (2023-08-23) - -**Note:** Version bump only for package @aws-amplify/cache - -## 5.1.9 (2023-08-22) - -**Note:** Version bump only for package @aws-amplify/cache - -## 5.1.8 (2023-08-17) - -**Note:** Version bump only for package @aws-amplify/cache - -## 5.1.7 (2023-08-10) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.1.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.1.5...@aws-amplify/cache@5.1.6) (2023-07-31) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.1.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.1.4...@aws-amplify/cache@5.1.5) (2023-07-20) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.1.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.1.3...@aws-amplify/cache@5.1.4) (2023-07-13) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.1.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.1.2...@aws-amplify/cache@5.1.3) (2023-06-28) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.1.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.1.1...@aws-amplify/cache@5.1.2) (2023-06-21) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.1.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.1.0...@aws-amplify/cache@5.1.1) (2023-06-20) - -**Note:** Version bump only for package @aws-amplify/cache - -# [5.1.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.33...@aws-amplify/cache@5.1.0) (2023-06-05) - -### Features - -- **clients:** support CN partition by adding DNS suffix resolver ([#11311](https://github.com/aws-amplify/amplify-js/issues/11311)) ([9de2975](https://github.com/aws-amplify/amplify-js/commit/9de297519fdbaaf1e9b4ae98f12aed4137400222)) - -## [5.0.33](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.32...@aws-amplify/cache@5.0.33) (2023-05-27) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.32](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.31...@aws-amplify/cache@5.0.32) (2023-05-12) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.31](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.30...@aws-amplify/cache@5.0.31) (2023-05-04) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.30](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.29...@aws-amplify/cache@5.0.30) (2023-04-27) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.29](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.28...@aws-amplify/cache@5.0.29) (2023-04-20) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.28](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.27...@aws-amplify/cache@5.0.28) (2023-04-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.27](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.26...@aws-amplify/cache@5.0.27) (2023-04-13) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.26](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.25...@aws-amplify/cache@5.0.26) (2023-04-12) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.25](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.24...@aws-amplify/cache@5.0.25) (2023-04-06) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.24](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.23...@aws-amplify/cache@5.0.24) (2023-04-04) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.23](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.22...@aws-amplify/cache@5.0.23) (2023-03-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.22](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.21...@aws-amplify/cache@5.0.22) (2023-03-23) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.21](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.20...@aws-amplify/cache@5.0.21) (2023-03-21) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.20](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.19...@aws-amplify/cache@5.0.20) (2023-03-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.19](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.18...@aws-amplify/cache@5.0.19) (2023-03-13) - -### Bug Fixes - -- Run ts coverage check with test ([#11047](https://github.com/aws-amplify/amplify-js/issues/11047)) ([430bedf](https://github.com/aws-amplify/amplify-js/commit/430bedfd0d0618bd0093b488233521356feef787)) - -## [5.0.18](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.17...@aws-amplify/cache@5.0.18) (2023-03-08) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.17](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.16...@aws-amplify/cache@5.0.17) (2023-03-06) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.16](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.15...@aws-amplify/cache@5.0.16) (2023-02-24) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.15](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.14...@aws-amplify/cache@5.0.15) (2023-02-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.14](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.13...@aws-amplify/cache@5.0.14) (2023-02-09) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.13](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.12...@aws-amplify/cache@5.0.13) (2023-02-08) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.12](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.11...@aws-amplify/cache@5.0.12) (2023-01-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.11](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.10...@aws-amplify/cache@5.0.11) (2023-01-19) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.9...@aws-amplify/cache@5.0.10) (2023-01-13) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.8...@aws-amplify/cache@5.0.9) (2023-01-10) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.7...@aws-amplify/cache@5.0.8) (2022-12-27) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.6...@aws-amplify/cache@5.0.7) (2022-12-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.5...@aws-amplify/cache@5.0.6) (2022-12-15) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.4...@aws-amplify/cache@5.0.5) (2022-12-06) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.3...@aws-amplify/cache@5.0.4) (2022-11-23) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.2...@aws-amplify/cache@5.0.3) (2022-11-19) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.1...@aws-amplify/cache@5.0.2) (2022-11-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [5.0.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@5.0.0...@aws-amplify/cache@5.0.1) (2022-11-11) - -**Note:** Version bump only for package @aws-amplify/cache - -# [5.0.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.63...@aws-amplify/cache@5.0.0) (2022-11-09) - -### Bug Fixes - -- Standardize `cache` named export to preserve interoperability with RN ([#10546](https://github.com/aws-amplify/amplify-js/issues/10546)) ([20b096b](https://github.com/aws-amplify/amplify-js/commit/20b096b1a34e6a102d08dabcedb38772f3a6caf7)) - -### Features - -- Setup tslib & importHelpers to improve bundle size ([#10435](https://github.com/aws-amplify/amplify-js/pull/10435)) -- Remove (most) default exports ([10461](https://github.com/aws-amplify/amplify-js/pull/10461)) -- Expand \* exports to optimize tree-shaking ([#10555](https://github.com/aws-amplify/amplify-js/pull/10555)) -- Move cache sideEffects to align with other packages ([#10562](https://github.com/aws-amplify/amplify-js/pull/10562)) -- add a typescript coverage report mechanism ([#10551](https://github.com/aws-amplify/amplify-js/issues/10551)) ([8e8df55](https://github.com/aws-amplify/amplify-js/commit/8e8df55b449f8bae2fe962fe282613d1b818cc5a)), closes [#10379](https://github.com/aws-amplify/amplify-js/issues/10379) - -## [4.0.62](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.61...@aws-amplify/cache@4.0.62) (2022-10-27) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.61](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.60...@aws-amplify/cache@4.0.61) (2022-10-26) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.60](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.59...@aws-amplify/cache@4.0.60) (2022-10-25) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.59](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.58...@aws-amplify/cache@4.0.59) (2022-10-14) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.58](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.57...@aws-amplify/cache@4.0.58) (2022-10-14) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.57](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.55...@aws-amplify/cache@4.0.57) (2022-09-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.56](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.55...@aws-amplify/cache@4.0.56) (2022-09-20) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.55](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.54...@aws-amplify/cache@4.0.55) (2022-09-08) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.54](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.53...@aws-amplify/cache@4.0.54) (2022-09-01) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.53](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.52...@aws-amplify/cache@4.0.53) (2022-08-23) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.52](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.51...@aws-amplify/cache@4.0.52) (2022-08-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.50...@aws-amplify/cache@4.0.51) (2022-08-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.49...@aws-amplify/cache@4.0.50) (2022-08-01) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.48...@aws-amplify/cache@4.0.49) (2022-07-28) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.47...@aws-amplify/cache@4.0.48) (2022-07-21) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.46...@aws-amplify/cache@4.0.47) (2022-07-07) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.45...@aws-amplify/cache@4.0.46) (2022-06-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.44...@aws-amplify/cache@4.0.45) (2022-06-15) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.43...@aws-amplify/cache@4.0.44) (2022-05-24) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.42...@aws-amplify/cache@4.0.43) (2022-05-23) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.41...@aws-amplify/cache@4.0.42) (2022-05-12) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.40...@aws-amplify/cache@4.0.41) (2022-05-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.39...@aws-amplify/cache@4.0.40) (2022-04-14) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.38...@aws-amplify/cache@4.0.39) (2022-04-04) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.37...@aws-amplify/cache@4.0.38) (2022-03-28) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.37](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.36...@aws-amplify/cache@4.0.37) (2022-03-22) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.36](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.35...@aws-amplify/cache@4.0.36) (2022-03-10) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.35](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.34...@aws-amplify/cache@4.0.35) (2022-02-28) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.34](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.33...@aws-amplify/cache@4.0.34) (2022-02-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.33](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.32...@aws-amplify/cache@4.0.33) (2022-01-27) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.32](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.31...@aws-amplify/cache@4.0.32) (2022-01-07) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.31](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.30...@aws-amplify/cache@4.0.31) (2021-12-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.30](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.29...@aws-amplify/cache@4.0.30) (2021-12-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.29](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.28...@aws-amplify/cache@4.0.29) (2021-12-02) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.28](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.27...@aws-amplify/cache@4.0.28) (2021-11-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.27](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.26...@aws-amplify/cache@4.0.27) (2021-11-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.26](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.25...@aws-amplify/cache@4.0.26) (2021-11-12) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.25](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.24...@aws-amplify/cache@4.0.25) (2021-11-09) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.24](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.23...@aws-amplify/cache@4.0.24) (2021-10-28) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.23](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.22...@aws-amplify/cache@4.0.23) (2021-10-21) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.22](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.21...@aws-amplify/cache@4.0.22) (2021-10-07) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.21](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.20...@aws-amplify/cache@4.0.21) (2021-09-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.20](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.19...@aws-amplify/cache@4.0.20) (2021-09-24) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.19](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.18...@aws-amplify/cache@4.0.19) (2021-09-22) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.18](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.17...@aws-amplify/cache@4.0.18) (2021-09-17) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.17](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.16...@aws-amplify/cache@4.0.17) (2021-09-09) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.16](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.15...@aws-amplify/cache@4.0.16) (2021-09-07) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.15](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.14...@aws-amplify/cache@4.0.15) (2021-09-04) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.14](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.13...@aws-amplify/cache@4.0.14) (2021-09-02) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.13](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.12...@aws-amplify/cache@4.0.13) (2021-08-26) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.12](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.11...@aws-amplify/cache@4.0.12) (2021-08-19) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.11](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.10...@aws-amplify/cache@4.0.11) (2021-08-12) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.9...@aws-amplify/cache@4.0.10) (2021-07-28) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.8...@aws-amplify/cache@4.0.9) (2021-07-22) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.7...@aws-amplify/cache@4.0.8) (2021-07-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.6...@aws-amplify/cache@4.0.7) (2021-07-08) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.5...@aws-amplify/cache@4.0.6) (2021-06-24) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.4...@aws-amplify/cache@4.0.5) (2021-06-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.3...@aws-amplify/cache@4.0.4) (2021-06-10) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.1...@aws-amplify/cache@4.0.3) (2021-05-26) - -**Note:** Version bump only for package @aws-amplify/cache - -## [4.0.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@4.0.0...@aws-amplify/cache@4.0.1) (2021-05-14) - -**Note:** Version bump only for package @aws-amplify/cache - -# [4.0.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.56...@aws-amplify/cache@4.0.0) (2021-05-11) - -- chore!: Upgrade to @react-native-async-storage/async-storage (#8250) ([1de4853](https://github.com/aws-amplify/amplify-js/commit/1de48531b68e3c53c3b7dbf4487da4578cb79888)), closes [#8250](https://github.com/aws-amplify/amplify-js/issues/8250) - -### BREAKING CHANGES - -- Upgrade from React Native AsyncStorage to @react-native-async-storage/async-storage - -Co-authored-by: Ashish Nanda -Co-authored-by: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> - -## [3.1.56](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.55...@aws-amplify/cache@3.1.56) (2021-05-06) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.55](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.54...@aws-amplify/cache@3.1.55) (2021-04-15) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.54](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.53...@aws-amplify/cache@3.1.54) (2021-03-25) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.53](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.52...@aws-amplify/cache@3.1.53) (2021-03-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.52](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.51...@aws-amplify/cache@3.1.52) (2021-03-12) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.51](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.50...@aws-amplify/cache@3.1.51) (2021-03-08) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.50](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.49...@aws-amplify/cache@3.1.50) (2021-03-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.49](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.48...@aws-amplify/cache@3.1.49) (2021-02-25) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.48](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.47...@aws-amplify/cache@3.1.48) (2021-02-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.47](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.46...@aws-amplify/cache@3.1.47) (2021-02-15) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.46](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.45...@aws-amplify/cache@3.1.46) (2021-02-09) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.45](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.44...@aws-amplify/cache@3.1.45) (2021-02-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.44](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.43...@aws-amplify/cache@3.1.44) (2021-02-01) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.43](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.42...@aws-amplify/cache@3.1.43) (2021-01-29) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.42](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.41...@aws-amplify/cache@3.1.42) (2021-01-07) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.41](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.40...@aws-amplify/cache@3.1.41) (2020-12-17) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.40](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.39...@aws-amplify/cache@3.1.40) (2020-12-10) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.39](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.38...@aws-amplify/cache@3.1.39) (2020-11-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.38](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.37...@aws-amplify/cache@3.1.38) (2020-11-23) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.37](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.36...@aws-amplify/cache@3.1.37) (2020-11-20) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.36](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.35...@aws-amplify/cache@3.1.36) (2020-11-13) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.35](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.34...@aws-amplify/cache@3.1.35) (2020-11-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.34](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.33...@aws-amplify/cache@3.1.34) (2020-10-31) - -### Bug Fixes - -- **amazon-cognito-identity-js:** update random implementation ([#7090](https://github.com/aws-amplify/amplify-js/issues/7090)) ([7048453](https://github.com/aws-amplify/amplify-js/commit/70484532da8a9953384b00b223b2b3ba0c0e845e)) - -## [3.1.33](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.32...@aws-amplify/cache@3.1.33) (2020-10-29) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.32](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.31...@aws-amplify/cache@3.1.32) (2020-10-15) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.31](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.30...@aws-amplify/cache@3.1.31) (2020-10-01) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.30](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.29...@aws-amplify/cache@3.1.30) (2020-09-25) - -### Bug Fixes - -- Add files with Amplify.register to sideEffects array ([#6867](https://github.com/aws-amplify/amplify-js/issues/6867)) ([58ddbf8](https://github.com/aws-amplify/amplify-js/commit/58ddbf8811e44695d97b6ab8be8f7cd2a2242921)) - -## [3.1.29](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.28...@aws-amplify/cache@3.1.29) (2020-09-16) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.28](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.27...@aws-amplify/cache@3.1.28) (2020-09-15) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.27](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.26...@aws-amplify/cache@3.1.27) (2020-09-10) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.26](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.25...@aws-amplify/cache@3.1.26) (2020-09-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.25](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.24...@aws-amplify/cache@3.1.25) (2020-09-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.24](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.23...@aws-amplify/cache@3.1.24) (2020-09-01) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.23](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.22...@aws-amplify/cache@3.1.23) (2020-08-19) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.22](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.21...@aws-amplify/cache@3.1.22) (2020-08-06) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.21](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.20...@aws-amplify/cache@3.1.21) (2020-07-27) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.20](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.19...@aws-amplify/cache@3.1.20) (2020-07-22) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.19](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.18...@aws-amplify/cache@3.1.19) (2020-07-09) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.18](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.17...@aws-amplify/cache@3.1.18) (2020-07-07) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.17](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.16...@aws-amplify/cache@3.1.17) (2020-06-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.16](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.15...@aws-amplify/cache@3.1.16) (2020-06-09) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.15](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.14...@aws-amplify/cache@3.1.15) (2020-06-04) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.14](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.13...@aws-amplify/cache@3.1.14) (2020-06-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.13](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.12...@aws-amplify/cache@3.1.13) (2020-06-02) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.12](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.11...@aws-amplify/cache@3.1.12) (2020-05-26) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.11](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.10...@aws-amplify/cache@3.1.11) (2020-05-22) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.10](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.9...@aws-amplify/cache@3.1.10) (2020-05-14) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.9](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.8...@aws-amplify/cache@3.1.9) (2020-04-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.7...@aws-amplify/cache@3.1.8) (2020-04-24) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.6...@aws-amplify/cache@3.1.7) (2020-04-14) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.5...@aws-amplify/cache@3.1.6) (2020-04-08) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.4...@aws-amplify/cache@3.1.5) (2020-04-07) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.3...@aws-amplify/cache@3.1.4) (2020-04-03) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.2...@aws-amplify/cache@3.1.3) (2020-04-02) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.1...@aws-amplify/cache@3.1.2) (2020-04-01) - -**Note:** Version bump only for package @aws-amplify/cache - -## [3.1.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@3.1.0...@aws-amplify/cache@3.1.1) (2020-04-01) - -**Note:** Version bump only for package @aws-amplify/cache - -# [3.1.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.8...@aws-amplify/cache@3.1.0) (2020-03-31) - -### Bug Fixes - -- **@aws-amplify/cache:** expose tree-shaking for Webpack ([32061ac](https://github.com/aws-amplify/amplify-js/commit/32061ac8cdd16f0b0a675912b29e0dbfc44513fb)) - -### Features - -- **@aws-amplify/cache:** publish ES2015/ESM artifacts ([22da40e](https://github.com/aws-amplify/amplify-js/commit/22da40e4a72827bce51059b34fa45e5ea3f2367c)) - -### Reverts - -- Revert "Publish" ([1319d31](https://github.com/aws-amplify/amplify-js/commit/1319d319b69717e76660fbfa6f1a845195c6d635)) - -## [2.1.8](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.7...@aws-amplify/cache@2.1.8) (2020-03-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [2.1.7](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.6...@aws-amplify/cache@2.1.7) (2020-03-25) - -**Note:** Version bump only for package @aws-amplify/cache - -## [2.1.6](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.5...@aws-amplify/cache@2.1.6) (2020-02-28) - -**Note:** Version bump only for package @aws-amplify/cache - -## [2.1.5](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.3...@aws-amplify/cache@2.1.5) (2020-02-07) - -### Bug Fixes - -- **cache:** export correct module for RN ([#4786](https://github.com/aws-amplify/amplify-js/issues/4786)) ([a15730c](https://github.com/aws-amplify/amplify-js/commit/a15730cc50692d9d31a0f586c3544b3dcdbea659)) - -## [2.1.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.2...@aws-amplify/cache@2.1.3) (2020-01-10) - -### Bug Fixes - -- [#4311](https://github.com/aws-amplify/amplify-js/issues/4311) Update main entry field to point to CJS builds instead of webpack bundles ([#4678](https://github.com/aws-amplify/amplify-js/issues/4678)) ([54fbdf4](https://github.com/aws-amplify/amplify-js/commit/54fbdf4b1393567735fb7b5f4144db273f1a5f6a)) - -## [2.1.2](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.1...@aws-amplify/cache@2.1.2) (2019-12-18) - -**Note:** Version bump only for package @aws-amplify/cache - -## [2.1.1](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@2.1.0...@aws-amplify/cache@2.1.1) (2019-12-03) - -**Note:** Version bump only for package @aws-amplify/cache - -# [2.1.0](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@1.1.4...@aws-amplify/cache@2.1.0) (2019-11-15) - -### Features - -- enable watch mode for builds ([#4358](https://github.com/aws-amplify/amplify-js/issues/4358)) ([055e530](https://github.com/aws-amplify/amplify-js/commit/055e5308efc308ae6beee78f8963bb2f812e1f85)) - -## [1.1.4](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@1.1.3...@aws-amplify/cache@1.1.4) (2019-10-29) - -**Note:** Version bump only for package @aws-amplify/cache - -## [1.1.3](https://github.com/aws-amplify/amplify-js/compare/@aws-amplify/cache@1.1.2...@aws-amplify/cache@1.1.3) (2019-10-23) - -**Note:** Version bump only for package @aws-amplify/cache - -## [1.1.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.1.0...@aws-amplify/cache@1.1.2) (2019-10-10) - -**Note:** Version bump only for package @aws-amplify/cache - -# [1.1.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.34...@aws-amplify/cache@1.1.0) (2019-10-10) - -### Features - -- Added Prettier formatting ([4dfd9aa](https://github.com/aws/aws-amplify/commit/4dfd9aa9ab900307c9d17c68448a6ca4aa08fd5a)) - -## [1.0.34](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.33...@aws-amplify/cache@1.0.34) (2019-09-05) - -**Note:** Version bump only for package @aws-amplify/cache - -## [1.0.33](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.32...@aws-amplify/cache@1.0.33) (2019-09-04) - -**Note:** Version bump only for package @aws-amplify/cache - -## [1.0.32](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.31...@aws-amplify/cache@1.0.32) (2019-08-05) - -**Note:** Version bump only for package @aws-amplify/cache - -## [1.0.31](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.30...@aws-amplify/cache@1.0.31) (2019-07-31) - -**Note:** Version bump only for package @aws-amplify/cache - -## [1.0.30](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.29...@aws-amplify/cache@1.0.30) (2019-07-30) - -**Note:** Version bump only for package @aws-amplify/cache - -## [1.0.29](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.28...@aws-amplify/cache@1.0.29) (2019-07-18) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.28](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.28-unstable.2...@aws-amplify/cache@1.0.28) (2019-06-17) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.28-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.28-unstable.1...@aws-amplify/cache@1.0.28-unstable.2) (2019-06-14) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.28-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.27...@aws-amplify/cache@1.0.28-unstable.1) (2019-05-24) - -### Bug Fixes - -- **aws-amplify:** manual version bumps for lerna issue ([9ce5a72](https://github.com/aws/aws-amplify/commit/9ce5a72)) - - - -## [1.0.27](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.27-unstable.0...@aws-amplify/cache@1.0.27) (2019-05-14) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.27-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.26...@aws-amplify/cache@1.0.27-unstable.0) (2019-05-13) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.26](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.26-unstable.2...@aws-amplify/cache@1.0.26) (2019-05-06) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.26-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.26-unstable.1...@aws-amplify/cache@1.0.26-unstable.2) (2019-05-06) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.26-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.26-unstable.0...@aws-amplify/cache@1.0.26-unstable.1) (2019-04-17) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.26-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.25...@aws-amplify/cache@1.0.26-unstable.0) (2019-04-12) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.25](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.25-unstable.1...@aws-amplify/cache@1.0.25) (2019-04-04) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.25-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.25-unstable.0...@aws-amplify/cache@1.0.25-unstable.1) (2019-04-04) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.25-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.24...@aws-amplify/cache@1.0.25-unstable.0) (2019-04-02) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.24](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.24-unstable.1...@aws-amplify/cache@1.0.24) (2019-03-28) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.24-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.24-unstable.0...@aws-amplify/cache@1.0.24-unstable.1) (2019-03-28) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.24-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.23...@aws-amplify/cache@1.0.24-unstable.0) (2019-03-22) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.23](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.23-unstable.3...@aws-amplify/cache@1.0.23) (2019-03-04) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.23-unstable.3](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.23-unstable.2...@aws-amplify/cache@1.0.23-unstable.3) (2019-03-04) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.23-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.23-unstable.1...@aws-amplify/cache@1.0.23-unstable.2) (2019-02-27) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.23-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.23-unstable.0...@aws-amplify/cache@1.0.23-unstable.1) (2019-02-27) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.23-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.22...@aws-amplify/cache@1.0.23-unstable.0) (2019-01-10) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.22](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.22-unstable.0...@aws-amplify/cache@1.0.22) (2019-01-10) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.22-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.21...@aws-amplify/cache@1.0.22-unstable.0) (2018-12-26) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.21](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.21-unstable.0...@aws-amplify/cache@1.0.21) (2018-12-26) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.21-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.20...@aws-amplify/cache@1.0.21-unstable.0) (2018-12-22) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.20](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.20-unstable.0...@aws-amplify/cache@1.0.20) (2018-12-13) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.20-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.19...@aws-amplify/cache@1.0.20-unstable.0) (2018-12-07) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.19](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.19-unstable.4...@aws-amplify/cache@1.0.19) (2018-12-03) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.19-unstable.4](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.19-unstable.3...@aws-amplify/cache@1.0.19-unstable.4) (2018-11-27) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.19-unstable.3](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.19-unstable.2...@aws-amplify/cache@1.0.19-unstable.3) (2018-11-26) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.19-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.19-unstable.1...@aws-amplify/cache@1.0.19-unstable.2) (2018-11-20) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.19-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.19-unstable.0...@aws-amplify/cache@1.0.19-unstable.1) (2018-11-19) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.19-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.18...@aws-amplify/cache@1.0.19-unstable.0) (2018-11-15) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.18](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.18-unstable.0...@aws-amplify/cache@1.0.18) (2018-11-12) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.18-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.17...@aws-amplify/cache@1.0.18-unstable.0) (2018-11-06) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.17](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.17-unstable.0...@aws-amplify/cache@1.0.17) (2018-11-01) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.17-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.16...@aws-amplify/cache@1.0.17-unstable.0) (2018-10-30) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.16](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.16-unstable.3...@aws-amplify/cache@1.0.16) (2018-10-17) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.16-unstable.3](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.16-unstable.2...@aws-amplify/cache@1.0.16-unstable.3) (2018-10-16) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.16-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.16-unstable.1...@aws-amplify/cache@1.0.16-unstable.2) (2018-10-08) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.16-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.16-unstable.0...@aws-amplify/cache@1.0.16-unstable.1) (2018-10-05) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.16-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.15-unstable.1...@aws-amplify/cache@1.0.16-unstable.0) (2018-10-05) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.15](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.15-unstable.1...@aws-amplify/cache@1.0.15) (2018-10-04) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.15-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.15-unstable.0...@aws-amplify/cache@1.0.15-unstable.1) (2018-10-03) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.15-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.14-unstable.1...@aws-amplify/cache@1.0.15-unstable.0) (2018-10-03) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.14](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.14-unstable.1...@aws-amplify/cache@1.0.14) (2018-10-03) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.14-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.14-unstable.0...@aws-amplify/cache@1.0.14-unstable.1) (2018-10-01) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.14-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.13...@aws-amplify/cache@1.0.14-unstable.0) (2018-09-28) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.13](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.13-unstable.1...@aws-amplify/cache@1.0.13) (2018-09-27) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.13-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.13-unstable.0...@aws-amplify/cache@1.0.13-unstable.1) (2018-09-25) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.13-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.12...@aws-amplify/cache@1.0.13-unstable.0) (2018-09-22) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.12](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.12-unstable.0...@aws-amplify/cache@1.0.12) (2018-09-21) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.12-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.10...@aws-amplify/cache@1.0.12-unstable.0) (2018-09-21) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.11](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.10...@aws-amplify/cache@1.0.11) (2018-09-21) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.10](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.9...@aws-amplify/cache@1.0.10) (2018-09-17) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.9](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.8...@aws-amplify/cache@1.0.9) (2018-09-12) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.8](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.8-unstable.3...@aws-amplify/cache@1.0.8) (2018-09-09) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.8-unstable.3](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.7...@aws-amplify/cache@1.0.8-unstable.3) (2018-08-31) - -### Bug Fixes - -- **@aws-amplify/cache:** check if window object exists for browser usage ([988e553](https://github.com/aws/aws-amplify/commit/988e553)) - - - -## [1.0.8-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.7...@aws-amplify/cache@1.0.8-unstable.2) (2018-08-30) - -### Bug Fixes - -- **@aws-amplify/cache:** check if window object exists for browser usage ([988e553](https://github.com/aws/aws-amplify/commit/988e553)) - - - -## [1.0.8-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.7...@aws-amplify/cache@1.0.8-unstable.1) (2018-08-30) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.7](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.6-unstable.2...@aws-amplify/cache@1.0.7) (2018-08-28) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.6-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.6-unstable.1...@aws-amplify/cache@1.0.6-unstable.2) (2018-08-21) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.6-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.6-unstable.0...@aws-amplify/cache@1.0.6-unstable.1) (2018-08-20) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.6-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.5...@aws-amplify/cache@1.0.6-unstable.0) (2018-08-19) - -### Bug Fixes - -- **aws-amplify-angular:** Angular rollup ([#1441](https://github.com/aws/aws-amplify/issues/1441)) ([eb84e01](https://github.com/aws/aws-amplify/commit/eb84e01)) - - - -## [1.0.5](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.5-unstable.0...@aws-amplify/cache@1.0.5) (2018-08-14) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.5-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.4...@aws-amplify/cache@1.0.5-unstable.0) (2018-08-09) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.4](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.3-unstable.1...@aws-amplify/cache@1.0.4) (2018-08-06) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.3-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.3...@aws-amplify/cache@1.0.3-unstable.1) (2018-08-06) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.3](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.3-unstable.0...@aws-amplify/cache@1.0.3) (2018-07-28) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.3-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.2...@aws-amplify/cache@1.0.3-unstable.0) (2018-07-26) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.2-unstable.0...@aws-amplify/cache@1.0.2) (2018-07-19) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.2-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.1...@aws-amplify/cache@1.0.2-unstable.0) (2018-07-19) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.1-unstable.2...@aws-amplify/cache@1.0.1) (2018-07-18) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.1-unstable.2](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.1-unstable.1...@aws-amplify/cache@1.0.1-unstable.2) (2018-07-18) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.1-unstable.1](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.1...@aws-amplify/cache@1.0.1-unstable.1) (2018-07-18) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## [1.0.1-unstable.0](https://github.com/aws/aws-amplify/compare/@aws-amplify/cache@1.0.1...@aws-amplify/cache@1.0.1-unstable.0) (2018-07-18) - -**Note:** Version bump only for package @aws-amplify/cache - - - -## 0.1.1-unstable.0 (2018-06-27) - -**Note:** Version bump only for package @aws-amplify/cache diff --git a/packages/core/src/Cache/InMemoryCache.ts b/packages/core/src/Cache/InMemoryCache.ts deleted file mode 100644 index 59ed6e9cd69..00000000000 --- a/packages/core/src/Cache/InMemoryCache.ts +++ /dev/null @@ -1,347 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { CacheList, getCurrTime, CacheObject } from './Utils'; - -import { StorageCache } from './StorageCache'; -import { ICache, CacheConfig, CacheItem, CacheItemOptions } from './types'; -import { ConsoleLogger as Logger } from '../Logger'; -import { getCurrSizeKey } from './Utils/CacheUtils'; -import { assert, CacheErrorCode } from './Utils/errorHelpers'; - -const logger = new Logger('InMemoryCache'); - -/** - * Customized in-memory cache with LRU implemented - * @member cacheObj - object which store items - * @member cacheList - list of keys in the cache with LRU - * @member curSizeInBytes - current size of the cache - * @member maxPriority - max of the priority - */ -export class InMemoryCacheClass extends StorageCache implements ICache { - private cacheList: CacheList[]; - private curSizeInBytes: number; - private maxPriority: number; - - /** - * initialize the cache - * - * @param config - the configuration of the cache - */ - constructor(config?: CacheConfig) { - super(config); - - this.cacheList = []; - this.curSizeInBytes = 0; - this.maxPriority = 5; - - this.getItem = this.getItem.bind(this); - this.setItem = this.setItem.bind(this); - this.removeItem = this.removeItem.bind(this); - - // initialize list for every priority - for (let i = 0; i < this.maxPriority; i += 1) { - this.cacheList[i] = new CacheList(); - } - } - - /** - * decrease current size of the cache - * - * @param amount - the amount of the cache size which needs to be decreased - */ - private _decreaseCurSizeInBytes(amount: number): void { - this.curSizeInBytes -= amount; - } - - /** - * increase current size of the cache - * - * @param amount - the amount of the cache szie which need to be increased - */ - private _increaseCurSizeInBytes(amount: number): void { - this.curSizeInBytes += amount; - } - - /** - * check whether item is expired - * - * @param key - the key of the item - * - * @return true if the item is expired. - */ - private _isExpired(key: string): boolean { - const text: string | null = CacheObject.getItem(key); - - assert(text !== null, CacheErrorCode.NoCacheItem, `Key: ${key}`); - const item: CacheItem = JSON.parse(text); - if (getCurrTime() >= item.expires) { - return true; - } - return false; - } - - /** - * delete item from cache - * - * @param prefixedKey - the key of the item - * @param listIdx - indicates which cache list the key belongs to - */ - private _removeItem(prefixedKey: string, listIdx: number): void { - // delete the key from the list - this.cacheList[listIdx].removeItem(prefixedKey); - // decrease the current size of the cache - const item = CacheObject.getItem(prefixedKey); - assert(item !== null, CacheErrorCode.NoCacheItem, `Key: ${prefixedKey}`); - this._decreaseCurSizeInBytes(JSON.parse(item).byteSize); - // finally remove the item from memory - CacheObject.removeItem(prefixedKey); - } - - /** - * put item into cache - * - * @param prefixedKey - the key of the item - * @param itemData - the value of the item - * @param itemSizeInBytes - the byte size of the item - * @param listIdx - indicates which cache list the key belongs to - */ - private _setItem( - prefixedKey: string, - item: CacheItem, - listIdx: number - ): void { - // insert the key into the list - this.cacheList[listIdx].insertItem(prefixedKey); - // increase the current size of the cache - this._increaseCurSizeInBytes(item.byteSize); - // finally add the item into memory - CacheObject.setItem(prefixedKey, JSON.stringify(item)); - } - - /** - * see whether cache is full - * - * @param itemSize - * - * @return true if cache is full - */ - private _isCacheFull(itemSize: number): boolean { - return this.curSizeInBytes + itemSize > this.cacheConfig.capacityInBytes; - } - - /** - * check whether the cache contains the key - * - * @param key - */ - private containsKey(key: string): number { - const prefixedKey: string = this.cacheConfig.keyPrefix + key; - for (let i = 0; i < this.maxPriority; i += 1) { - if (this.cacheList[i].containsKey(prefixedKey)) { - return i + 1; - } - } - return -1; - } - - /** - * * Set item into cache. You can put number, string, boolean or object. - * The cache will first check whether has the same key. - * If it has, it will delete the old item and then put the new item in - * The cache will pop out items if it is full - * You can specify the cache item options. The cache will abort and output a warning: - * If the key is invalid - * If the size of the item exceeds itemMaxSize. - * If the value is undefined - * If incorrect cache item configuration - * If error happened with browser storage - * - * @param key - the key of the item - * @param value - the value of the item - * @param options - optional, the specified meta-data - * - * @throws if the item is too big which exceeds the limit of single item size - * @throws if the key is invalid - */ - public setItem( - key: string, - value: object | string | number | boolean, - options?: CacheItemOptions - ): void { - const prefixedKey: string = this.cacheConfig.keyPrefix + key; - // invalid keys - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - logger.warn(`Invalid key: should not be empty or 'CurSize'`); - return; - } - - if (typeof value === 'undefined') { - logger.warn(`The value of item should not be undefined!`); - return; - } - - const cacheItemOptions = { - priority: - options && options.priority !== undefined - ? options.priority - : this.cacheConfig.defaultPriority, - expires: - options && options.expires !== undefined - ? options.expires - : this.cacheConfig.defaultTTL + getCurrTime(), - }; - - if (cacheItemOptions.priority < 1 || cacheItemOptions.priority > 5) { - logger.warn( - `Invalid parameter: priority due to out or range. It should be within 1 and 5.` - ); - return; - } - - const item: CacheItem = this.fillCacheItem( - prefixedKey, - value, - cacheItemOptions - ); - - // check wether this item is too big; - if (item.byteSize > this.cacheConfig.itemMaxSize) { - logger.warn( - `Item with key: ${key} you are trying to put into is too big!` - ); - return; - } - - // if key already in the cache, then delete it. - const presentKeyPrio: number = this.containsKey(key); - if (presentKeyPrio !== -1) { - this._removeItem(prefixedKey, presentKeyPrio - 1); - } - - // pop out items in the cache when cache is full based on LRU - // first start from lowest priority cache list - let cacheListIdx = this.maxPriority - 1; - while (this._isCacheFull(item.byteSize) && cacheListIdx >= 0) { - if (!this.cacheList[cacheListIdx].isEmpty()) { - const popedItemKey = this.cacheList[cacheListIdx].getLastItem(); - this._removeItem(popedItemKey, cacheListIdx); - } else { - cacheListIdx -= 1; - } - } - - this._setItem(prefixedKey, item, Number(item.priority) - 1); - } - - /** - * Get item from cache. It will return null if item doesn’t exist or it has been expired. - * If you specified callback function in the options, - * then the function will be executed if no such item in the cache - * and finally put the return value into cache. - * Please make sure the callback function will return the value you want to put into the cache. - * The cache will abort output a warning: - * If the key is invalid - * - * @param key - the key of the item - * @param options - the options of callback function - */ - public getItem(key: string, options?: CacheItemOptions): any { - let ret: string | null = null; - const prefixedKey: string = this.cacheConfig.keyPrefix + key; - - if ( - prefixedKey === this.cacheConfig.keyPrefix || - prefixedKey === getCurrSizeKey(this.cacheConfig.keyPrefix) - ) { - logger.warn(`Invalid key: should not be empty or 'CurSize'`); - return null; - } - - // check whether it's in the cachelist - const presentKeyPrio: number = this.containsKey(key); - if (presentKeyPrio !== -1) { - if (this._isExpired(prefixedKey)) { - // if expired, remove that item and return null - this._removeItem(prefixedKey, presentKeyPrio - 1); - } else { - // if not expired, great, return the value and refresh it - ret = CacheObject.getItem(prefixedKey) ?? ''; - const item: CacheItem = JSON.parse(ret); - this.cacheList[item.priority - 1].refresh(prefixedKey); - return item.data; - } - } - - if (options && options.callback !== undefined) { - const val: object | string | number | boolean = options.callback(); - if (val !== null) { - this.setItem(key, val, options); - } - return val; - } - return null; - } - - /** - * remove item from the cache - * - * @param key - the key of the item - */ - public removeItem(key: string): void { - const prefixedKey: string = this.cacheConfig.keyPrefix + key; - - // check if the key is in the cache - const presentKeyPrio: number = this.containsKey(key); - if (presentKeyPrio !== -1) { - this._removeItem(prefixedKey, presentKeyPrio - 1); - } - } - - /** - * clear the entire cache - */ - public clear(): void { - for (let i = 0; i < this.maxPriority; i += 1) { - for (const key of this.cacheList[i].getKeys()) { - this._removeItem(key, i); - } - } - } - - /** - * Return all the keys in the cache. - */ - public getAllKeys(): string[] { - const keys: string[] = []; - for (let i = 0; i < this.maxPriority; i += 1) { - for (const key of this.cacheList[i].getKeys()) { - keys.push(key.substring(this.cacheConfig.keyPrefix.length)); - } - } - - return keys; - } - - /** - * return the current size of the cache - * - * @return the current size of the cache - */ - public getCacheCurSize(): number { - return this.curSizeInBytes; - } - - /** - * Return a new instance of cache with customized configuration. - * @param config - the customized configuration - */ - public createInstance(config: CacheConfig): ICache { - return new InMemoryCacheClass(config); - } -} - -export const InMemoryCache: ICache = new InMemoryCacheClass(); diff --git a/packages/core/src/Cache/StorageCache.native.ts b/packages/core/src/Cache/StorageCache.native.ts new file mode 100644 index 00000000000..c8d5e57bd50 --- /dev/null +++ b/packages/core/src/Cache/StorageCache.native.ts @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { loadAsyncStorage } from '@aws-amplify/react-native'; +import { ConsoleLogger as Logger } from '../Logger'; +import { defaultConfig } from './constants'; +import { StorageCacheCommon } from './StorageCacheCommon'; +import { Cache, CacheConfig } from './types'; +import { getCurrentSizeKey, getCurrentTime } from './utils'; + +const logger = new Logger('StorageCache'); +const AsyncStorage = loadAsyncStorage(); + +/* + * Customized cache which based on the AsyncStorage with LRU implemented + */ +export class StorageCache extends StorageCacheCommon implements Cache { + /** + * initialize the cache + * @param config - the configuration of the cache + */ + constructor(config?: CacheConfig) { + super({ config, keyValueStorage: AsyncStorage }); + + this.getItem = this.getItem.bind(this); + this.setItem = this.setItem.bind(this); + this.removeItem = this.removeItem.bind(this); + } + + protected async getAllCacheKeys(options?: { omitSizeKey?: boolean }) { + const { omitSizeKey } = options ?? {}; + const keys: string[] = []; + for (const key of await AsyncStorage.getAllKeys()) { + if (omitSizeKey && key === getCurrentSizeKey(this.config.keyPrefix)) { + continue; + } + if (key?.startsWith(this.config.keyPrefix)) { + keys.push(key.substring(this.config.keyPrefix.length)); + } + } + return keys; + } + + protected async getAllStorageKeys() { + try { + return AsyncStorage.getAllKeys(); + } catch (e) { + logger.warn(`getAllKeys failed! ${e}`); + return []; + } + } + + /** + * Return a new instance of cache with customized configuration. + * @param {Object} config - the customized configuration + * @return {Object} - the new instance of Cache + */ + public createInstance(config: CacheConfig): Cache { + if (!config.keyPrefix || config.keyPrefix === defaultConfig.keyPrefix) { + logger.error('invalid keyPrefix, setting keyPrefix with timeStamp'); + config.keyPrefix = getCurrentTime.toString(); + } + return new StorageCache(config); + } +} diff --git a/packages/core/src/Cache/StorageCache.ts b/packages/core/src/Cache/StorageCache.ts index dc6ec91627b..406ca7bc76b 100644 --- a/packages/core/src/Cache/StorageCache.ts +++ b/packages/core/src/Cache/StorageCache.ts @@ -1,158 +1,61 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { getCurrTime, getByteLength, defaultConfig } from './Utils'; -import { Amplify } from '../singleton'; -import { CacheConfig, CacheItem, CacheItemOptions } from './types'; -import { ConsoleLogger as Logger } from '../Logger'; - -const logger = new Logger('StorageCache'); - -/** - * Initialization of the cache - * - */ -export class StorageCache { - // Contains any fields that have been customized for this Cache instance (i.e. without default values) - private instanceConfig?: CacheConfig; - - /** - * Initialize the cache - * - * @param config - Custom configuration for this instance. - */ - constructor(config?: CacheConfig) { - if (config) { - // A configuration was specified for this specific instance - this.instanceConfig = config; - } - - this.sanitizeConfig(); - } - - public getModuleName() { - return 'Cache'; - } - - private sanitizeConfig(): void { - const tempInstanceConfig = this.instanceConfig || ({} as CacheConfig); - - if (this.cacheConfig.itemMaxSize > this.cacheConfig.capacityInBytes) { - logger.error( - 'Invalid parameter: itemMaxSize. It should be smaller than capacityInBytes. Setting back to default.' - ); - tempInstanceConfig.itemMaxSize = defaultConfig.itemMaxSize; - } - - if ( - this.cacheConfig.defaultPriority > 5 || - this.cacheConfig.defaultPriority < 1 - ) { - logger.error( - 'Invalid parameter: defaultPriority. It should be between 1 and 5. Setting back to default.' - ); - tempInstanceConfig.defaultPriority = defaultConfig.defaultPriority; - } - - if ( - Number(this.cacheConfig.warningThreshold) > 1 || - Number(this.cacheConfig.warningThreshold) < 0 - ) { - logger.error( - 'Invalid parameter: warningThreshold. It should be between 0 and 1. Setting back to default.' - ); - tempInstanceConfig.warningThreshold = defaultConfig.warningThreshold; - } - - // Set 5MB limit - const cacheLimit: number = 5 * 1024 * 1024; - if (this.cacheConfig.capacityInBytes > cacheLimit) { - logger.error( - 'Cache Capacity should be less than 5MB. Setting back to default. Setting back to default.' - ); - tempInstanceConfig.capacityInBytes = defaultConfig.capacityInBytes; - } - - // Apply sanitized values to the instance config - if (Object.keys(tempInstanceConfig).length > 0) { - this.instanceConfig = tempInstanceConfig; - } - } - - /** - * produce a JSON object with meta-data and data value - * @param value - the value of the item - * @param options - optional, the specified meta-data - * - * @return - the item which has the meta-data and the value - */ - protected fillCacheItem( - key: string, - value: object | number | string | boolean, - options: CacheItemOptions - ): CacheItem { - const ret: CacheItem = { - key, - data: value, - timestamp: getCurrTime(), - visitedTime: getCurrTime(), - priority: options.priority ?? 0, - expires: options.expires ?? 0, - type: typeof value, - byteSize: 0, - }; - - ret.byteSize = getByteLength(JSON.stringify(ret)); - - // for accurate size - ret.byteSize = getByteLength(JSON.stringify(ret)); - return ret; - } - - /** - * Set custom configuration for the cache instance. - * - * @param config - customized configuration (without keyPrefix, which can't be changed) - * - * @return - the current configuration - */ - public configure(config?: Omit): CacheConfig { - if (config) { - if ((config as CacheConfig).keyPrefix) { - logger.warn( - 'keyPrefix can not be re-configured on an existing Cache instance.' - ); - } - - this.instanceConfig = this.instanceConfig - ? Object.assign({}, this.instanceConfig, config) - : (config as CacheConfig); - } - - this.sanitizeConfig(); - - return this.cacheConfig; - } - - /** - * Returns an appropriate configuration for the Cache instance. Will apply any custom configuration for this - * instance on top of the global configuration. Default configuration will be applied in all cases. - * - * @internal - */ - protected get cacheConfig(): CacheConfig { - // const globalCacheConfig = Amplify.getConfig().Cache || {}; - const globalCacheConfig = {}; - - if (this.instanceConfig) { - return Object.assign( - {}, - defaultConfig, - globalCacheConfig, - this.instanceConfig - ); - } else { - return Object.assign({}, defaultConfig, globalCacheConfig); - } - } -} +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ConsoleLogger as Logger } from '../Logger'; +import { KeyValueStorage } from '../storage/KeyValueStorage'; +import { getLocalStorageWithFallback } from '../storage/utils'; +import { defaultConfig } from './constants'; +import { StorageCacheCommon } from './StorageCacheCommon'; +import { Cache, CacheConfig } from './types'; +import { getCurrentSizeKey, getCurrentTime } from './utils'; + +const logger = new Logger('Cache'); + +/** + * Customized storage based on the SessionStorage or LocalStorage with LRU implemented + */ +export class StorageCache extends StorageCacheCommon implements Cache { + storage: Storage; + /** + * initialize the cache + * @param config - the configuration of the cache + */ + constructor(config?: CacheConfig) { + const storage = getLocalStorageWithFallback(); + super({ config, keyValueStorage: new KeyValueStorage(storage) }); + + this.storage = storage; + this.getItem = this.getItem.bind(this); + this.setItem = this.setItem.bind(this); + this.removeItem = this.removeItem.bind(this); + } + + protected async getAllCacheKeys(options?: { omitSizeKey?: boolean }) { + const { omitSizeKey } = options ?? {}; + const keys: string[] = []; + for (let i = 0; i < this.storage.length; i++) { + const key = this.storage.key(i); + if (omitSizeKey && key === getCurrentSizeKey(this.config.keyPrefix)) { + continue; + } + if (key?.startsWith(this.config.keyPrefix)) { + keys.push(key.substring(this.config.keyPrefix.length)); + } + } + return keys; + } + + /** + * Return a new instance of cache with customized configuration. + * @param {Object} config - the customized configuration + * @return {Object} - the new instance of Cache + */ + public createInstance(config: CacheConfig): Cache { + if (!config.keyPrefix || config.keyPrefix === defaultConfig.keyPrefix) { + logger.error('invalid keyPrefix, setting keyPrefix with timeStamp'); + config.keyPrefix = getCurrentTime.toString(); + } + + return new StorageCache(config); + } +} diff --git a/packages/core/src/Cache/StorageCacheCommon.ts b/packages/core/src/Cache/StorageCacheCommon.ts new file mode 100644 index 00000000000..9b2352f9fc8 --- /dev/null +++ b/packages/core/src/Cache/StorageCacheCommon.ts @@ -0,0 +1,581 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Amplify } from '../singleton'; +import { ConsoleLogger as Logger } from '../Logger'; +import { KeyValueStorageInterface } from '../types'; +import { currentSizeKey, defaultConfig } from './constants'; +import { CacheConfig, CacheItem, CacheItemOptions } from './types'; +import { getCurrentSizeKey, getCurrentTime, getByteLength } from './utils'; +import { assert, CacheErrorCode } from './utils/errorHelpers'; + +const logger = new Logger('StorageCache'); + +/** + * Initialization of the cache + * + */ +export abstract class StorageCacheCommon { + protected config: CacheConfig; + protected keyValueStorage: KeyValueStorageInterface; + + /** + * Initialize the cache + * + * @param config - Custom configuration for this instance. + */ + constructor({ + config, + keyValueStorage, + }: { + config?: CacheConfig; + keyValueStorage: KeyValueStorageInterface; + }) { + const globalCacheConfig = Amplify.getConfig().Cache ?? {}; + + this.config = { + ...defaultConfig, + ...globalCacheConfig, + ...config, + }; + this.keyValueStorage = keyValueStorage; + + this.sanitizeConfig(); + } + + protected abstract getAllCacheKeys(options?: { + omitSizeKey?: boolean; + }): Promise; + + public getModuleName() { + return 'Cache'; + } + + /** + * Set custom configuration for the cache instance. + * + * @param config - customized configuration (without keyPrefix, which can't be changed) + * + * @return - the current configuration + */ + public configure(config?: Omit): CacheConfig { + if (config) { + if ((config as CacheConfig).keyPrefix) { + logger.warn( + 'keyPrefix can not be re-configured on an existing Cache instance.' + ); + } + + this.config = { + ...this.config, + ...config, + }; + } + + this.sanitizeConfig(); + + return this.config; + } + + /** + * return the current size of the cache + * @return {Promise} + */ + public async getCurrentCacheSize() { + let size = await this.getStorage().getItem( + getCurrentSizeKey(this.config.keyPrefix) + ); + if (!size) { + await this.getStorage().setItem( + getCurrentSizeKey(this.config.keyPrefix), + '0' + ); + size = '0'; + } + return Number(size); + } + + /** + * Set item into cache. You can put number, string, boolean or object. + * The cache will first check whether has the same key. + * If it has, it will delete the old item and then put the new item in + * The cache will pop out items if it is full + * You can specify the cache item options. The cache will abort and output a warning: + * If the key is invalid + * If the size of the item exceeds itemMaxSize. + * If the value is undefined + * If incorrect cache item configuration + * If error happened with browser storage + * + * @param {String} key - the key of the item + * @param {Object} value - the value of the item + * @param {Object} [options] - optional, the specified meta-data + * + * @return {Promise} + */ + public async setItem( + key: string, + value: any, + options?: Record + ): Promise { + logger.debug( + `Set item: key is ${key}, value is ${value} with options: ${options}` + ); + + if (!key || key === currentSizeKey) { + logger.warn( + `Invalid key: should not be empty or reserved key: '${currentSizeKey}'` + ); + return; + } + + if (typeof value === 'undefined') { + logger.warn(`The value of item should not be undefined!`); + return; + } + + const cacheItemOptions = { + priority: + options?.priority !== undefined + ? options.priority + : this.config.defaultPriority, + expires: + options?.expires !== undefined + ? options.expires + : this.config.defaultTTL + getCurrentTime(), + }; + + if (cacheItemOptions.priority < 1 || cacheItemOptions.priority > 5) { + logger.warn( + `Invalid parameter: priority due to out or range. It should be within 1 and 5.` + ); + return; + } + + const prefixedKey = `${this.config.keyPrefix}${key}`; + const item = this.fillCacheItem(prefixedKey, value, cacheItemOptions); + + // check whether this item is too big; + if (item.byteSize > this.config.itemMaxSize) { + logger.warn( + `Item with key: ${key} you are trying to put into is too big!` + ); + return; + } + + try { + // first look into the storage, if it exists, delete it. + const val = await this.getStorage().getItem(prefixedKey); + if (val) { + await this.removeCacheItem(prefixedKey, JSON.parse(val).byteSize); + } + + // check whether the cache is full + if (await this.isCacheFull(item.byteSize)) { + const validKeys = await this.clearInvalidAndGetRemainingKeys(); + if (await this.isCacheFull(item.byteSize)) { + const sizeToPop = await this.sizeToPop(item.byteSize); + await this.popOutItems(validKeys, sizeToPop); + } + } + // put item in the cache + return this.setCacheItem(prefixedKey, item); + } catch (e) { + logger.warn(`setItem failed! ${e}`); + } + } + + /** + * Get item from cache. It will return null if item doesn’t exist or it has been expired. + * If you specified callback function in the options, + * then the function will be executed if no such item in the cache + * and finally put the return value into cache. + * Please make sure the callback function will return the value you want to put into the cache. + * The cache will abort output a warning: + * If the key is invalid + * If error happened with AsyncStorage + * + * @param {String} key - the key of the item + * @param {Object} [options] - the options of callback function + * + * @return {Promise} - return a promise resolves to be the value of the item + */ + public async getItem( + key: string, + options?: CacheItemOptions + ): Promise { + logger.debug(`Get item: key is ${key} with options ${options}`); + let cached; + + if (!key || key === currentSizeKey) { + logger.warn( + `Invalid key: should not be empty or reserved key: '${currentSizeKey}'` + ); + return null; + } + + const prefixedKey = `${this.config.keyPrefix}${key}`; + + try { + cached = await this.getStorage().getItem(prefixedKey); + if (cached != null) { + if (await this.isExpired(prefixedKey)) { + // if expired, remove that item and return null + await this.removeCacheItem(prefixedKey, JSON.parse(cached).byteSize); + } else { + // if not expired, update its visitedTime and return the value + const item = await this.updateVisitedTime( + JSON.parse(cached), + prefixedKey + ); + return item.data; + } + } + + if (options?.callback) { + const val = options.callback(); + if (val !== null) { + await this.setItem(key, val, options); + } + return val; + } + return null; + } catch (e) { + logger.warn(`getItem failed! ${e}`); + return null; + } + } + + /** + * remove item from the cache + * The cache will abort output a warning: + * If error happened with AsyncStorage + * @param {String} key - the key of the item + * @return {Promise} + */ + public async removeItem(key: string): Promise { + logger.debug(`Remove item: key is ${key}`); + + if (!key || key === currentSizeKey) { + logger.warn( + `Invalid key: should not be empty or reserved key: '${currentSizeKey}'` + ); + return; + } + + const prefixedKey = `${this.config.keyPrefix}${key}`; + + try { + const val = await this.getStorage().getItem(prefixedKey); + if (val) { + await this.removeCacheItem(prefixedKey, JSON.parse(val).byteSize); + } + } catch (e) { + logger.warn(`removeItem failed! ${e}`); + } + } + + /** + * Return all the keys owned by this cache. + * Will return an empty array if error occurred. + * + * @return {Promise} + */ + public async getAllKeys() { + try { + return await this.getAllCacheKeys(); + } catch (e) { + logger.warn(`getAllkeys failed! ${e}`); + return []; + } + } + + protected getStorage(): KeyValueStorageInterface { + return this.keyValueStorage; + } + + /** + * check whether item is expired + * + * @param key - the key of the item + * + * @return true if the item is expired. + */ + protected async isExpired(key: string): Promise { + const text = await this.getStorage().getItem(key); + assert(text !== null, CacheErrorCode.NoCacheItem, `Key: ${key}`); + const item: CacheItem = JSON.parse(text); + if (getCurrentTime() >= item.expires) { + return true; + } + return false; + } + + /** + * delete item from cache + * + * @param prefixedKey - the key of the item + * @param size - optional, the byte size of the item + */ + protected async removeCacheItem( + prefixedKey: string, + size?: number + ): Promise { + const item = await this.getStorage().getItem(prefixedKey); + assert(item !== null, CacheErrorCode.NoCacheItem, `Key: ${prefixedKey}`); + const itemSize: number = size ?? JSON.parse(item).byteSize; + // first try to update the current size of the cache + await this.decreaseCurrentSizeInBytes(itemSize); + + // try to remove the item from cache + try { + await this.getStorage().removeItem(prefixedKey); + } catch (removeItemError) { + // if some error happened, we need to rollback the current size + await this.increaseCurrentSizeInBytes(itemSize); + logger.error(`Failed to remove item: ${removeItemError}`); + } + } + + /** + * produce a JSON object with meta-data and data value + * @param value - the value of the item + * @param options - optional, the specified meta-data + * + * @return - the item which has the meta-data and the value + */ + protected fillCacheItem( + key: string, + value: object | number | string | boolean, + options: CacheItemOptions + ): CacheItem { + const item: CacheItem = { + key, + data: value, + timestamp: getCurrentTime(), + visitedTime: getCurrentTime(), + priority: options.priority ?? 0, + expires: options.expires ?? 0, + type: typeof value, + byteSize: 0, + }; + // calculate byte size + item.byteSize = getByteLength(JSON.stringify(item)); + // re-calculate using cache item with updated byteSize property + item.byteSize = getByteLength(JSON.stringify(item)); + return item; + } + + private sanitizeConfig(): void { + if (this.config.itemMaxSize > this.config.capacityInBytes) { + logger.error( + 'Invalid parameter: itemMaxSize. It should be smaller than capacityInBytes. Setting back to default.' + ); + this.config.itemMaxSize = defaultConfig.itemMaxSize; + } + + if (this.config.defaultPriority > 5 || this.config.defaultPriority < 1) { + logger.error( + 'Invalid parameter: defaultPriority. It should be between 1 and 5. Setting back to default.' + ); + this.config.defaultPriority = defaultConfig.defaultPriority; + } + + if ( + Number(this.config.warningThreshold) > 1 || + Number(this.config.warningThreshold) < 0 + ) { + logger.error( + 'Invalid parameter: warningThreshold. It should be between 0 and 1. Setting back to default.' + ); + this.config.warningThreshold = defaultConfig.warningThreshold; + } + + // Set 5MB limit + const cacheLimit: number = 5 * 1024 * 1024; + if (this.config.capacityInBytes > cacheLimit) { + logger.error( + 'Cache Capacity should be less than 5MB. Setting back to default. Setting back to default.' + ); + this.config.capacityInBytes = defaultConfig.capacityInBytes; + } + } + + /** + * increase current size of the cache + * + * @param amount - the amount of the cache szie which need to be increased + */ + private async increaseCurrentSizeInBytes(amount: number): Promise { + const size = await this.getCurrentCacheSize(); + await this.getStorage().setItem( + getCurrentSizeKey(this.config.keyPrefix), + (size + amount).toString() + ); + } + + /** + * decrease current size of the cache + * + * @param amount - the amount of the cache size which needs to be decreased + */ + private async decreaseCurrentSizeInBytes(amount: number): Promise { + const size = await this.getCurrentCacheSize(); + await this.getStorage().setItem( + getCurrentSizeKey(this.config.keyPrefix), + (size - amount).toString() + ); + } + + /** + * update the visited time if item has been visited + * + * @param item - the item which need to be updated + * @param prefixedKey - the key of the item + * + * @return the updated item + */ + private async updateVisitedTime( + item: CacheItem, + prefixedKey: string + ): Promise { + item.visitedTime = getCurrentTime(); + await this.getStorage().setItem(prefixedKey, JSON.stringify(item)); + return item; + } + + /** + * put item into cache + * + * @param prefixedKey - the key of the item + * @param itemData - the value of the item + * @param itemSizeInBytes - the byte size of the item + */ + private async setCacheItem( + prefixedKey: string, + item: CacheItem + ): Promise { + // first try to update the current size of the cache. + await this.increaseCurrentSizeInBytes(item.byteSize); + + // try to add the item into cache + try { + await this.getStorage().setItem(prefixedKey, JSON.stringify(item)); + } catch (setItemErr) { + // if some error happened, we need to rollback the current size + await this.decreaseCurrentSizeInBytes(item.byteSize); + logger.error(`Failed to set item ${setItemErr}`); + } + } + + /** + * total space needed when poping out items + * + * @param itemSize + * + * @return total space needed + */ + private async sizeToPop(itemSize: number): Promise { + const cur = await this.getCurrentCacheSize(); + const spaceItemNeed = cur + itemSize - this.config.capacityInBytes; + const cacheThresholdSpace = + (1 - this.config.warningThreshold) * this.config.capacityInBytes; + return spaceItemNeed > cacheThresholdSpace + ? spaceItemNeed + : cacheThresholdSpace; + } + + /** + * see whether cache is full + * + * @param itemSize + * + * @return true if cache is full + */ + private async isCacheFull(itemSize: number): Promise { + const cur = await this.getCurrentCacheSize(); + return itemSize + cur > this.config.capacityInBytes; + } + + /** + * get all the items we have, sort them by their priority, + * if priority is same, sort them by their last visited time + * pop out items from the low priority (5 is the lowest) + * @private + * @param keys - all the keys in this cache + * @param sizeToPop - the total size of the items which needed to be poped out + */ + private async popOutItems(keys: string[], sizeToPop: number): Promise { + const items: any[] = []; + let remainedSize = sizeToPop; + for (let i = 0; i < keys.length; i += 1) { + const val = await this.getStorage().getItem(keys[i]); + if (val != null) { + const item = JSON.parse(val); + items.push(item); + } + } + + // first compare priority + // then compare visited time + items.sort((a, b) => { + if (a.priority > b.priority) { + return -1; + } else if (a.priority < b.priority) { + return 1; + } else { + if (a.visitedTime < b.visitedTime) { + return -1; + } else return 1; + } + }); + + for (let i = 0; i < items.length; i += 1) { + // pop out items until we have enough room for new item + await this.removeCacheItem(items[i].key, items[i].byteSize); + remainedSize -= items[i].byteSize; + if (remainedSize <= 0) { + return; + } + } + } + + /** + * Scan the storage and combine the following operations for efficiency + * 1. Clear out all expired keys owned by this cache, not including the size key. + * 2. Return the remaining keys. + * + * @return The remaining valid keys + */ + private async clearInvalidAndGetRemainingKeys(): Promise { + const remainingKeys = []; + const keys = await this.getAllCacheKeys({ + omitSizeKey: true, + }); + for (const key of keys) { + if (await this.isExpired(key)) { + await this.removeCacheItem(key); + } else { + remainingKeys.push(key); + } + } + return remainingKeys; + } + + /** + * clear the entire cache + * The cache will abort and output a warning if error occurs + * @return {Promise} + */ + async clear() { + logger.debug(`Clear Cache`); + try { + const keys = await this.getAllKeys(); + for (const key of keys) { + await this.getStorage().removeItem(key); + } + } catch (e) { + logger.warn(`clear failed! ${e}`); + } + } +} diff --git a/packages/core/src/Cache/Utils/CacheUtils.ts b/packages/core/src/Cache/Utils/CacheUtils.ts deleted file mode 100644 index f394ca50073..00000000000 --- a/packages/core/src/Cache/Utils/CacheUtils.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { CacheConfig } from '../types'; -import { getDefaultStorageWithFallback } from '../../storage/utils'; -/** - * Default cache config - */ -export const defaultConfig: CacheConfig = { - keyPrefix: 'aws-amplify-cache', - capacityInBytes: 1048576, // 1MB - itemMaxSize: 210000, // about 200kb - defaultTTL: 259200000, // about 3 days - defaultPriority: 5, - warningThreshold: 0.8, - storage: getDefaultStorageWithFallback(), -}; - -/** - * return the byte size of the string - * @param str - */ -export function getByteLength(str: string): number { - let ret: number = 0; - ret = str.length; - - for (let i = str.length; i >= 0; i -= 1) { - const charCode: number = str.charCodeAt(i); - if (charCode > 0x7f && charCode <= 0x7ff) { - ret += 1; - } else if (charCode > 0x7ff && charCode <= 0xffff) { - ret += 2; - } - // trail surrogate - if (charCode >= 0xdc00 && charCode <= 0xdfff) { - i -= 1; - } - } - - return ret; -} - -/** - * get current time - */ -export function getCurrTime(): number { - const currTime = new Date(); - return currTime.getTime(); -} - -/** - * check if passed value is an integer - */ -export function isInteger(value?: number): boolean { - if (Number.isInteger) { - return Number.isInteger(value); - } - - return _isInteger(value); -} - -function _isInteger(value?: number): boolean { - return ( - typeof value === 'number' && isFinite(value) && Math.floor(value) === value - ); -} - -/** - * provide an object as the in-memory cache - */ -let store: Record = {}; -export class CacheObject { - static clear(): void { - store = {}; - } - - static getItem(key: string): string | null { - return store[key] || null; - } - - static setItem(key: string, value: string): void { - store[key] = value; - } - - static removeItem(key: string): void { - delete store[key]; - } -} - -export const getCurrSizeKey = (keyPrefix: string) => keyPrefix + 'CurSize'; diff --git a/packages/core/src/Cache/constants.ts b/packages/core/src/Cache/constants.ts new file mode 100644 index 00000000000..7c4e026f93f --- /dev/null +++ b/packages/core/src/Cache/constants.ts @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CacheConfig } from './types'; + +/** + * Default cache config + */ +export const defaultConfig: Omit = { + keyPrefix: 'aws-amplify-cache', + capacityInBytes: 1048576, // 1MB + itemMaxSize: 210000, // about 200kb + defaultTTL: 259200000, // about 3 days + defaultPriority: 5, + warningThreshold: 0.8, +}; + +export const currentSizeKey = 'CurSize'; diff --git a/packages/core/src/Cache/index.ts b/packages/core/src/Cache/index.ts new file mode 100644 index 00000000000..7c596ff8f83 --- /dev/null +++ b/packages/core/src/Cache/index.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { StorageCache } from './StorageCache'; + +export const Cache = new StorageCache(); diff --git a/packages/core/src/Cache/reactnative.ts b/packages/core/src/Cache/reactnative.ts deleted file mode 100644 index ad4a46d4185..00000000000 --- a/packages/core/src/Cache/reactnative.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { Cache, AsyncStorageCache } from './AsyncStorageCache'; - -export { AsyncStorageCache }; - -// Standard `Cache` export to maintain interoperability with React Native -export { Cache }; diff --git a/packages/core/src/Cache/types/Cache.ts b/packages/core/src/Cache/types/Cache.ts deleted file mode 100644 index d0418715365..00000000000 --- a/packages/core/src/Cache/types/Cache.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -/** - * Cache Interface - */ -export interface ICache { - /** Put item into cache */ - setItem(key: string, value: any, options?: CacheItemOptions): void; - - /** Get item from cache */ - getItem(key: string, options?: CacheItemOptions): any; - - /** Remove item from cache */ - removeItem(key: string): void; - - /** Remove all items from cache */ - clear(): void; - - /** Get all keys form cache */ - getAllKeys(): string[] | Promise; - - /** Get current size of the cache */ - getCacheCurSize(): number | Promise; - - /** create a new instance with customized config */ - createInstance(config: CacheConfig): ICache; - - /** change current configuration */ - configure(config: CacheConfig): CacheConfig; -} - -/** - * Cache instance options - */ -export interface CacheConfig { - /** Prepend to key to avoid conflicts */ - keyPrefix: string; - - /** Cache capacity, in bytes */ - capacityInBytes: number; - - /** Max size of one item */ - itemMaxSize: number; - - /** Time to live, in milliseconds */ - defaultTTL: number; - - /** Warn when over threshold percentage of capacity, maximum 1 */ - warningThreshold: number; - - /** default priority number put on cached items */ - defaultPriority: number; - - storage?: Storage; - - Cache?: Cache; -} - -export interface CacheItem { - key: string; - data: any; - timestamp: number; - visitedTime: number; - priority: number; - expires: number; - type: string; - byteSize: number; -} - -export interface CacheItemOptions { - priority?: number; - expires?: number; - callback?: Function; -} diff --git a/packages/core/src/Cache/types/cache.ts b/packages/core/src/Cache/types/cache.ts new file mode 100644 index 00000000000..f7e06448d1a --- /dev/null +++ b/packages/core/src/Cache/types/cache.ts @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CacheConfig } from '../../singleton/Cache/types'; +export { CacheConfig }; + +/** + * Cache Interface + */ +export interface Cache { + /** Put item into cache */ + setItem(key: string, value: any, options?: CacheItemOptions): Promise; + + /** Get item from cache */ + getItem(key: string, options?: CacheItemOptions): Promise; + + /** Remove item from cache */ + removeItem(key: string): Promise; + + /** Remove all items from cache */ + clear(): Promise; + + /** Get all keys form cache */ + getAllKeys(): Promise; + + /** Get current size of the cache */ + getCurrentCacheSize(): Promise; + + /** create a new instance with customized config */ + createInstance(config: CacheConfig): Cache; + + /** change current configuration */ + configure(config: CacheConfig): CacheConfig; +} + +export interface CacheItem { + key: string; + data: any; + timestamp: number; + visitedTime: number; + priority: number; + expires: number; + type: string; + byteSize: number; +} + +export interface CacheItemOptions { + priority?: number; + expires?: number; + callback?: Function; +} diff --git a/packages/core/src/Cache/types/index.ts b/packages/core/src/Cache/types/index.ts index 2ba823ff03c..5ca15b25bd6 100644 --- a/packages/core/src/Cache/types/index.ts +++ b/packages/core/src/Cache/types/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export * from './Cache'; +export * from './cache'; diff --git a/packages/core/src/Cache/Utils/CacheList.ts b/packages/core/src/Cache/utils/CacheList.ts similarity index 95% rename from packages/core/src/Cache/Utils/CacheList.ts rename to packages/core/src/Cache/utils/CacheList.ts index 596601c0b0c..eecd79c520e 100644 --- a/packages/core/src/Cache/Utils/CacheList.ts +++ b/packages/core/src/Cache/utils/CacheList.ts @@ -1,185 +1,185 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { assert, CacheErrorCode } from './errorHelpers'; - -class DoubleLinkedNode { - key: string; - prevNode: DoubleLinkedNode | null; - nextNode: DoubleLinkedNode | null; - - constructor(keyVal?: string) { - this.key = keyVal ? keyVal : ''; - this.prevNode = null; - this.nextNode = null; - } -} - -/** - * double linked list plus a hash table inside - * each key in the cache stored as a node in the list - * recently visited node will be rotated to the head - * so the Last Recently Visited node will be at the tail - * - * @member head - dummy head of the linked list - * @member tail - dummy tail of the linked list - * @member hashtable - the hashtable which maps cache key to list node - * @member length - length of the list - */ -export default class CacheList { - private head: DoubleLinkedNode; - private tail: DoubleLinkedNode; - private hashtable: Record; - private length: number; - - /** - * initialization - */ - constructor() { - this.head = new DoubleLinkedNode(); - this.tail = new DoubleLinkedNode(); - this.hashtable = {}; - this.length = 0; - - this.head.nextNode = this.tail; - this.tail.prevNode = this.head; - } - - /** - * insert node to the head of the list - * - * @param node - */ - private insertNodeToHead(node: DoubleLinkedNode) { - const tmp: DoubleLinkedNode | null = this.head.nextNode; - this.head.nextNode = node; - node.nextNode = tmp; - node.prevNode = this.head; - assert(tmp !== null, CacheErrorCode.NullPreviousNode); - tmp.prevNode = node; - - this.length = this.length + 1; - } - - /** - * remove node - * - * @param node - */ - private removeNode(node: DoubleLinkedNode): void { - assert(node.prevNode !== null, CacheErrorCode.NullPreviousNode); - assert(node.nextNode !== null, CacheErrorCode.NullNextNode); - node.prevNode.nextNode = node.nextNode; - node.nextNode.prevNode = node.prevNode; - - node.prevNode = null; - node.nextNode = null; - - this.length = this.length - 1; - } - - /** - * @return true if list is empty - */ - public isEmpty(): boolean { - return this.length === 0; - } - - /** - * refresh node so it is rotated to the head - * - * @param key - key of the node - */ - public refresh(key: string): void { - const node: DoubleLinkedNode = this.hashtable[key]; - this.removeNode(node); - this.insertNodeToHead(node); - } - - /** - * insert new node to the head and add it in the hashtable - * - * @param key - the key of the node - */ - public insertItem(key: string): void { - const node: DoubleLinkedNode = new DoubleLinkedNode(key); - this.hashtable[key] = node; - this.insertNodeToHead(node); - } - - /** - * @return the LAST Recently Visited key - */ - public getLastItem(): string { - assert(this.tail.prevNode !== null, CacheErrorCode.NullPreviousNode); - return this.tail.prevNode.key; - } - - /** - * remove the cache key from the list and hashtable - * @param key - the key of the node - */ - public removeItem(key: string): void { - const removedItem: DoubleLinkedNode = this.hashtable[key]; - this.removeNode(removedItem); - delete this.hashtable[key]; - } - - /** - * @return length of the list - */ - public getSize(): number { - return this.length; - } - - /** - * @return true if the key is in the hashtable - * @param key - */ - public containsKey(key: string): boolean { - return key in this.hashtable; - } - - /** - * clean up the list and hashtable - */ - public clearList(): void { - for (const key of Object.keys(this.hashtable)) { - if (this.hashtable.hasOwnProperty(key)) { - delete this.hashtable[key]; - } - } - this.head.nextNode = this.tail; - this.tail.prevNode = this.head; - this.length = 0; - } - - /** - * @return all keys in the hashtable - */ - public getKeys(): string[] { - return Object.keys(this.hashtable); - } - - /** - * mainly for test - * - * @param key - * @return true if key is the head node - */ - public isHeadNode(key: string): boolean { - const node = this.hashtable[key]; - return node.prevNode === this.head; - } - - /** - * mainly for test - * - * @param key - * @return true if key is the tail node - */ - public isTailNode(key: string): boolean { - const node = this.hashtable[key]; - return node.nextNode === this.tail; - } -} +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { assert, CacheErrorCode } from './errorHelpers'; + +class DoubleLinkedNode { + key: string; + prevNode: DoubleLinkedNode | null; + nextNode: DoubleLinkedNode | null; + + constructor(keyVal?: string) { + this.key = keyVal ? keyVal : ''; + this.prevNode = null; + this.nextNode = null; + } +} + +/** + * double linked list plus a hash table inside + * each key in the cache stored as a node in the list + * recently visited node will be rotated to the head + * so the Last Recently Visited node will be at the tail + * + * @member head - dummy head of the linked list + * @member tail - dummy tail of the linked list + * @member hashtable - the hashtable which maps cache key to list node + * @member length - length of the list + */ +export class CacheList { + private head: DoubleLinkedNode; + private tail: DoubleLinkedNode; + private hashtable: Record; + private length: number; + + /** + * initialization + */ + constructor() { + this.head = new DoubleLinkedNode(); + this.tail = new DoubleLinkedNode(); + this.hashtable = {}; + this.length = 0; + + this.head.nextNode = this.tail; + this.tail.prevNode = this.head; + } + + /** + * insert node to the head of the list + * + * @param node + */ + private insertNodeToHead(node: DoubleLinkedNode) { + const tmp: DoubleLinkedNode | null = this.head.nextNode; + this.head.nextNode = node; + node.nextNode = tmp; + node.prevNode = this.head; + assert(tmp !== null, CacheErrorCode.NullPreviousNode); + tmp.prevNode = node; + + this.length = this.length + 1; + } + + /** + * remove node + * + * @param node + */ + private removeNode(node: DoubleLinkedNode): void { + assert(node.prevNode !== null, CacheErrorCode.NullPreviousNode); + assert(node.nextNode !== null, CacheErrorCode.NullNextNode); + node.prevNode.nextNode = node.nextNode; + node.nextNode.prevNode = node.prevNode; + + node.prevNode = null; + node.nextNode = null; + + this.length = this.length - 1; + } + + /** + * @return true if list is empty + */ + public isEmpty(): boolean { + return this.length === 0; + } + + /** + * refresh node so it is rotated to the head + * + * @param key - key of the node + */ + public refresh(key: string): void { + const node: DoubleLinkedNode = this.hashtable[key]; + this.removeNode(node); + this.insertNodeToHead(node); + } + + /** + * insert new node to the head and add it in the hashtable + * + * @param key - the key of the node + */ + public insertItem(key: string): void { + const node: DoubleLinkedNode = new DoubleLinkedNode(key); + this.hashtable[key] = node; + this.insertNodeToHead(node); + } + + /** + * @return the LAST Recently Visited key + */ + public getLastItem(): string { + assert(this.tail.prevNode !== null, CacheErrorCode.NullPreviousNode); + return this.tail.prevNode.key; + } + + /** + * remove the cache key from the list and hashtable + * @param key - the key of the node + */ + public removeItem(key: string): void { + const removedItem: DoubleLinkedNode = this.hashtable[key]; + this.removeNode(removedItem); + delete this.hashtable[key]; + } + + /** + * @return length of the list + */ + public getSize(): number { + return this.length; + } + + /** + * @return true if the key is in the hashtable + * @param key + */ + public containsKey(key: string): boolean { + return key in this.hashtable; + } + + /** + * clean up the list and hashtable + */ + public clearList(): void { + for (const key of Object.keys(this.hashtable)) { + if (this.hashtable.hasOwnProperty(key)) { + delete this.hashtable[key]; + } + } + this.head.nextNode = this.tail; + this.tail.prevNode = this.head; + this.length = 0; + } + + /** + * @return all keys in the hashtable + */ + public getKeys(): string[] { + return Object.keys(this.hashtable); + } + + /** + * mainly for test + * + * @param key + * @return true if key is the head node + */ + public isHeadNode(key: string): boolean { + const node = this.hashtable[key]; + return node.prevNode === this.head; + } + + /** + * mainly for test + * + * @param key + * @return true if key is the tail node + */ + public isTailNode(key: string): boolean { + const node = this.hashtable[key]; + return node.nextNode === this.tail; + } +} diff --git a/packages/core/src/Cache/utils/cacheHelpers.ts b/packages/core/src/Cache/utils/cacheHelpers.ts new file mode 100644 index 00000000000..90235802b12 --- /dev/null +++ b/packages/core/src/Cache/utils/cacheHelpers.ts @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { currentSizeKey } from '../constants'; + +/** + * return the byte size of the string + * @param str + */ +export function getByteLength(str: string): number { + let ret: number = 0; + ret = str.length; + + for (let i = str.length; i >= 0; i -= 1) { + const charCode: number = str.charCodeAt(i); + if (charCode > 0x7f && charCode <= 0x7ff) { + ret += 1; + } else if (charCode > 0x7ff && charCode <= 0xffff) { + ret += 2; + } + // trail surrogate + if (charCode >= 0xdc00 && charCode <= 0xdfff) { + i -= 1; + } + } + + return ret; +} + +/** + * get current time + */ +export function getCurrentTime(): number { + const currentTime = new Date(); + return currentTime.getTime(); +} + +/** + * check if passed value is an integer + */ +export function isInteger(value?: number): boolean { + if (Number.isInteger) { + return Number.isInteger(value); + } + + return ( + typeof value === 'number' && isFinite(value) && Math.floor(value) === value + ); +} + +export const getCurrentSizeKey = (keyPrefix: string) => + `${keyPrefix}${currentSizeKey}`; diff --git a/packages/core/src/Cache/Utils/errorHelpers.ts b/packages/core/src/Cache/utils/errorHelpers.ts similarity index 85% rename from packages/core/src/Cache/Utils/errorHelpers.ts rename to packages/core/src/Cache/utils/errorHelpers.ts index ccb4b7fee05..f191ce4449b 100644 --- a/packages/core/src/Cache/Utils/errorHelpers.ts +++ b/packages/core/src/Cache/utils/errorHelpers.ts @@ -6,7 +6,6 @@ import { AmplifyErrorMap, AssertionFunction } from '../../types'; export enum CacheErrorCode { NoCacheItem = 'NoCacheItem', - NoCacheStorage = 'NoCacheStorage', NullNextNode = 'NullNextNode', NullPreviousNode = 'NullPreviousNode', } @@ -15,9 +14,6 @@ const cacheErrorMap: AmplifyErrorMap = { [CacheErrorCode.NoCacheItem]: { message: 'Item not found in the cache storage.', }, - [CacheErrorCode.NoCacheStorage]: { - message: 'Storage is not defined in the cache config.', - }, [CacheErrorCode.NullNextNode]: { message: 'Next node is null.', }, diff --git a/packages/core/src/Cache/Utils/index.ts b/packages/core/src/Cache/utils/index.ts similarity index 55% rename from packages/core/src/Cache/Utils/index.ts rename to packages/core/src/Cache/utils/index.ts index e642a04ff8a..3a81dd7c53e 100644 --- a/packages/core/src/Cache/Utils/index.ts +++ b/packages/core/src/Cache/utils/index.ts @@ -2,10 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 export { - CacheObject, - defaultConfig, getByteLength, - getCurrTime, + getCurrentSizeKey, + getCurrentTime, isInteger, -} from './CacheUtils'; -export { default as CacheList } from './CacheList'; +} from './cacheHelpers'; +export { CacheList } from './CacheList'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e62f745d581..00a69d6442b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,6 +24,7 @@ export { AuthUserPoolConfig, AuthUserPoolAndIdentityPoolConfig, APIConfig, + CacheConfig, StorageAccessLevel, StorageConfig, GetCredentialsOptions, @@ -60,10 +61,7 @@ export { export { KeyValueStorageInterface } from './types'; // Cache exports -import { BrowserStorageCache } from './Cache/BrowserStorageCache'; -export { InMemoryCache } from './Cache/InMemoryCache'; -export { BrowserStorageCache }; -export { BrowserStorageCache as Cache }; // Maintain interoperability with React Native +export { Cache } from './Cache'; // Internationalization utilities export { I18n } from './I18n'; diff --git a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts index 01f1e21dea4..044fbd76ebf 100644 --- a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts +++ b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts @@ -93,6 +93,6 @@ export const updateEndpoint = async ({ await clientUpdateEndpoint({ credentials, region, userAgentValue }, input); // if we had to create an endpoint id, we need to now cache it if (!!createdEndpointId) { - cacheEndpointId(appId, category, createdEndpointId); + return cacheEndpointId(appId, category, createdEndpointId); } }; diff --git a/packages/core/src/singleton/Cache/types.ts b/packages/core/src/singleton/Cache/types.ts new file mode 100644 index 00000000000..798e2a015d3 --- /dev/null +++ b/packages/core/src/singleton/Cache/types.ts @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Cache instance options + */ +export interface CacheConfig { + /** Prepend to key to avoid conflicts */ + keyPrefix: string; + + /** Cache capacity, in bytes */ + capacityInBytes: number; + + /** Max size of one item */ + itemMaxSize: number; + + /** Time to live, in milliseconds */ + defaultTTL: number; + + /** Warn when over threshold percentage of capacity, maximum 1 */ + warningThreshold: number; + + /** default priority number put on cached items */ + defaultPriority: number; + + storage?: Storage; +} diff --git a/packages/core/src/singleton/types.ts b/packages/core/src/singleton/types.ts index 69bd6d49d09..d16634c1014 100644 --- a/packages/core/src/singleton/types.ts +++ b/packages/core/src/singleton/types.ts @@ -12,6 +12,7 @@ import { GetCredentialsOptions, CognitoIdentityPoolConfig, } from './Auth/types'; +import { CacheConfig } from './Cache/types'; import { GeoConfig } from './Geo/types'; import { LibraryStorageOptions, @@ -32,7 +33,7 @@ export type ResourcesConfig = { API?: APIConfig; Analytics?: AnalyticsConfig; Auth?: AuthConfig; - // Cache?: CacheConfig; + Cache?: CacheConfig; // DataStore?: {}; I18n?: I18nConfig; // Interactions?: {}; @@ -55,6 +56,7 @@ export { AuthUserPoolConfig, AuthIdentityPoolConfig, AuthUserPoolAndIdentityPoolConfig, + CacheConfig, GetCredentialsOptions, StorageAccessLevel, StorageConfig, diff --git a/packages/core/src/storage/DefaultStorage.ts b/packages/core/src/storage/DefaultStorage.ts index 8a864abe648..fdacd0e6550 100644 --- a/packages/core/src/storage/DefaultStorage.ts +++ b/packages/core/src/storage/DefaultStorage.ts @@ -2,13 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { KeyValueStorage } from './KeyValueStorage'; -import { getDefaultStorageWithFallback } from './utils'; +import { getLocalStorageWithFallback } from './utils'; /** * @internal */ export class DefaultStorage extends KeyValueStorage { constructor() { - super(getDefaultStorageWithFallback()); + super(getLocalStorageWithFallback()); } } diff --git a/packages/core/src/storage/utils.ts b/packages/core/src/storage/utils.ts index 73245dea687..527d91d7f4e 100644 --- a/packages/core/src/storage/utils.ts +++ b/packages/core/src/storage/utils.ts @@ -7,7 +7,7 @@ import { InMemoryStorage } from './InMemoryStorage'; * @internal * @returns Either a reference to window.localStorage or an in-memory storage as fallback */ -export const getDefaultStorageWithFallback = (): Storage => +export const getLocalStorageWithFallback = (): Storage => typeof window !== 'undefined' && window.localStorage ? window.localStorage : new InMemoryStorage(); From 79743a137d3b9d2b1e8eb927715cdc6e2038c96c Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Thu, 12 Oct 2023 17:27:51 -0500 Subject: [PATCH 21/22] chore: Enable `integ_datastore_auth_v2-owner-based-default` DS test (#12267) --- .github/integ-config/integ-all.yml | 16 ++++++++-------- packages/aws-amplify/package.json | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index 08c0116ed42..3af0fd44f3f 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -107,13 +107,13 @@ tests: # sample_name: owner-custom-field-default # spec: owner-custom-field-default # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-owner-based-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/owner-based-default-v2 - # spec: owner-based-default - # browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-owner-based-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/owner-based-default-v2 + spec: owner-based-default + browser: *minimal_browser_list # - test_name: integ_datastore_auth_v2-static-user-pool-groups-default # desc: 'DataStore Auth CLI v2' # framework: react @@ -582,7 +582,7 @@ tests: spec: personalize-unauth # Temp fix: browser: *minimal_browser_list - + - test_name: integ_react_analytics_kinesis_data_firehose_auth desc: 'Test record API for KDF with authenticated user' framework: react diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index f62f55ba627..a346433a1d7 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -232,7 +232,8 @@ "storage", "datastore", "in-app-messaging", - "push-notifications" + "push-notifications", + "utils" ], "dependencies": { "@aws-amplify/api": "6.0.0", From 134f90a0c6f34071b4508832ff40ee9ac3dd8afb Mon Sep 17 00:00:00 2001 From: ManojNB Date: Thu, 12 Oct 2023 17:03:07 -0700 Subject: [PATCH 22/22] feat(inapp): initializeInAppMessaging API (#12269) * feat: initialize api * chore: intiialize Inapp in tests --------- Co-authored-by: Jim Blanchard --- .../aws-amplify/__tests__/exports.test.ts | 2 + .../pinpoint/apis/dispatchEvent.test.ts | 8 ++- .../pinpoint/apis/identifyUser.test.ts | 6 +- .../apis/initializeInAppMessaging.test.ts | 49 ++++++++++++++ .../pinpoint/apis/interactionEvents.test.ts | 4 ++ .../pinpoint/apis/setConflictHandler.test.ts | 4 ++ .../pinpoint/apis/syncMessages.test.ts | 6 +- .../utils/processInAppMessages.test.ts | 6 +- .../src/inAppMessaging/errors/validation.ts | 6 ++ .../notifications/src/inAppMessaging/index.ts | 1 + .../src/inAppMessaging/providers/index.ts | 1 + .../providers/pinpoint/apis/dispatchEvent.ts | 7 +- .../providers/pinpoint/apis/identifyUser.ts | 4 +- .../providers/pinpoint/apis/index.ts | 1 + .../pinpoint/apis/initializeInAppMessaging.ts | 64 +++++++++++++++++++ .../pinpoint/apis/notifyMessageInteraction.ts | 4 ++ .../pinpoint/apis/onMessageActionTaken.ts | 4 ++ .../pinpoint/apis/onMessageDismissed.ts | 4 ++ .../pinpoint/apis/onMessageDisplayed.ts | 4 ++ .../pinpoint/apis/onMessageReceived.ts | 4 ++ .../pinpoint/apis/setConflictHandler.ts | 5 +- .../providers/pinpoint/apis/syncMessages.ts | 4 +- .../providers/pinpoint/index.ts | 1 + .../providers/pinpoint/utils/helpers.ts | 2 - .../providers/pinpoint/utils/index.ts | 6 +- ...essages.ts => messageProcessingHelpers.ts} | 62 ++++++++++++++++-- .../src/inAppMessaging/utils/index.ts | 8 +++ .../src/inAppMessaging/utils/statusHelpers.ts | 32 ++++++++++ yarn.lock | 2 +- 29 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts create mode 100644 packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts rename packages/notifications/src/inAppMessaging/providers/pinpoint/utils/{processInAppMessages.ts => messageProcessingHelpers.ts} (72%) create mode 100644 packages/notifications/src/inAppMessaging/utils/index.ts create mode 100644 packages/notifications/src/inAppMessaging/utils/statusHelpers.ts diff --git a/packages/aws-amplify/__tests__/exports.test.ts b/packages/aws-amplify/__tests__/exports.test.ts index fbf09baa9ab..74a35a59a5c 100644 --- a/packages/aws-amplify/__tests__/exports.test.ts +++ b/packages/aws-amplify/__tests__/exports.test.ts @@ -122,6 +122,7 @@ describe('aws-amplify Exports', () => { "syncMessages", "dispatchEvent", "setConflictHandler", + "initializeInAppMessaging", "onMessageReceived", "onMessageDisplayed", "onMessageDismissed", @@ -139,6 +140,7 @@ describe('aws-amplify Exports', () => { "syncMessages", "dispatchEvent", "setConflictHandler", + "initializeInAppMessaging", "onMessageReceived", "onMessageDisplayed", "onMessageDismissed", diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts index d23b28c7768..b65b05e608d 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { defaultStorage } from '@aws-amplify/core'; -import { dispatchEvent } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import { + dispatchEvent, + initializeInAppMessaging, +} from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; import { processInAppMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; import { inAppMessages, @@ -22,6 +25,9 @@ const mockNotifyEventListeners = notifyEventListeners as jest.Mock; const mockProcessInAppMessages = processInAppMessages as jest.Mock; describe('dispatchEvent', () => { + beforeAll(() => { + initializeInAppMessaging(); + }); beforeEach(() => { mockDefaultStorage.setItem.mockClear(); mockNotifyEventListeners.mockClear(); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts index 566792db9ca..455322703d9 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts @@ -1,7 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { identifyUser } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import { + identifyUser, + initializeInAppMessaging, +} from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; import { resolveCredentials, resolveConfig, @@ -34,6 +37,7 @@ describe('InAppMessaging Pinpoint Provider API: identifyUser', () => { const mockResolveCredentials = resolveCredentials as jest.Mock; beforeAll(() => { + initializeInAppMessaging(); mockgetInAppMessagingUserAgentString.mockReturnValue(userAgentValue); mockResolveConfig.mockReturnValue(config); mockResolveCredentials.mockResolvedValue(credentials); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts new file mode 100644 index 00000000000..ecd31923309 --- /dev/null +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Hub } from '@aws-amplify/core'; +import { + notifyEventListeners, + addEventListener, +} from '../../../../../src/common'; +import { initializeInAppMessaging } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import SessionTracker from '../../../../../src/inAppMessaging/sessionTracker'; + +jest.mock('@aws-amplify/core'); +jest.mock('@aws-amplify/core/internals/utils'); +jest.mock('../../../../../src/common/eventListeners'); +jest.mock('../../../../../src/inAppMessaging/sessionTracker', () => { + return jest.fn().mockImplementation(() => { + return { start: jest.fn() }; + }); +}); + +const mockNotifyEventListeners = notifyEventListeners as jest.Mock; +const mockAddEventListener = addEventListener as jest.Mock; + +describe('initializeInAppMessaging', () => { + beforeEach(() => { + mockNotifyEventListeners.mockClear(); + }); + it('will intialize session tracking, analytics listeners and in-app events listeners', async () => { + initializeInAppMessaging(); + + expect(SessionTracker).toHaveBeenCalledTimes(1); + expect(mockAddEventListener).toHaveBeenNthCalledWith( + 1, + 'messageDisplayed', + expect.any(Function) + ); + expect(mockAddEventListener).toHaveBeenNthCalledWith( + 2, + 'messageDismissed', + expect.any(Function) + ); + expect(mockAddEventListener).toHaveBeenNthCalledWith( + 3, + 'messageActionTaken', + expect.any(Function) + ); + expect(Hub.listen).toHaveBeenCalledWith('analytics', expect.any(Function)); + }); +}); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts index c6b62f42cab..989584925e2 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts @@ -7,6 +7,7 @@ import { addEventListener, } from '../../../../../src/common'; import { + initializeInAppMessaging, notifyMessageInteraction, onMessageActionTaken, onMessageDismissed, @@ -21,6 +22,9 @@ const mockAddEventListener = addEventListener as jest.Mock; describe('Interaction events', () => { const handler = jest.fn(); + beforeAll(() => { + initializeInAppMessaging(); + }); it('can be listened to by onMessageReceived', () => { onMessageReceived(handler); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts index 65fa9e1dafb..a2371b6ea66 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts @@ -4,6 +4,7 @@ import { defaultStorage } from '@aws-amplify/core'; import { dispatchEvent, + initializeInAppMessaging, setConflictHandler, } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; import { processInAppMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; @@ -24,6 +25,9 @@ const mockNotifyEventListeners = notifyEventListeners as jest.Mock; const mockProcessInAppMessages = processInAppMessages as jest.Mock; describe('setConflictHandler', () => { + beforeAll(() => { + initializeInAppMessaging(); + }); beforeEach(() => { mockDefaultStorage.setItem.mockClear(); mockNotifyEventListeners.mockClear(); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts index c3b058de5a3..957f8da9a22 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { defaultStorage } from '@aws-amplify/core'; -import { syncMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import { + initializeInAppMessaging, + syncMessages, +} from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; import { STORAGE_KEY_SUFFIX, resolveCredentials, @@ -53,6 +56,7 @@ const mockedEmptyMessages = { describe('syncMessages', () => { beforeAll(() => { + initializeInAppMessaging(); mockGetInAppMessagingUserAgentString.mockReturnValue(userAgentValue); mockResolveConfig.mockReturnValue(config); mockResolveCredentials.mockResolvedValue(credentials); diff --git a/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts b/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts index 7d43a650050..ff0412c34ee 100644 --- a/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts @@ -5,7 +5,7 @@ import { pinpointInAppMessage, simpleInAppMessagingEvent, } from '../../../__mocks__/data'; -import { processInAppMessages } from '../../../src/inAppMessaging/providers/pinpoint/utils/processInAppMessages'; +import { processInAppMessages } from '../../../src/inAppMessaging/providers/pinpoint/utils'; import { cloneDeep } from 'lodash'; import { isBeforeEndDate, @@ -13,6 +13,7 @@ import { matchesEventType, matchesMetrics, } from '../../../src/inAppMessaging/providers/pinpoint/utils/helpers'; +import { initializeInAppMessaging } from '../../../src/inAppMessaging/providers/pinpoint/apis'; jest.mock('@aws-amplify/core'); jest.mock('@aws-amplify/core/internals/utils'); @@ -31,6 +32,9 @@ describe('processInAppMessages', () => { { ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-3', Priority: 1 }, { ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-4', Priority: 2 }, ]; + beforeAll(() => { + initializeInAppMessaging(); + }); beforeEach(() => { mockMatchesEventType.mockReturnValue(true); mockMatchesAttributes.mockReturnValue(true); diff --git a/packages/notifications/src/inAppMessaging/errors/validation.ts b/packages/notifications/src/inAppMessaging/errors/validation.ts index 90bf3d57cf8..dc696257e65 100644 --- a/packages/notifications/src/inAppMessaging/errors/validation.ts +++ b/packages/notifications/src/inAppMessaging/errors/validation.ts @@ -8,6 +8,7 @@ export enum InAppMessagingValidationErrorCode { NoCredentials = 'NoCredentials', NoRegion = 'NoRegion', NoEndpointId = 'NoEndpointId', + NotInitialized = 'NotInitialized', } export const validationErrorMap: AmplifyErrorMap = @@ -24,4 +25,9 @@ export const validationErrorMap: AmplifyErrorMap { + assertIsInitialized(); try { const key = `${PINPOINT_KEY_PREFIX}${STORAGE_KEY_SUFFIX}`; const cachedMessages = await defaultStorage.getItem(key); @@ -46,7 +50,6 @@ export async function dispatchEvent(input: DispatchEventInput): Promise { input ); const flattenedMessages = flatten(messages); - if (flattenedMessages.length > 0) { notifyEventListeners( 'messageReceived', diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts index 168bcc4065c..e899e9e987f 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts @@ -12,6 +12,7 @@ import { resolveCredentials, } from '../utils'; import { IdentifyUserInput } from '../types'; +import { assertIsInitialized } from '../../../utils'; /** * Sends information about a user to Pinpoint. Sending user information allows you to associate a user to their user @@ -22,7 +23,7 @@ import { IdentifyUserInput } from '../types'; * API. * @throws service: {@link UpdateEndpointException} - Thrown when the underlying Pinpoint service returns an error. * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library - * configuration is incorrect. + * configuration is incorrect, or if In App messaging hasn't been initialized. * @returns A promise that will resolve when the operation is complete. * @example * ```ts @@ -69,6 +70,7 @@ export const identifyUser = async ({ userProfile, options, }: IdentifyUserInput): Promise => { + assertIsInitialized(); const { credentials, identityId } = await resolveCredentials(); const { appId, region } = resolveConfig(); const { address, optOut, userAttributes } = options?.serviceOptions ?? {}; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts index 46811a85823..efbab938f67 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/index.ts @@ -5,6 +5,7 @@ export { identifyUser } from './identifyUser'; export { syncMessages } from './syncMessages'; export { dispatchEvent } from './dispatchEvent'; export { setConflictHandler } from './setConflictHandler'; +export { initializeInAppMessaging } from './initializeInAppMessaging'; export { onMessageReceived } from './onMessageReceived'; export { onMessageDismissed } from './onMessageDismissed'; export { onMessageDisplayed } from './onMessageDisplayed'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts new file mode 100644 index 00000000000..ac34ac5efa8 --- /dev/null +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import SessionTracker from '../../../sessionTracker'; +import { InAppMessage, InAppMessagingEvent } from '../../../types'; +import { addEventListener } from '../../../../common'; +import { recordAnalyticsEvent } from '../utils/helpers'; +import { PinpointMessageEvent } from '../types'; +import { Hub, HubCapsule } from '@aws-amplify/core'; +import { dispatchEvent } from './dispatchEvent'; +import { incrementMessageCounts, sessionStateChangeHandler } from '../utils'; +import { isInitialized, initialize } from '../../../utils'; + +/** + * Initialize and set up in-app messaging category. This API needs to be called to enable other InAppMessaging APIs. + * + * @remarks + * Make sure to call this early in your app at the root entry point after configuring Amplify. + * @example + * ```ts + * Amplify.configure(config); + * initializeInAppMessaging(); + * ``` + */ +export function initializeInAppMessaging(): void { + if (isInitialized()) { + return; + } + // set up the session tracker and start it + const sessionTracker = new SessionTracker(sessionStateChangeHandler); + sessionTracker.start(); + + // wire up default Pinpoint message event handling + addEventListener('messageDisplayed', (message: InAppMessage) => { + console.log('Recording message displayed event'); + recordAnalyticsEvent(PinpointMessageEvent.MESSAGE_DISPLAYED, message); + incrementMessageCounts(message.id); + }); + addEventListener('messageDismissed', (message: InAppMessage) => { + recordAnalyticsEvent(PinpointMessageEvent.MESSAGE_DISMISSED, message); + }); + addEventListener('messageActionTaken', (message: InAppMessage) => { + recordAnalyticsEvent(PinpointMessageEvent.MESSAGE_ACTION_TAKEN, message); + }); + + // listen to analytics hub events + Hub.listen('analytics', analyticsListener); + + initialize(); +} + +function analyticsListener({ + payload, +}: HubCapsule) { + const { event, data } = payload; + switch (event) { + case 'record': { + dispatchEvent(data); + break; + } + default: + break; + } +} diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts index ca37112d186..ec9a628ca7d 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/notifyMessageInteraction.ts @@ -2,12 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import { notifyEventListeners } from '../../../../common'; +import { assertIsInitialized } from '../../../utils'; import { NotifyMessageInteractionInput } from '../types/inputs'; /** * Notifies the respective listener of the specified type with the message given. * * @param {NotifyMessageInteractionInput} input - The input object that holds the type and message. + * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect, or if In App messaging hasn't been initialized. * @example * ```ts * onMessageRecieved((message) => { @@ -20,5 +23,6 @@ export function notifyMessageInteraction({ type, message, }: NotifyMessageInteractionInput): void { + assertIsInitialized(); notifyEventListeners(type, message); } diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts index 59af1ab308e..3bf37f46e3c 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageActionTaken.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { addEventListener } from '../../../../common'; +import { assertIsInitialized } from '../../../utils'; import { OnMessageActionTakenInput } from '../types/inputs'; import { OnMessageActionTakenOutput } from '../types/outputs'; @@ -9,6 +10,8 @@ import { OnMessageActionTakenOutput } from '../types/outputs'; * Registers a callback that will be invoked on `messageActionTaken` events. * * @param {OnMessageActionTakenInput} input - The input object that holds the callback handler. + * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect, or if In App messaging hasn't been initialized. * @returns {OnMessageActionTakenOutput} - An object that holds a remove method to stop listening to events. * @example * ```ts @@ -21,5 +24,6 @@ import { OnMessageActionTakenOutput } from '../types/outputs'; export function onMessageActionTaken( input: OnMessageActionTakenInput ): OnMessageActionTakenOutput { + assertIsInitialized(); return addEventListener('messageActionTaken', input); } diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts index 486925b5514..696d44fd4a9 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDismissed.ts @@ -4,11 +4,14 @@ import { addEventListener } from '../../../../common'; import { OnMessageDismissedOutput } from '../types/outputs'; import { OnMessageDismissedInput } from '../types/inputs'; +import { assertIsInitialized } from '../../../utils'; /** * Registers a callback that will be invoked on `messageDismissed` events. * * @param {OnMessageDismissedInput} input - The input object that holds the callback handler. + * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect, or if In App messaging hasn't been initialized. * @returns {OnMessageDismissedOutput} - An object that holds a remove method to stop listening to events. * @example * ```ts @@ -21,5 +24,6 @@ import { OnMessageDismissedInput } from '../types/inputs'; export function onMessageDismissed( input: OnMessageDismissedInput ): OnMessageDismissedOutput { + assertIsInitialized(); return addEventListener('messageDismissed', input); } diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts index f269c4c968f..41386a00c26 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageDisplayed.ts @@ -4,11 +4,14 @@ import { addEventListener } from '../../../../common'; import { OnMessageDisplayedOutput } from '../types/outputs'; import { OnMessageDisplayedInput } from '../types/inputs'; +import { assertIsInitialized } from '../../../utils'; /** * Registers a callback that will be invoked on `messageDisplayed` events. * * @param {OnMessageDisplayedInput} input - The input object that holds the callback handler. + * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect, or if In App messaging hasn't been initialized. * @returns {OnMessageDismissedOutput} - An object that holds a remove method to stop listening to events. * @example * ```ts @@ -21,5 +24,6 @@ import { OnMessageDisplayedInput } from '../types/inputs'; export function onMessageDisplayed( input: OnMessageDisplayedInput ): OnMessageDisplayedOutput { + assertIsInitialized(); return addEventListener('messageDisplayed', input); } diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts index a655cf6675d..4180b1a7b4c 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/onMessageReceived.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { addEventListener } from '../../../../common'; +import { assertIsInitialized } from '../../../utils'; import { OnMessageReceivedInput } from '../types/inputs'; import { OnMessageReceivedOutput } from '../types/outputs'; @@ -9,6 +10,8 @@ import { OnMessageReceivedOutput } from '../types/outputs'; * Registers a callback that will be invoked on `messageReceived` events. * * @param {OnMessageReceivedInput} input - The input object that holds the callback handler. + * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect, or if In App messaging hasn't been initialized. * @returns {OnMessageReceivedOutput} - An object that holds a remove method to stop listening to events. * @example * ```ts @@ -21,5 +24,6 @@ import { OnMessageReceivedOutput } from '../types/outputs'; export function onMessageReceived( input: OnMessageReceivedInput ): OnMessageReceivedOutput { + assertIsInitialized(); return addEventListener('messageReceived', input); } diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts index 9aab53f6771..58333a11c9d 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { InAppMessage } from '../../../types'; +import { assertIsInitialized } from '../../../utils'; import { InAppMessageConflictHandler, SetConflictHandlerInput } from '../types'; export let conflictHandler: InAppMessageConflictHandler = @@ -14,7 +15,8 @@ export let conflictHandler: InAppMessageConflictHandler = * @remark * The conflict handler is not persisted across app restarts and so must be set again before dispatching an event for * any custom handling to take effect. - * + * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect, or if In App messaging hasn't been initialized. * @param SetConflictHandlerInput: The input object that holds the conflict handler to be used. * @example * ```ts @@ -36,6 +38,7 @@ export let conflictHandler: InAppMessageConflictHandler = * ``` */ export function setConflictHandler(input: SetConflictHandlerInput): void { + assertIsInitialized(); conflictHandler = input; } diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts index 4747238f97d..20036b72e89 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts @@ -26,6 +26,7 @@ import { assertServiceError, assertValidationError, } from '../../../errors'; +import { assertIsInitialized } from '../../../utils'; /** * Fetch and persist messages from Pinpoint campaigns. @@ -33,7 +34,7 @@ import { * * @throws service exceptions - Thrown when the underlying Pinpoint service returns an error. * @throws validation: {@link InAppMessagingValidationErrorCode} - Thrown when the provided parameters or library - * configuration is incorrect. + * configuration is incorrect, or if In App messaging hasn't been initialized. * @returns A promise that will resolve when the operation is complete. * @example * ```ts @@ -43,6 +44,7 @@ import { * ``` */ export async function syncMessages(): Promise { + assertIsInitialized(); const messages = await fetchInAppMessages(); if (!messages || messages.length === 0) { return; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts index 087413bfa27..789b8e1ae1c 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/index.ts @@ -6,6 +6,7 @@ export { syncMessages, dispatchEvent, setConflictHandler, + initializeInAppMessaging, onMessageReceived, onMessageDisplayed, onMessageDismissed, diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts index a32353ea53a..1d4edfb6551 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts @@ -1,11 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Hub } from '@aws-amplify/core'; import { ConsoleLogger, InAppMessagingAction, - AMPLIFY_SYMBOL, } from '@aws-amplify/core/internals/utils'; import type { InAppMessageCampaign as PinpointInAppMessage } from '@aws-amplify/core/internals/aws-clients/pinpoint'; import isEmpty from 'lodash/isEmpty'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts index d27d53e161f..2a41e79a9b0 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/index.ts @@ -11,4 +11,8 @@ export { STORAGE_KEY_SUFFIX, } from './constants'; -export { processInAppMessages } from './processInAppMessages'; +export { + processInAppMessages, + sessionStateChangeHandler, + incrementMessageCounts, +} from './messageProcessingHelpers'; diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/processInAppMessages.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/messageProcessingHelpers.ts similarity index 72% rename from packages/notifications/src/inAppMessaging/providers/pinpoint/utils/processInAppMessages.ts rename to packages/notifications/src/inAppMessaging/providers/pinpoint/utils/messageProcessingHelpers.ts index 1207bcbede9..287e2048b4d 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/processInAppMessages.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/messageProcessingHelpers.ts @@ -19,12 +19,13 @@ import { import type { InAppMessageCampaign as PinpointInAppMessage } from '@aws-amplify/core/internals/aws-clients/pinpoint'; import { ConsoleLogger } from '@aws-amplify/core/internals/utils'; import { defaultStorage } from '@aws-amplify/core'; +import { SessionState } from '../../../sessionTracker'; const MESSAGE_DAILY_COUNT_KEY = 'pinpointProvider_inAppMessages_dailyCount'; const MESSAGE_TOTAL_COUNT_KEY = 'pinpointProvider_inAppMessages_totalCount'; const logger = new ConsoleLogger('InAppMessaging.processInAppMessages'); -const sessionMessageCountMap: InAppMessageCountMap = {}; +let sessionMessageCountMap: InAppMessageCountMap = {}; export async function processInAppMessages( messages: PinpointInAppMessage[], @@ -68,6 +69,23 @@ export async function processInAppMessages( return normalizeMessages(acc); } +export function sessionStateChangeHandler(state: SessionState): void { + if (state === 'started') { + console.log('Resetting the count'); + // reset all session counts + sessionMessageCountMap = {}; + } +} + +export async function incrementMessageCounts(messageId: string): Promise { + const { sessionCount, dailyCount, totalCount } = await getMessageCounts( + messageId + ); + setSessionCount(messageId, sessionCount + 1); + setDailyCount(dailyCount + 1); + await setTotalCount(messageId, totalCount + 1); +} + function normalizeMessages(messages: PinpointInAppMessage[]): InAppMessage[] { return messages.map(message => { const { CampaignId, InAppMessage } = message; @@ -89,10 +107,11 @@ async function isBelowCap({ const { sessionCount, dailyCount, totalCount } = await getMessageCounts( CampaignId ); + return ( - (!SessionCap ?? sessionCount < SessionCap) && - (!DailyCap ?? dailyCount < DailyCap) && - (!TotalCap ?? totalCount < TotalCap) + (!SessionCap || sessionCount < SessionCap) && + (!DailyCap || dailyCount < DailyCap) && + (!TotalCap || totalCount < TotalCap) ); } @@ -111,7 +130,40 @@ async function getMessageCounts( } function getSessionCount(messageId: string): number { - return sessionMessageCountMap[messageId] || 0; + return sessionMessageCountMap[messageId] ?? 0; +} + +function setSessionCount(messageId: string, count: number): void { + sessionMessageCountMap[messageId] = count; +} + +function setDailyCount(count: number): void { + const dailyCount: DailyInAppMessageCounter = { + count, + lastCountTimestamp: getStartOfDay(), + }; + try { + defaultStorage.setItem(MESSAGE_DAILY_COUNT_KEY, JSON.stringify(dailyCount)); + } catch (err) { + logger.error('Failed to save daily message count to storage', err); + } +} + +function setTotalCountMap(countMap: InAppMessageCountMap): void { + try { + defaultStorage.setItem(MESSAGE_TOTAL_COUNT_KEY, JSON.stringify(countMap)); + } catch (err) { + logger.error('Failed to save total count to storage', err); + } +} + +async function setTotalCount(messageId: string, count: number): Promise { + const totalCountMap = await getTotalCountMap(); + const updatedMap = { + ...totalCountMap, + [messageId]: count, + }; + setTotalCountMap(updatedMap); } async function getDailyCount(): Promise { diff --git a/packages/notifications/src/inAppMessaging/utils/index.ts b/packages/notifications/src/inAppMessaging/utils/index.ts new file mode 100644 index 00000000000..9a148c1f816 --- /dev/null +++ b/packages/notifications/src/inAppMessaging/utils/index.ts @@ -0,0 +1,8 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { + isInitialized, + assertIsInitialized, + initialize, +} from './statusHelpers'; diff --git a/packages/notifications/src/inAppMessaging/utils/statusHelpers.ts b/packages/notifications/src/inAppMessaging/utils/statusHelpers.ts new file mode 100644 index 00000000000..8f44c11dec6 --- /dev/null +++ b/packages/notifications/src/inAppMessaging/utils/statusHelpers.ts @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + InAppMessagingValidationErrorCode, + assertValidationError, +} from '../errors'; + +let initialized = false; + +/** + * Sets initialization status to true. + * + * @internal + */ +export const initialize = () => { + initialized = true; +}; + +/** + * Returns the initialization status of In-App Messaging. + * + * @internal + */ +export const isInitialized = () => initialized; + +export function assertIsInitialized() { + assertValidationError( + isInitialized(), + InAppMessagingValidationErrorCode.NotInitialized + ); +} diff --git a/yarn.lock b/yarn.lock index e52d50e4242..4c4d7fb08b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15082,7 +15082,7 @@ uuid@8.3.2, uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^3.2.1, uuid@^3.3.2: +uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==