From 32990ee997ca61dc9d1b16214616c1ad177a1cbe Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Mon, 11 Mar 2024 09:51:01 -0700 Subject: [PATCH 01/33] Feat: DownloadData refactor (#13073) * updating type info * update downloadData implementation * fix output type * path dynamic construction * resolve merge conflit * add unit tests for path * update bundle size * update deprecation warning * update test * misc changes * address comments --- packages/aws-amplify/package.json | 2 +- .../providers/s3/apis/downloadData.test.ts | 173 ++++++++++++++++-- .../validateStorageOperationInput.test.ts | 53 ++++++ .../storage/src/errors/types/validation.ts | 4 + .../src/providers/s3/apis/downloadData.ts | 148 ++++++++++----- .../uploadData/multipart/uploadHandlers.ts | 2 +- .../s3/apis/uploadData/putObjectJob.ts | 2 +- .../storage/src/providers/s3/types/index.ts | 7 +- .../storage/src/providers/s3/types/inputs.ts | 18 +- .../storage/src/providers/s3/types/options.ts | 10 +- .../storage/src/providers/s3/types/outputs.ts | 33 +++- .../src/providers/s3/utils/constants.ts | 3 + .../storage/src/providers/s3/utils/index.ts | 1 + .../s3/utils/resolveS3ConfigAndInput.ts | 2 + .../s3/utils/validateStorageOperationInput.ts | 40 ++++ packages/storage/src/types/index.ts | 5 +- packages/storage/src/types/inputs.ts | 23 ++- packages/storage/src/types/options.ts | 1 + packages/storage/src/types/outputs.ts | 31 +++- 19 files changed, 465 insertions(+), 93 deletions(-) create mode 100644 packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts create mode 100644 packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index ef74d972717..b4f4be3f1b7 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -466,7 +466,7 @@ "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "14.00 kB" + "limit": "14.10 kB" }, { "name": "[Storage] getProperties (S3)", diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 62f1704398b..88e9cb21eb7 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -5,8 +5,18 @@ 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'; -import { createDownloadTask } from '../../../../src/providers/s3/utils'; -import { DownloadDataOptions } from '../../../../src/providers/s3/types'; +import { + createDownloadTask, + validateStorageOperationInput, +} from '../../../../src/providers/s3/utils'; +import { + DownloadDataOptionsKey, + DownloadDataOptionsPath, +} from '../../../../src/providers/s3/types'; +import { + STORAGE_INPUT_KEY, + STORAGE_INPUT_PATH, +} from '../../../../src/providers/s3/utils/constants'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('../../../../src/providers/s3/utils'); @@ -34,9 +44,10 @@ const defaultIdentityId = 'defaultIdentityId'; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockCreateDownloadTask = createDownloadTask as jest.Mock; +const mockValidateStorageInput = validateStorageOperationInput as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; -describe('downloadData', () => { +describe('downloadData with key', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -52,16 +63,20 @@ describe('downloadData', () => { }); }); mockCreateDownloadTask.mockReturnValue('downloadTask'); + mockValidateStorageInput.mockReturnValue({ + inputType: STORAGE_INPUT_KEY, + objectKey: key, + }); beforeEach(() => { jest.clearAllMocks(); }); - it('should return a download task', async () => { + it('should return a download task with key', async () => { expect(downloadData({ key: 'key' })).toBe('downloadTask'); }); - [ + test.each([ { expectedKey: `public/${key}`, }, @@ -81,14 +96,9 @@ describe('downloadData', () => { options: { accessLevel: 'protected', targetIdentityId }, expectedKey: `protected/${targetIdentityId}/${key}`, }, - ].forEach(({ options, expectedKey }) => { - const accessLevelMsg = options?.accessLevel ?? 'default'; - const targetIdentityIdMsg = options?.targetIdentityId - ? `and targetIdentityId` - : ''; - - it(`should supply the correct parameters to getObject API handler with ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { - expect.assertions(2); + ])( + 'should supply the correct parameters to getObject API handler with $expectedKey accessLevel', + async ({ options, expectedKey }) => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const onProgress = jest.fn(); downloadData({ @@ -97,7 +107,7 @@ describe('downloadData', () => { ...options, useAccelerateEndpoint: true, onProgress, - } as DownloadDataOptions, + } as DownloadDataOptionsKey, }); const job = mockCreateDownloadTask.mock.calls[0][0].job; await job(); @@ -116,11 +126,10 @@ describe('downloadData', () => { Key: expectedKey, }, ); - }); - }); + }, + ); - it('should assign the getObject API handler response to the result', async () => { - expect.assertions(2); + it('should assign the getObject API handler response to the result with key', async () => { const lastModified = 'lastModified'; const contentLength = 'contentLength'; const eTag = 'eTag'; @@ -177,3 +186,131 @@ describe('downloadData', () => { ); }); }); + +describe('downloadData with path', () => { + beforeAll(() => { + mockFetchAuthSession.mockResolvedValue({ + credentials, + identityId: defaultIdentityId, + }); + mockGetConfig.mockReturnValue({ + Storage: { + S3: { + bucket, + region, + }, + }, + }); + mockCreateDownloadTask.mockReturnValue('downloadTask'); + mockValidateStorageInput.mockReturnValue({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'path', + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return a download task with path', async () => { + expect(downloadData({ path: 'path' })).toBe('downloadTask'); + }); + + test.each([ + { + path: 'path', + expectedKey: 'path', + }, + { + path: () => 'path', + expectedKey: 'path', + }, + ])( + 'should call getObject API with $expectedKey when path provided is $path', + async ({ path, expectedKey }) => { + (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); + const onProgress = jest.fn(); + downloadData({ + path: path, + options: { + useAccelerateEndpoint: true, + onProgress, + } as DownloadDataOptionsPath, + }); + const job = mockCreateDownloadTask.mock.calls[0][0].job; + await job(); + expect(getObject).toHaveBeenCalledTimes(1); + expect(getObject).toHaveBeenCalledWith( + { + credentials, + region, + useAccelerateEndpoint: true, + onDownloadProgress: onProgress, + abortSignal: expect.any(AbortSignal), + userAgentValue: expect.any(String), + }, + { + Bucket: bucket, + Key: expectedKey, + }, + ); + }, + ); + + it('should assign the getObject API handler response to the result with path', async () => { + const lastModified = 'lastModified'; + const contentLength = 'contentLength'; + const eTag = 'eTag'; + const metadata = 'metadata'; + const versionId = 'versionId'; + const contentType = 'contentType'; + const body = 'body'; + const path = 'path'; + (getObject as jest.Mock).mockResolvedValueOnce({ + Body: body, + LastModified: lastModified, + ContentLength: contentLength, + ETag: eTag, + Metadata: metadata, + VersionId: versionId, + ContentType: contentType, + }); + downloadData({ path }); + const job = mockCreateDownloadTask.mock.calls[0][0].job; + const result = await job(); + expect(getObject).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + path, + body, + lastModified, + size: contentLength, + eTag, + metadata, + versionId, + contentType, + }); + }); + + it('should forward the bytes range option to the getObject API', async () => { + const start = 1; + const end = 100; + (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); + + downloadData({ + path: 'mockPath', + options: { + bytesRange: { start, end }, + }, + }); + + const job = mockCreateDownloadTask.mock.calls[0][0].job; + await job(); + + expect(getObject).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + Range: `bytes=${start}-${end}`, + }), + ); + }); +}); diff --git a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts new file mode 100644 index 00000000000..83388bd3457 --- /dev/null +++ b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + StorageValidationErrorCode, + validationErrorMap, +} from '../../../../../src/errors/types/validation'; +import { validateStorageOperationInput } from '../../../../../src/providers/s3/utils'; +import { + STORAGE_INPUT_KEY, + STORAGE_INPUT_PATH, +} from '../../../../../src/providers/s3/utils/constants'; + +describe('validateStorageOperationInput', () => { + it('should return inputType as STORAGE_INPUT_PATH and objectKey as testPath when input is path as string', () => { + const input = { path: 'testPath' }; + const result = validateStorageOperationInput(input); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'testPath', + }); + }); + + it('should return inputType as STORAGE_INPUT_PATH and objectKey as result of path function when input is path as function', () => { + const input = { + path: ({ identityId }: { identityId?: string }) => + `testPath/${identityId}`, + }; + const result = validateStorageOperationInput(input, '123'); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'testPath/123', + }); + }); + + it('should return inputType as STORAGE_INPUT_KEY and objectKey as testKey when input is key', () => { + const input = { key: 'testKey' }; + const result = validateStorageOperationInput(input); + expect(result).toEqual({ + inputType: STORAGE_INPUT_KEY, + objectKey: 'testKey', + }); + }); + + it('should throw an error when input is invalid', () => { + const input = { invalid: 'test' } as any; + expect(() => validateStorageOperationInput(input)).toThrow( + validationErrorMap[ + StorageValidationErrorCode.InvalidStorageOperationInput + ].message, + ); + }); +}); diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index f596b8d8e25..d63e3d21b34 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -14,6 +14,7 @@ export enum StorageValidationErrorCode { UrlExpirationMaxLimitExceed = 'UrlExpirationMaxLimitExceed', ObjectIsTooLarge = 'ObjectIsTooLarge', InvalidUploadSource = 'InvalidUploadSource', + InvalidStorageOperationInput = 'InvalidStorageOperationInput', } export const validationErrorMap: AmplifyErrorMap = { @@ -49,4 +50,7 @@ export const validationErrorMap: AmplifyErrorMap = { message: 'Upload source type can only be a `Blob`, `File`, `ArrayBuffer`, or `string`.', }, + [StorageValidationErrorCode.InvalidStorageOperationInput]: { + message: 'Missing path or key parameter in Input', + }, }; diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index b10fc35d8ea..68f335d0843 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -4,45 +4,93 @@ import { Amplify } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { DownloadDataInput, DownloadDataOutput, S3Exception } from '../types'; +import { + DownloadDataInput, + DownloadDataInputKey, + DownloadDataInputPath, + DownloadDataOutput, + DownloadDataOutputKey, + DownloadDataOutputPath, +} from '../types'; import { resolveS3ConfigAndInput } from '../utils/resolveS3ConfigAndInput'; -import { StorageValidationErrorCode } from '../../../errors/types/validation'; -import { createDownloadTask } from '../utils'; +import { createDownloadTask, validateStorageOperationInput } from '../utils'; import { getObject } from '../utils/client'; import { getStorageUserAgentValue } from '../utils/userAgent'; import { logger } from '../../../utils'; +import { + StorageDownloadDataOutput, + StorageItemKey, + StorageItemPath, +} from '../../../types'; +import { STORAGE_INPUT_KEY } from '../utils/constants'; -/** - * Download S3 object data to memory - * - * @param input - The DownloadDataInput object. - * @returns A cancelable task exposing result promise from `result` property. - * @throws service: {@link S3Exception} - thrown when checking for existence of the object - * @throws validation: {@link StorageValidationErrorCode } - Validation errors - * - * @example - * ```ts - * // Download a file from s3 bucket - * const { body, eTag } = await downloadData({ key, data: file, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * @example - * ```ts - * // Cancel a task - * const downloadTask = downloadData({ key, data: file }); - * //... - * downloadTask.cancel(); - * try { - * await downloadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - */ -export const downloadData = (input: DownloadDataInput): DownloadDataOutput => { +interface DownloadData { + /** + * Download S3 object data to memory + * + * @param input - The DownloadDataInputPath object. + * @returns A cancelable task exposing result promise from `result` property. + * + * @example + * ```ts + * // Download a file from s3 bucket + * const { body, eTag } = await downloadData({ path, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * @example + * ```ts + * // Cancel a task + * const downloadTask = downloadData({ path }); + * //... + * downloadTask.cancel(); + * try { + * await downloadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + */ + (input: DownloadDataInputPath): DownloadDataOutputPath; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and will be removed in next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. + * + * Download S3 object data to memory + * + * @param input - The DownloadDataInputKey object. + * @returns A cancelable task exposing result promise from `result` property. + * + * @example + * ```ts + * // Download a file from s3 bucket + * const { body, eTag } = await downloadData({ key, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * @example + * ```ts + * // Cancel a task + * const downloadTask = downloadData({ key }); + * //... + * downloadTask.cancel(); + * try { + * await downloadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + */ + (input: DownloadDataInputKey): DownloadDataOutputKey; +} + +export const downloadData: DownloadData = ( + input: DownloadDataInput, +): Output => { const abortController = new AbortController(); const downloadTask = createDownloadTask({ @@ -52,22 +100,25 @@ export const downloadData = (input: DownloadDataInput): DownloadDataOutput => { }, }); - return downloadTask; + return downloadTask as Output; }; const downloadDataJob = - ( - { options: downloadDataOptions, key }: DownloadDataInput, - abortSignal: AbortSignal, - ) => - async () => { - const { bucket, keyPrefix, s3Config } = await resolveS3ConfigAndInput( - Amplify, - downloadDataOptions, + (downloadDataInput: DownloadDataInput, abortSignal: AbortSignal) => + async (): Promise< + StorageDownloadDataOutput + > => { + const { options: downloadDataOptions } = downloadDataInput; + const { bucket, keyPrefix, s3Config, identityId } = + await resolveS3ConfigAndInput(Amplify, downloadDataOptions); + const { inputType, objectKey } = validateStorageOperationInput( + downloadDataInput, + identityId, ); - const finalKey = keyPrefix + key; + const finalKey = + inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey; - logger.debug(`download ${key} from ${finalKey}.`); + logger.debug(`download ${objectKey} from ${finalKey}.`); const { Body: body, @@ -93,8 +144,7 @@ const downloadDataJob = }, ); - return { - key, + const result = { body, lastModified, size, @@ -103,4 +153,8 @@ const downloadDataJob = metadata, versionId, }; + + return inputType === STORAGE_INPUT_KEY + ? { key: objectKey, ...result } + : { path: finalKey, ...result }; }; 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 587ee33c434..7aa62f08390 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts @@ -6,7 +6,7 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { UploadDataInput } from '../../../types'; import { resolveS3ConfigAndInput } from '../../../utils'; -import { Item as S3Item } from '../../../types/outputs'; +import { ItemKey as S3Item } from '../../../types/outputs'; import { DEFAULT_ACCESS_LEVEL, DEFAULT_QUEUE_SIZE, diff --git a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts index 5d1a40786ad..30819b854d8 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts @@ -6,7 +6,7 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { UploadDataInput } from '../../types'; import { calculateContentMd5, resolveS3ConfigAndInput } from '../../utils'; -import { Item as S3Item } from '../../types/outputs'; +import { ItemKey as S3Item } from '../../types/outputs'; import { putObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 4366ee48383..773dde39361 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -8,12 +8,15 @@ export { ListAllOptions, ListPaginateOptions, RemoveOptions, - DownloadDataOptions, + DownloadDataOptionsPath, + DownloadDataOptionsKey, CopyDestinationOptions, CopySourceOptions, } from './options'; export { DownloadDataOutput, + DownloadDataOutputKey, + DownloadDataOutputPath, GetUrlOutput, UploadDataOutput, ListOutputItem, @@ -31,6 +34,8 @@ export { ListPaginateInput, RemoveInput, DownloadDataInput, + DownloadDataInputKey, + DownloadDataInputPath, UploadDataInput, } from './inputs'; export { S3Exception } from './errors'; diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index 9a360d0bfe3..3ec41b7d47a 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -1,9 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { StrictUnion } from '@aws-amplify/core/internals/utils'; + import { StorageCopyInput, - StorageDownloadDataInput, + StorageDownloadDataInputKey, + StorageDownloadDataInputPath, StorageGetPropertiesInput, StorageGetUrlInput, StorageListInput, @@ -13,7 +16,8 @@ import { import { CopyDestinationOptions, CopySourceOptions, - DownloadDataOptions, + DownloadDataOptionsKey, + DownloadDataOptionsPath, GetPropertiesOptions, GetUrlOptions, ListAllOptions, @@ -60,7 +64,15 @@ export type RemoveInput = StorageRemoveInput; /** * Input type for S3 downloadData API. */ -export type DownloadDataInput = StorageDownloadDataInput; +export type DownloadDataInput = StrictUnion< + DownloadDataInputKey | DownloadDataInputPath +>; + +/** @deprecated Use {@link DownloadDataInputPath} instead. */ +export type DownloadDataInputKey = + StorageDownloadDataInputKey; +export type DownloadDataInputPath = + StorageDownloadDataInputPath; /** * Input type for S3 uploadData API. diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index dad92d72f1b..93f83eebea4 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -18,11 +18,14 @@ interface CommonOptions { useAccelerateEndpoint?: boolean; } +/** @deprecated This will be removed in next major version. */ type ReadOptions = | { accessLevel?: 'guest' | 'private' } | { accessLevel: 'protected'; targetIdentityId?: string }; +/** @deprecated This will be removed in next major version. */ interface WriteOptions { + /** @deprecated This will be removed in next major version. */ accessLevel?: StorageAccessLevel; } @@ -87,11 +90,14 @@ export type GetUrlOptions = ReadOptions & /** * Input options type for S3 downloadData API. */ -export type DownloadDataOptions = ReadOptions & - CommonOptions & +export type DownloadDataOptions = CommonOptions & TransferOptions & BytesRangeOptions; +/** @deprecated Use {@link DownloadDataOptionsPath} instead. */ +export type DownloadDataOptionsKey = ReadOptions & DownloadDataOptions; +export type DownloadDataOptionsPath = DownloadDataOptions; + export type UploadDataOptions = WriteOptions & CommonOptions & TransferOptions & { diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index bbcb9fc75b1..35b6784af93 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -5,15 +5,16 @@ import { DownloadTask, StorageDownloadDataOutput, StorageGetUrlOutput, - StorageItem, + StorageItemKey, + StorageItemPath, StorageListOutput, UploadTask, } from '../../../types'; /** - * type for S3 item. + * Base type for an S3 item. */ -export interface Item extends StorageItem { +export interface ItemBase { /** * VersionId used to reference a specific version of the object. */ @@ -24,15 +25,29 @@ export interface Item extends StorageItem { contentType?: string; } +/** + * @deprecated Use {@link ItemPath} instead. + */ +export type ItemKey = ItemBase & StorageItemKey; +export type ItemPath = ItemBase & StorageItemPath; + /** * type for S3 list item. */ -export type ListOutputItem = Omit; +export type ListOutputItem = Omit; + +/** @deprecated Use {@link DownloadDataOutputPath} instead. */ +export type DownloadDataOutputKey = DownloadTask< + StorageDownloadDataOutput +>; +export type DownloadDataOutputPath = DownloadTask< + StorageDownloadDataOutput +>; /** * Output type for S3 downloadData API. */ -export type DownloadDataOutput = DownloadTask>; +export type DownloadDataOutput = DownloadDataOutputKey | DownloadDataOutputPath; /** * Output type for S3 getUrl API. @@ -42,12 +57,12 @@ export type GetUrlOutput = StorageGetUrlOutput; /** * Output type for S3 uploadData API. */ -export type UploadDataOutput = UploadTask; +export type UploadDataOutput = UploadTask; /** * Output type for S3 getProperties API. */ -export type GetPropertiesOutput = Item; +export type GetPropertiesOutput = ItemKey; /** * Output type for S3 list API. Lists all bucket objects. @@ -64,9 +79,9 @@ export type ListPaginateOutput = StorageListOutput & { /** * Output type for S3 copy API. */ -export type CopyOutput = Pick; +export type CopyOutput = Pick; /** * Output type for S3 remove API. */ -export type RemoveOutput = Pick; +export type RemoveOutput = Pick; diff --git a/packages/storage/src/providers/s3/utils/constants.ts b/packages/storage/src/providers/s3/utils/constants.ts index 9e48b80047f..0aa063cef4d 100644 --- a/packages/storage/src/providers/s3/utils/constants.ts +++ b/packages/storage/src/providers/s3/utils/constants.ts @@ -19,3 +19,6 @@ export const MAX_PARTS_COUNT = 10000; export const DEFAULT_QUEUE_SIZE = 4; export const UPLOADS_STORAGE_KEY = '__uploadInProgress'; + +export const STORAGE_INPUT_KEY = 'key'; +export const STORAGE_INPUT_PATH = 'path'; diff --git a/packages/storage/src/providers/s3/utils/index.ts b/packages/storage/src/providers/s3/utils/index.ts index fe8ee9db247..a4c451c5105 100644 --- a/packages/storage/src/providers/s3/utils/index.ts +++ b/packages/storage/src/providers/s3/utils/index.ts @@ -4,3 +4,4 @@ export { calculateContentMd5 } from './md5'; export { resolveS3ConfigAndInput } from './resolveS3ConfigAndInput'; export { createDownloadTask, createUploadTask } from './transferTask'; +export { validateStorageOperationInput } from './validateStorageOperationInput'; diff --git a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts index d99149f6c0a..8eb28ef8681 100644 --- a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts +++ b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts @@ -22,6 +22,7 @@ interface ResolvedS3ConfigAndInput { bucket: string; keyPrefix: string; isObjectLockEnabled?: boolean; + identityId?: string; } /** @@ -84,6 +85,7 @@ export const resolveS3ConfigAndInput = async ( }, bucket, keyPrefix, + identityId, isObjectLockEnabled, }; }; diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts new file mode 100644 index 00000000000..038df38f931 --- /dev/null +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { StrictUnion } from '@aws-amplify/core/internals/utils'; + +import { + StorageOperationInputKey, + StorageOperationInputPath, +} from '../../../types/inputs'; +import { assertValidationError } from '../../../errors/utils/assertValidationError'; +import { StorageValidationErrorCode } from '../../../errors/types/validation'; + +import { STORAGE_INPUT_KEY, STORAGE_INPUT_PATH } from './constants'; + +type Input = StrictUnion; + +const isInputWithPath = (input: Input): input is StorageOperationInputPath => { + return input.path !== undefined; +}; + +export const validateStorageOperationInput = ( + input: Input, + identityId?: string, +) => { + assertValidationError( + !!(input as Input).key || !!(input as Input).path, + StorageValidationErrorCode.InvalidStorageOperationInput, + ); + + if (isInputWithPath(input)) { + const { path } = input; + + return { + inputType: STORAGE_INPUT_PATH, + objectKey: typeof path === 'string' ? path : path({ identityId }), + }; + } else { + return { inputType: STORAGE_INPUT_KEY, objectKey: input.key }; + } +}; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 39bb1c049a3..78569a76f36 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -12,7 +12,8 @@ export { StorageListInput, StorageGetPropertiesInput, StorageRemoveInput, - StorageDownloadDataInput, + StorageDownloadDataInputKey, + StorageDownloadDataInputPath, StorageUploadDataInput, StorageCopyInput, StorageGetUrlInput, @@ -26,6 +27,8 @@ export { } from './options'; export { StorageItem, + StorageItemKey, + StorageItemPath, StorageListOutput, StorageDownloadDataOutput, StorageGetUrlOutput, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 861ebf53876..7e1e1ea15dc 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -7,6 +7,26 @@ import { StorageOptions, } from './options'; +/** @deprecated Use {@link StorageOperationInputPath} instead. */ +export interface StorageOperationInputKey { + /** @deprecated Use `path` instead. */ + key: string; +} +export interface StorageOperationInputPath { + path: string | (({ identityId }: { identityId?: string }) => string); +} +export interface StorageOperationOptions { + options?: Options; +} + +/** @deprecated Use {@link StorageDownloadDataInputPath} instead. */ +export type StorageDownloadDataInputKey = + StorageOperationInputKey & StorageOperationOptions; + +export type StorageDownloadDataInputPath = StorageOperationInputPath & + StorageOperationOptions; + +// TODO: This needs to be removed after refactor of all storage APIs export interface StorageOperationInput { key: string; options?: Options; @@ -30,9 +50,6 @@ export interface StorageListInput< export type StorageGetUrlInput = StorageOperationInput; -export type StorageDownloadDataInput = - StorageOperationInput; - export type StorageUploadDataInput = StorageOperationInput & { data: StorageUploadDataPayload; diff --git a/packages/storage/src/types/options.ts b/packages/storage/src/types/options.ts index df992df5838..29bdd1cd43d 100644 --- a/packages/storage/src/types/options.ts +++ b/packages/storage/src/types/options.ts @@ -4,6 +4,7 @@ import { StorageAccessLevel } from '@aws-amplify/core'; export interface StorageOptions { + /** @deprecated This will be removed in next major version. */ accessLevel?: StorageAccessLevel; } diff --git a/packages/storage/src/types/outputs.ts b/packages/storage/src/types/outputs.ts index b5b3a9236c0..0d4307fd73b 100644 --- a/packages/storage/src/types/outputs.ts +++ b/packages/storage/src/types/outputs.ts @@ -3,11 +3,10 @@ import { ResponseBodyMixin } from '@aws-amplify/core/internals/aws-client-utils'; -export interface StorageItem { - /** - * Key of the object - */ - key: string; +/** + * Base type for a storage item. + */ +export interface StorageItemBase { /** * Creation date of the object. */ @@ -28,7 +27,27 @@ export interface StorageItem { metadata?: Record; } -export type StorageDownloadDataOutput = T & { +/** @deprecated Use {@link StorageItemPath} instead. */ +export type StorageItemKey = StorageItemBase & { + /** + * Key of the object. + */ + key: string; +}; + +export type StorageItemPath = StorageItemBase & { + /** + * Path of the object. + */ + path: string; +}; + +/** + * A storage item can be identified either by a key or a path. + */ +export type StorageItem = StorageItemKey | StorageItemPath; + +export type StorageDownloadDataOutput = Item & { body: ResponseBodyMixin; }; From e882c689c779c74ca3d953bd2146b22f7bb2f894 Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:46:48 -0700 Subject: [PATCH 02/33] Chore: Update Storage deprecation warning (#13107) * update deprecation warning per product * include integ test path --- .github/workflows/push-integ-test.yml | 1 + packages/storage/src/errors/types/validation.ts | 2 +- packages/storage/src/providers/s3/apis/downloadData.ts | 6 +++++- packages/storage/src/providers/s3/types/options.ts | 6 +++--- packages/storage/src/types/inputs.ts | 6 +++--- packages/storage/src/types/options.ts | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml index a56ca5db116..0fd20d26555 100644 --- a/.github/workflows/push-integ-test.yml +++ b/.github/workflows/push-integ-test.yml @@ -9,6 +9,7 @@ on: push: branches: - next/main + - gen2-storage jobs: e2e: diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index d63e3d21b34..d0890608feb 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -51,6 +51,6 @@ export const validationErrorMap: AmplifyErrorMap = { 'Upload source type can only be a `Blob`, `File`, `ArrayBuffer`, or `string`.', }, [StorageValidationErrorCode.InvalidStorageOperationInput]: { - message: 'Missing path or key parameter in Input', + message: 'Missing path or key parameter in Input.', }, }; diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index 68f335d0843..e083ada06e8 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -30,6 +30,8 @@ interface DownloadData { * * @param input - The DownloadDataInputPath object. * @returns A cancelable task exposing result promise from `result` property. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors * * @example * ```ts @@ -55,13 +57,15 @@ interface DownloadData { */ (input: DownloadDataInputPath): DownloadDataOutputPath; /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and will be removed in next major version. + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. * * Download S3 object data to memory * * @param input - The DownloadDataInputKey object. * @returns A cancelable task exposing result promise from `result` property. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors * * @example * ```ts diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 93f83eebea4..6356ca81a93 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -18,14 +18,14 @@ interface CommonOptions { useAccelerateEndpoint?: boolean; } -/** @deprecated This will be removed in next major version. */ +/** @deprecated This may be removed in the next major version. */ type ReadOptions = | { accessLevel?: 'guest' | 'private' } | { accessLevel: 'protected'; targetIdentityId?: string }; -/** @deprecated This will be removed in next major version. */ +/** @deprecated This may be removed in the next major version. */ interface WriteOptions { - /** @deprecated This will be removed in next major version. */ + /** @deprecated This may be removed in the next major version. */ accessLevel?: StorageAccessLevel; } diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 7e1e1ea15dc..087792f894b 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -15,16 +15,16 @@ export interface StorageOperationInputKey { export interface StorageOperationInputPath { path: string | (({ identityId }: { identityId?: string }) => string); } -export interface StorageOperationOptions { +export interface StorageOperationOptionsInput { options?: Options; } /** @deprecated Use {@link StorageDownloadDataInputPath} instead. */ export type StorageDownloadDataInputKey = - StorageOperationInputKey & StorageOperationOptions; + StorageOperationInputKey & StorageOperationOptionsInput; export type StorageDownloadDataInputPath = StorageOperationInputPath & - StorageOperationOptions; + StorageOperationOptionsInput; // TODO: This needs to be removed after refactor of all storage APIs export interface StorageOperationInput { diff --git a/packages/storage/src/types/options.ts b/packages/storage/src/types/options.ts index 29bdd1cd43d..b9c74590ba6 100644 --- a/packages/storage/src/types/options.ts +++ b/packages/storage/src/types/options.ts @@ -4,7 +4,7 @@ import { StorageAccessLevel } from '@aws-amplify/core'; export interface StorageOptions { - /** @deprecated This will be removed in next major version. */ + /** @deprecated This may be removed in the next major version. */ accessLevel?: StorageAccessLevel; } From 1a1e0f49f04a06e65cc501976af8cc27f9028279 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Tue, 12 Mar 2024 17:23:42 -0700 Subject: [PATCH 03/33] chore(storage): extract isInputWithPath into separate file (#13114) * fix: extract isInputWithPath into separate file * chore: add unit test * Update packages/storage/__tests__/providers/s3/apis/utils/isInputWithPath.test.ts Co-authored-by: ashika112 <155593080+ashika112@users.noreply.github.com> * Update packages/storage/__tests__/providers/s3/apis/utils/isInputWithPath.test.ts * address feedback --------- Co-authored-by: Ashwin Kumar Co-authored-by: ashika112 <155593080+ashika112@users.noreply.github.com> --- .../s3/apis/utils/isInputWithPath.test.ts | 13 +++++++++++++ packages/storage/src/providers/s3/utils/index.ts | 1 + .../src/providers/s3/utils/isInputWithPath.ts | 13 +++++++++++++ .../s3/utils/validateStorageOperationInput.ts | 14 ++------------ packages/storage/src/types/inputs.ts | 8 ++++++++ 5 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 packages/storage/__tests__/providers/s3/apis/utils/isInputWithPath.test.ts create mode 100644 packages/storage/src/providers/s3/utils/isInputWithPath.ts diff --git a/packages/storage/__tests__/providers/s3/apis/utils/isInputWithPath.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/isInputWithPath.test.ts new file mode 100644 index 00000000000..3a0c96de924 --- /dev/null +++ b/packages/storage/__tests__/providers/s3/apis/utils/isInputWithPath.test.ts @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { isInputWithPath } from '../../../../../src/providers/s3/utils'; + +describe('isInputWithPath', () => { + it('should return true if input contains path', async () => { + expect(isInputWithPath({ path: '' })).toBe(true); + }); + it('should return false if input does not contain path', async () => { + expect(isInputWithPath({ key: '' })).toBe(false); + }); +}); diff --git a/packages/storage/src/providers/s3/utils/index.ts b/packages/storage/src/providers/s3/utils/index.ts index a4c451c5105..1b7584ad08f 100644 --- a/packages/storage/src/providers/s3/utils/index.ts +++ b/packages/storage/src/providers/s3/utils/index.ts @@ -5,3 +5,4 @@ export { calculateContentMd5 } from './md5'; export { resolveS3ConfigAndInput } from './resolveS3ConfigAndInput'; export { createDownloadTask, createUploadTask } from './transferTask'; export { validateStorageOperationInput } from './validateStorageOperationInput'; +export { isInputWithPath } from './isInputWithPath'; diff --git a/packages/storage/src/providers/s3/utils/isInputWithPath.ts b/packages/storage/src/providers/s3/utils/isInputWithPath.ts new file mode 100644 index 00000000000..5c31f924b2c --- /dev/null +++ b/packages/storage/src/providers/s3/utils/isInputWithPath.ts @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + StorageOperationInputPath, + StorageOperationInputType, +} from '../../../types/inputs'; + +export const isInputWithPath = ( + input: StorageOperationInputType, +): input is StorageOperationInputPath => { + return input.path !== undefined; +}; diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts index 038df38f931..a52b5c2ab53 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -1,23 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { StrictUnion } from '@aws-amplify/core/internals/utils'; - -import { - StorageOperationInputKey, - StorageOperationInputPath, -} from '../../../types/inputs'; +import { StorageOperationInputType as Input } from '../../../types/inputs'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { StorageValidationErrorCode } from '../../../errors/types/validation'; +import { isInputWithPath } from './isInputWithPath'; import { STORAGE_INPUT_KEY, STORAGE_INPUT_PATH } from './constants'; -type Input = StrictUnion; - -const isInputWithPath = (input: Input): input is StorageOperationInputPath => { - return input.path !== undefined; -}; - export const validateStorageOperationInput = ( input: Input, identityId?: string, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 087792f894b..0514feaba30 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -1,12 +1,20 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { StrictUnion } from '@aws-amplify/core/internals/utils'; + import { StorageListAllOptions, StorageListPaginateOptions, StorageOptions, } from './options'; +// TODO: rename to StorageOperationInput once the other type with +// the same named is removed +export type StorageOperationInputType = StrictUnion< + StorageOperationInputKey | StorageOperationInputPath +>; + /** @deprecated Use {@link StorageOperationInputPath} instead. */ export interface StorageOperationInputKey { /** @deprecated Use `path` instead. */ From ac873698a1c4c2b855ea61e482e1917b8b0ad771 Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:08:24 -0700 Subject: [PATCH 04/33] Feat: Prefix resolver & Path input validation (#13113) * add prefix resolver deprecation warning * add input validation on path parameter --- packages/aws-amplify/package.json | 2 +- packages/core/src/singleton/Storage/types.ts | 10 ++++++++++ .../utils/validateStorageOperationInput.test.ts | 17 +++++++++++++---- packages/storage/src/errors/types/validation.ts | 10 +++++++--- .../s3/utils/resolveS3ConfigAndInput.ts | 3 +-- .../s3/utils/validateStorageOperationInput.ts | 7 ++++++- 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index b4f4be3f1b7..5b037dd55f8 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -466,7 +466,7 @@ "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "14.10 kB" + "limit": "14.15 kB" }, { "name": "[Storage] getProperties (S3)", diff --git a/packages/core/src/singleton/Storage/types.ts b/packages/core/src/singleton/Storage/types.ts index 3245a008989..b21413a797a 100644 --- a/packages/core/src/singleton/Storage/types.ts +++ b/packages/core/src/singleton/Storage/types.ts @@ -3,6 +3,7 @@ import { AtLeastOne } from '../types'; +/** @deprecated This may be removed in the next major version. */ export type StorageAccessLevel = 'guest' | 'protected' | 'private'; export interface S3ProviderConfig { @@ -20,6 +21,7 @@ export interface S3ProviderConfig { export type StorageConfig = AtLeastOne; +/** @deprecated This may be removed in the next major version. */ type StoragePrefixResolver = (params: { accessLevel: StorageAccessLevel; targetIdentityId?: string; @@ -27,7 +29,15 @@ type StoragePrefixResolver = (params: { export interface LibraryStorageOptions { S3: { + /** + * @deprecated This may be removed in the next major version. + * This is currently used for Storage API signature using key as input parameter. + * */ prefixResolver?: StoragePrefixResolver; + /** + * @deprecated This may be removed in the next major version. + * This is currently used for Storage API signature using key as input parameter. + * */ defaultAccessLevel?: StorageAccessLevel; isObjectLockEnabled?: boolean; }; diff --git a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts index 83388bd3457..684fec06544 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts @@ -13,23 +13,23 @@ import { describe('validateStorageOperationInput', () => { it('should return inputType as STORAGE_INPUT_PATH and objectKey as testPath when input is path as string', () => { - const input = { path: 'testPath' }; + const input = { path: '/testPath' }; const result = validateStorageOperationInput(input); expect(result).toEqual({ inputType: STORAGE_INPUT_PATH, - objectKey: 'testPath', + objectKey: '/testPath', }); }); it('should return inputType as STORAGE_INPUT_PATH and objectKey as result of path function when input is path as function', () => { const input = { path: ({ identityId }: { identityId?: string }) => - `testPath/${identityId}`, + `/testPath/${identityId}`, }; const result = validateStorageOperationInput(input, '123'); expect(result).toEqual({ inputType: STORAGE_INPUT_PATH, - objectKey: 'testPath/123', + objectKey: '/testPath/123', }); }); @@ -42,6 +42,15 @@ describe('validateStorageOperationInput', () => { }); }); + it('should throw an error when input path does not start with a /', () => { + const input = { path: 'test' } as any; + expect(() => validateStorageOperationInput(input)).toThrow( + validationErrorMap[ + StorageValidationErrorCode.InvalidStoragePathInput + ].message, + ); + }); + it('should throw an error when input is invalid', () => { const input = { invalid: 'test' } as any; expect(() => validateStorageOperationInput(input)).toThrow( diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index d0890608feb..a0ea179ce58 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -11,10 +11,11 @@ export enum StorageValidationErrorCode { NoDestinationKey = 'NoDestinationKey', NoBucket = 'NoBucket', NoRegion = 'NoRegion', - UrlExpirationMaxLimitExceed = 'UrlExpirationMaxLimitExceed', - ObjectIsTooLarge = 'ObjectIsTooLarge', - InvalidUploadSource = 'InvalidUploadSource', InvalidStorageOperationInput = 'InvalidStorageOperationInput', + InvalidStoragePathInput = 'InvalidStoragePathInput', + InvalidUploadSource = 'InvalidUploadSource', + ObjectIsTooLarge = 'ObjectIsTooLarge', + UrlExpirationMaxLimitExceed = 'UrlExpirationMaxLimitExceed', } export const validationErrorMap: AmplifyErrorMap = { @@ -53,4 +54,7 @@ export const validationErrorMap: AmplifyErrorMap = { [StorageValidationErrorCode.InvalidStorageOperationInput]: { message: 'Missing path or key parameter in Input.', }, + [StorageValidationErrorCode.InvalidStoragePathInput]: { + message: 'Input `path` is missing a leading slash (/).', + }, }; diff --git a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts index 8eb28ef8681..701c046d52f 100644 --- a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts +++ b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts @@ -5,7 +5,6 @@ import { AmplifyClassV6, StorageAccessLevel } from '@aws-amplify/core'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { StorageValidationErrorCode } from '../../../errors/types/validation'; -import { StorageError } from '../../../errors/StorageError'; import { resolvePrefix as defaultPrefixResolver } from '../../../utils/resolvePrefix'; import { ResolvedS3Config } from '../types/options'; @@ -31,7 +30,7 @@ interface ResolvedS3ConfigAndInput { * @param {AmplifyClassV6} amplify The Amplify instance. * @param {S3ApiOptions} apiOptions The input options for S3 provider. * @returns {Promise} The resolved common input options for S3 API handlers. - * @throws A {@link StorageError} with `error.name` from {@link StorageValidationErrorCode} indicating invalid + * @throws A `StorageError` with `error.name` from `StorageValidationErrorCode` indicating invalid * configurations or Amplify library options. * * @internal diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts index a52b5c2ab53..f4d24e64512 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -19,10 +19,15 @@ export const validateStorageOperationInput = ( if (isInputWithPath(input)) { const { path } = input; + const objectKey = typeof path === 'string' ? path : path({ identityId }); + assertValidationError( + objectKey.startsWith('/'), + StorageValidationErrorCode.InvalidStoragePathInput, + ); return { inputType: STORAGE_INPUT_PATH, - objectKey: typeof path === 'string' ? path : path({ identityId }), + objectKey, }; } else { return { inputType: STORAGE_INPUT_KEY, objectKey: input.key }; From 301298c2398cefea29c65a35ebbf8ddfca4b3cae Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Mon, 18 Mar 2024 13:28:45 -0700 Subject: [PATCH 05/33] chore: increase downloadData bundle size (#13142) Co-authored-by: Ashwin Kumar --- packages/aws-amplify/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 13c76ed2293..9b0a0c825b5 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -467,7 +467,7 @@ "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "14.15 kB" + "limit": "14.20 kB" }, { "name": "[Storage] getProperties (S3)", From 12576975b5a2e3617e2a97e79290e3214da87b21 Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:18:13 -0700 Subject: [PATCH 06/33] [Fix] Path leading slash validation (#13146) * update validation * add gen2 integ test --- .github/integ-config/integ-all.yml | 9 +++++++++ .../__tests__/providers/s3/apis/downloadData.test.ts | 4 ++-- .../s3/apis/utils/validateStorageOperationInput.test.ts | 4 ++-- .../providers/s3/utils/validateStorageOperationInput.ts | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index 82827def957..262b59a4b44 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -730,6 +730,15 @@ tests: spec: multi-part-copy browser: *minimal_browser_list + # GEN2 STORAGE + - test_name: integ_react_storage + desc: 'React Storage Gen2' + framework: react + category: storage + sample_name: [storage-gen2] + spec: storage-gen2 + browser: *minimal_browser_list + # INAPPMESSAGING - test_name: integ_in_app_messaging desc: 'React InApp Messaging' diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 88e9cb21eb7..b309c9e6c33 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -218,11 +218,11 @@ describe('downloadData with path', () => { test.each([ { - path: 'path', + path: '/path', expectedKey: 'path', }, { - path: () => 'path', + path: () => '/path', expectedKey: 'path', }, ])( diff --git a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts index 684fec06544..b6cfd102525 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts @@ -17,7 +17,7 @@ describe('validateStorageOperationInput', () => { const result = validateStorageOperationInput(input); expect(result).toEqual({ inputType: STORAGE_INPUT_PATH, - objectKey: '/testPath', + objectKey: 'testPath', }); }); @@ -29,7 +29,7 @@ describe('validateStorageOperationInput', () => { const result = validateStorageOperationInput(input, '123'); expect(result).toEqual({ inputType: STORAGE_INPUT_PATH, - objectKey: '/testPath/123', + objectKey: 'testPath/123', }); }); diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts index f4d24e64512..2c7f740fe53 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -27,7 +27,7 @@ export const validateStorageOperationInput = ( return { inputType: STORAGE_INPUT_PATH, - objectKey, + objectKey: objectKey.slice(1), }; } else { return { inputType: STORAGE_INPUT_KEY, objectKey: input.key }; From 3e6528d5fc40d2d985d38d97e2a0e8120a80728e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 20 Mar 2024 14:49:59 -0700 Subject: [PATCH 07/33] feat(storage): add path support to copy API (#13104) * feat(storage): add path supp to copy API * address feedback * address feedback * update copy tests with leading / * address feedback * increase bundle size --------- Co-authored-by: Ashwin Kumar --- packages/aws-amplify/package.json | 4 +- .../__tests__/providers/s3/apis/copy.test.ts | 299 +++++++++++------- .../storage/src/errors/types/validation.ts | 8 + .../storage/src/providers/s3/apis/copy.ts | 53 +++- .../src/providers/s3/apis/internal/copy.ts | 98 +++++- .../src/providers/s3/apis/server/copy.ts | 53 +++- .../storage/src/providers/s3/types/index.ts | 8 +- .../storage/src/providers/s3/types/inputs.ts | 17 +- .../storage/src/providers/s3/types/options.ts | 8 +- .../storage/src/providers/s3/types/outputs.ts | 9 +- packages/storage/src/types/index.ts | 3 +- packages/storage/src/types/inputs.ts | 22 +- 12 files changed, 429 insertions(+), 153 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 9b0a0c825b5..dfbe9c86727 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -461,13 +461,13 @@ "name": "[Storage] copy (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ copy }", - "limit": "13.50 kB" + "limit": "13.61 kB" }, { "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "14.20 kB" + "limit": "14.23 kB" }, { "name": "[Storage] getProperties (S3)", diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index e7c61ef8a3a..7e951c9ec09 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -3,11 +3,13 @@ import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; +import { StorageError } from '../../../../src/errors/StorageError'; +import { StorageValidationErrorCode } from '../../../../src/errors/types/validation'; import { copyObject } from '../../../../src/providers/s3/utils/client'; import { copy } from '../../../../src/providers/s3/apis'; import { - CopySourceOptions, - CopyDestinationOptions, + CopySourceOptionsKey, + CopyDestinationOptionsKey, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -63,121 +65,177 @@ describe('copy API', () => { }, }); }); - describe('Happy Path Cases:', () => { - beforeEach(() => { - mockCopyObject.mockImplementation(() => { - return { - Metadata: { key: 'value' }, - }; + + describe('Happy Cases', () => { + describe('With key', () => { + beforeEach(() => { + mockCopyObject.mockImplementation(() => { + return { + Metadata: { key: 'value' }, + }; + }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + [ + { + source: { accessLevel: 'guest' }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/public/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'guest' }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/public/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'guest' }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/public/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'private' }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'private' }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'private' }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected' }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'protected' }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected' }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected', targetIdentityId }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'protected', targetIdentityId }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected', targetIdentityId }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + ].forEach( + ({ + source, + destination, + expectedSourceKey, + expectedDestinationKey, + }) => { + const targetIdentityIdMsg = source?.targetIdentityId + ? `with targetIdentityId` + : ''; + it(`should copy ${source.accessLevel} ${targetIdentityIdMsg} -> ${destination.accessLevel}`, async () => { + expect( + await copy({ + source: { + ...(source as CopySourceOptionsKey), + key: sourceKey, + }, + destination: { + ...(destination as CopyDestinationOptionsKey), + key: destinationKey, + }, + }), + ).toEqual(copyResult); + expect(copyObject).toHaveBeenCalledTimes(1); + expect(copyObject).toHaveBeenCalledWith(copyObjectClientConfig, { + ...copyObjectClientBaseParams, + CopySource: expectedSourceKey, + Key: expectedDestinationKey, + }); + }); + }, + ); }); - afterEach(() => { - jest.clearAllMocks(); - }); - [ - { - source: { accessLevel: 'guest' }, - destination: { accessLevel: 'guest' }, - expectedSourceKey: `${bucket}/public/${sourceKey}`, - expectedDestinationKey: `public/${destinationKey}`, - }, - { - source: { accessLevel: 'guest' }, - destination: { accessLevel: 'private' }, - expectedSourceKey: `${bucket}/public/${sourceKey}`, - expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, - }, - { - source: { accessLevel: 'guest' }, - destination: { accessLevel: 'protected' }, - expectedSourceKey: `${bucket}/public/${sourceKey}`, - expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, - }, - { - source: { accessLevel: 'private' }, - destination: { accessLevel: 'guest' }, - expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, - expectedDestinationKey: `public/${destinationKey}`, - }, - { - source: { accessLevel: 'private' }, - destination: { accessLevel: 'private' }, - expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, - expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, - }, - { - source: { accessLevel: 'private' }, - destination: { accessLevel: 'protected' }, - expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, - expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, - }, - { - source: { accessLevel: 'protected' }, - destination: { accessLevel: 'guest' }, - expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, - expectedDestinationKey: `public/${destinationKey}`, - }, - { - source: { accessLevel: 'protected' }, - destination: { accessLevel: 'private' }, - expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, - expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, - }, - { - source: { accessLevel: 'protected' }, - destination: { accessLevel: 'protected' }, - expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, - expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, - }, - { - source: { accessLevel: 'protected', targetIdentityId }, - destination: { accessLevel: 'guest' }, - expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, - expectedDestinationKey: `public/${destinationKey}`, - }, - { - source: { accessLevel: 'protected', targetIdentityId }, - destination: { accessLevel: 'private' }, - expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, - expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, - }, - { - source: { accessLevel: 'protected', targetIdentityId }, - destination: { accessLevel: 'protected' }, - expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, - expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, - }, - ].forEach( - ({ source, destination, expectedSourceKey, expectedDestinationKey }) => { - const targetIdentityIdMsg = source?.targetIdentityId - ? `with targetIdentityId` - : ''; - it(`should copy ${source.accessLevel} ${targetIdentityIdMsg} -> ${destination.accessLevel}`, async () => { - expect.assertions(3); + + describe('With path', () => { + beforeEach(() => { + mockCopyObject.mockImplementation(() => { + return { + Metadata: { key: 'value' }, + }; + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each([ + { + sourcePath: '/sourcePathAsString', + expectedSourcePath: 'sourcePathAsString', + destinationPath: '/destinationPathAsString', + expectedDestinationPath: 'destinationPathAsString', + }, + { + sourcePath: () => '/sourcePathAsFunction', + expectedSourcePath: 'sourcePathAsFunction', + destinationPath: () => '/destinationPathAsFunction', + expectedDestinationPath: 'destinationPathAsFunction', + }, + ])( + 'should copy $sourcePath -> $destinationPath', + async ({ + sourcePath, + expectedSourcePath, + destinationPath, + expectedDestinationPath, + }) => { expect( await copy({ - source: { - ...(source as CopySourceOptions), - key: sourceKey, - }, - destination: { - ...(destination as CopyDestinationOptions), - key: destinationKey, - }, + source: { path: sourcePath }, + destination: { path: destinationPath }, }), - ).toEqual(copyResult); + ).toEqual({ path: expectedDestinationPath }); expect(copyObject).toHaveBeenCalledTimes(1); expect(copyObject).toHaveBeenCalledWith(copyObjectClientConfig, { ...copyObjectClientBaseParams, - CopySource: expectedSourceKey, - Key: expectedDestinationKey, + CopySource: `${bucket}/${expectedSourcePath}`, + Key: expectedDestinationPath, }); - }); - }, - ); + }, + ); + }); }); - describe('Error Path Cases:', () => { + describe('Error Cases:', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -206,5 +264,34 @@ describe('copy API', () => { expect(error.$metadata.httpStatusCode).toBe(404); } }); + + it('should return a path not found error when source uses path and destination uses key', async () => { + expect.assertions(2); + try { + // @ts-expect-error + await copy({ + source: { path: 'sourcePath' }, + destination: { key: 'destinationKey' }, + }); + } catch (error: any) { + expect(error).toBeInstanceOf(StorageError); + // source uses path so destination expects path as well + expect(error.name).toBe(StorageValidationErrorCode.NoDestinationPath); + } + }); + + it('should return a key not found error when source uses key and destination uses path', async () => { + expect.assertions(2); + try { + // @ts-expect-error + await copy({ + source: { key: 'sourcePath' }, + destination: { path: 'destinationKey' }, + }); + } catch (error: any) { + expect(error).toBeInstanceOf(StorageError); + expect(error.name).toBe(StorageValidationErrorCode.NoDestinationKey); + } + }); }); }); diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index a0ea179ce58..c56299878c2 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -9,6 +9,8 @@ export enum StorageValidationErrorCode { NoKey = 'NoKey', NoSourceKey = 'NoSourceKey', NoDestinationKey = 'NoDestinationKey', + NoSourcePath = 'NoSourcePath', + NoDestinationPath = 'NoDestinationPath', NoBucket = 'NoBucket', NoRegion = 'NoRegion', InvalidStorageOperationInput = 'InvalidStorageOperationInput', @@ -35,6 +37,12 @@ export const validationErrorMap: AmplifyErrorMap = { [StorageValidationErrorCode.NoDestinationKey]: { message: 'Missing destination key in copy api call.', }, + [StorageValidationErrorCode.NoSourcePath]: { + message: 'Missing source path in copy api call.', + }, + [StorageValidationErrorCode.NoDestinationPath]: { + message: 'Missing destination path in copy api call.', + }, [StorageValidationErrorCode.NoBucket]: { message: 'Missing bucket name while accessing object.', }, diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index ca0ae3e8a39..1978d9595b3 100644 --- a/packages/storage/src/providers/s3/apis/copy.ts +++ b/packages/storage/src/providers/s3/apis/copy.ts @@ -3,21 +3,44 @@ import { Amplify } from '@aws-amplify/core'; -import { CopyInput, CopyOutput, S3Exception } from '../types'; -import { StorageValidationErrorCode } from '../../../errors/types/validation'; +import { + CopyInput, + CopyInputKey, + CopyInputPath, + CopyOutput, + CopyOutputKey, + CopyOutputPath, +} from '../types'; import { copy as copyInternal } from './internal/copy'; -/** - * Copy an object from a source object to a new object within the same bucket. Can optionally copy files across - * different level or identityId (if source object's level is 'protected'). - * - * @param input - The CopyInput object. - * @returns Output containing the destination key. - * @throws service: {@link S3Exception} - Thrown when checking for existence of the object - * @throws validation: {@link StorageValidationErrorCode } - Thrown when - * source or destination key are not defined. - */ -export const copy = async (input: CopyInput): Promise => { - return copyInternal(Amplify, input); -}; +interface Copy { + /** + * Copy an object from a source to a destination object within the same bucket. + * + * @param input - The CopyInputPath object. + * @returns Output containing the destination object path. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination path is not defined. + */ + (input: CopyInputPath): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. + * + * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across + * different accessLevel or identityId (if source object's accessLevel is 'protected'). + * + * @param input - The CopyInputKey object. + * @returns Output containing the destination object key. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination key is not defined. + */ + (input: CopyInputKey): Promise; +} + +export const copy: Copy = ( + input: CopyInput, +): Promise => copyInternal(Amplify, input) as Promise; diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts index feefdd1b5c3..26576323b86 100644 --- a/packages/storage/src/providers/s3/apis/internal/copy.ts +++ b/packages/storage/src/providers/s3/apis/internal/copy.ts @@ -4,18 +4,80 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { CopyInput, CopyOutput } from '../../types'; -import { resolveS3ConfigAndInput } from '../../utils'; +import { + CopyInput, + CopyInputKey, + CopyInputPath, + CopyOutput, + CopyOutputKey, + CopyOutputPath, +} from '../../types'; +import { ResolvedS3Config } from '../../types/options'; +import { + isInputWithPath, + resolveS3ConfigAndInput, + validateStorageOperationInput, +} from '../../utils'; import { StorageValidationErrorCode } from '../../../../errors/types/validation'; import { assertValidationError } from '../../../../errors/utils/assertValidationError'; import { copyObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; +const isCopyInputWithPath = (input: CopyInput): input is CopyInputPath => + isInputWithPath(input.source); + export const copy = async ( amplify: AmplifyClassV6, input: CopyInput, ): Promise => { + return isCopyInputWithPath(input) + ? copyWithPath(amplify, input) + : copyWithKey(amplify, input); +}; + +const copyWithPath = async ( + amplify: AmplifyClassV6, + input: CopyInputPath, +): Promise => { + const { source, destination } = input; + const { s3Config, bucket, identityId } = + await resolveS3ConfigAndInput(amplify); + + assertValidationError(!!source.path, StorageValidationErrorCode.NoSourcePath); + assertValidationError( + !!destination.path, + StorageValidationErrorCode.NoDestinationPath, + ); + + const { objectKey: sourcePath } = validateStorageOperationInput( + source, + identityId, + ); + const { objectKey: destinationPath } = validateStorageOperationInput( + destination, + identityId, + ); + + const finalCopySource = `${bucket}/${sourcePath}`; + const finalCopyDestination = destinationPath; + logger.debug(`copying "${finalCopySource}" to "${finalCopyDestination}".`); + + await serviceCopy({ + source: finalCopySource, + destination: finalCopyDestination, + bucket, + s3Config, + }); + + return { path: finalCopyDestination }; +}; + +/** @deprecated Use {@link copyWithPath} instead. */ +export const copyWithKey = async ( + amplify: AmplifyClassV6, + input: CopyInputKey, +): Promise => { const { source: { key: sourceKey }, destination: { key: destinationKey }, @@ -41,6 +103,30 @@ export const copy = async ( const finalCopySource = `${bucket}/${sourceKeyPrefix}${sourceKey}`; const finalCopyDestination = `${destinationKeyPrefix}${destinationKey}`; logger.debug(`copying "${finalCopySource}" to "${finalCopyDestination}".`); + + await serviceCopy({ + source: finalCopySource, + destination: finalCopyDestination, + bucket, + s3Config, + }); + + return { + key: destinationKey, + }; +}; + +const serviceCopy = async ({ + source, + destination, + bucket, + s3Config, +}: { + source: string; + destination: string; + bucket: string; + s3Config: ResolvedS3Config; +}) => { await copyObject( { ...s3Config, @@ -48,13 +134,9 @@ export const copy = async ( }, { Bucket: bucket, - CopySource: finalCopySource, - Key: finalCopyDestination, + CopySource: source, + Key: destination, MetadataDirective: 'COPY', // Copies over metadata like contentType as well }, ); - - return { - key: destinationKey, - }; }; diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index 8a4f64d272d..be7f6a3d012 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -1,17 +1,58 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - import { AmplifyServer, getAmplifyServerContext, } from '@aws-amplify/core/internals/adapter-core'; -import { CopyInput, CopyOutput } from '../../types'; +import { + CopyInput, + CopyInputKey, + CopyInputPath, + CopyOutput, + CopyOutputKey, + CopyOutputPath, +} from '../../types'; import { copy as copyInternal } from '../internal/copy'; -export const copy = async ( +interface Copy { + /** + * Copy an object from a source to a destination object within the same bucket. + * + * @param input - The CopyInputPath object. + * @returns Output containing the destination object path. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination path is not defined. + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: CopyInputPath, + ): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. + * + * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across + * different accessLevel or identityId (if source object's accessLevel is 'protected'). + * + * @param input - The CopyInputKey object. + * @returns Output containing the destination object key. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination key is not defined. + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: CopyInputKey, + ): Promise; +} + +export const copy: Copy = ( contextSpec: AmplifyServer.ContextSpec, input: CopyInput, -): Promise => { - return copyInternal(getAmplifyServerContext(contextSpec).amplify, input); -}; +): Promise => + copyInternal( + getAmplifyServerContext(contextSpec).amplify, + input, + ) as Promise; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 773dde39361..559bb111afa 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -10,8 +10,8 @@ export { RemoveOptions, DownloadDataOptionsPath, DownloadDataOptionsKey, - CopyDestinationOptions, - CopySourceOptions, + CopyDestinationOptionsKey, + CopySourceOptionsKey, } from './options'; export { DownloadDataOutput, @@ -24,10 +24,14 @@ export { ListPaginateOutput, GetPropertiesOutput, CopyOutput, + CopyOutputKey, + CopyOutputPath, RemoveOutput, } from './outputs'; export { CopyInput, + CopyInputKey, + CopyInputPath, GetPropertiesInput, GetUrlInput, ListAllInput, diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index 3ec41b7d47a..abdd54a841c 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -4,7 +4,8 @@ import { StrictUnion } from '@aws-amplify/core/internals/utils'; import { - StorageCopyInput, + StorageCopyInputKey, + StorageCopyInputPath, StorageDownloadDataInputKey, StorageDownloadDataInputPath, StorageGetPropertiesInput, @@ -14,8 +15,8 @@ import { StorageUploadDataInput, } from '../../../types'; import { - CopyDestinationOptions, - CopySourceOptions, + CopyDestinationOptionsKey, + CopySourceOptionsKey, DownloadDataOptionsKey, DownloadDataOptionsPath, GetPropertiesOptions, @@ -30,10 +31,14 @@ import { /** * Input type for S3 copy API. */ -export type CopyInput = StorageCopyInput< - CopySourceOptions, - CopyDestinationOptions +export type CopyInput = CopyInputKey | CopyInputPath; + +/** @deprecated Use {@link CopyInputPath} instead. */ +export type CopyInputKey = StorageCopyInputKey< + CopySourceOptionsKey, + CopyDestinationOptionsKey >; +export type CopyInputPath = StorageCopyInputPath; /** * Input type for S3 getProperties API. diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 6356ca81a93..5de24ebd4c9 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -123,11 +123,15 @@ export type UploadDataOptions = WriteOptions & metadata?: Record; }; -export type CopySourceOptions = ReadOptions & { +/** @deprecated This may be removed in the next major version. */ +export type CopySourceOptionsKey = ReadOptions & { + /** @deprecated This may be removed in the next major version. */ key: string; }; -export type CopyDestinationOptions = WriteOptions & { +/** @deprecated This may be removed in the next major version. */ +export type CopyDestinationOptionsKey = WriteOptions & { + /** @deprecated This may be removed in the next major version. */ key: string; }; diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index 35b6784af93..c99f442f22a 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { StrictUnion } from '@aws-amplify/core/internals/utils'; + import { DownloadTask, StorageDownloadDataOutput, @@ -77,9 +79,12 @@ export type ListPaginateOutput = StorageListOutput & { }; /** - * Output type for S3 copy API. + * @deprecated Use {@link CopyOutputPath} instead. */ -export type CopyOutput = Pick; +export type CopyOutputKey = Pick; +export type CopyOutputPath = Pick; + +export type CopyOutput = StrictUnion; /** * Output type for S3 remove API. diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 78569a76f36..db2fac1628d 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -15,7 +15,8 @@ export { StorageDownloadDataInputKey, StorageDownloadDataInputPath, StorageUploadDataInput, - StorageCopyInput, + StorageCopyInputKey, + StorageCopyInputPath, StorageGetUrlInput, StorageUploadDataPayload, } from './inputs'; diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 0514feaba30..57fd33b7c45 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -63,12 +63,28 @@ export type StorageUploadDataInput = data: StorageUploadDataPayload; }; -export interface StorageCopyInput< +/** @deprecated Use {@link StorageCopyInputPath} instead. */ +export interface StorageCopyInputKey< SourceOptions extends StorageOptions, DestinationOptions extends StorageOptions, > { - source: SourceOptions; - destination: DestinationOptions; + source: SourceOptions & { + path?: never; + }; + destination: DestinationOptions & { + path?: never; + }; +} + +export interface StorageCopyInputPath { + source: StorageOperationInputPath & { + /** @deprecated Use path instead. */ + key?: never; + }; + destination: StorageOperationInputPath & { + /** @deprecated Use path instead. */ + key?: never; + }; } /** From 29661a545d51c07296d9e0567d4e7f46420b7484 Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:15:59 -0700 Subject: [PATCH 08/33] Update Path assertion (#13160) * update validation on path assertion --------- Co-authored-by: Ashwin Kumar --- .../storage/__tests__/providers/s3/apis/copy.test.ts | 8 ++++---- .../__tests__/providers/s3/apis/downloadData.test.ts | 4 ++-- .../apis/utils/validateStorageOperationInput.test.ts | 8 ++++---- packages/storage/src/errors/types/validation.ts | 2 +- packages/storage/src/providers/s3/types/options.ts | 12 ++++++++++-- .../s3/utils/validateStorageOperationInput.ts | 4 ++-- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index 7e951c9ec09..24dda7bcd3f 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -199,15 +199,15 @@ describe('copy API', () => { test.each([ { - sourcePath: '/sourcePathAsString', + sourcePath: 'sourcePathAsString', expectedSourcePath: 'sourcePathAsString', - destinationPath: '/destinationPathAsString', + destinationPath: 'destinationPathAsString', expectedDestinationPath: 'destinationPathAsString', }, { - sourcePath: () => '/sourcePathAsFunction', + sourcePath: () => 'sourcePathAsFunction', expectedSourcePath: 'sourcePathAsFunction', - destinationPath: () => '/destinationPathAsFunction', + destinationPath: () => 'destinationPathAsFunction', expectedDestinationPath: 'destinationPathAsFunction', }, ])( diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index b309c9e6c33..88e9cb21eb7 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -218,11 +218,11 @@ describe('downloadData with path', () => { test.each([ { - path: '/path', + path: 'path', expectedKey: 'path', }, { - path: () => '/path', + path: () => 'path', expectedKey: 'path', }, ])( diff --git a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts index b6cfd102525..cf9f38318d1 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts @@ -13,7 +13,7 @@ import { describe('validateStorageOperationInput', () => { it('should return inputType as STORAGE_INPUT_PATH and objectKey as testPath when input is path as string', () => { - const input = { path: '/testPath' }; + const input = { path: 'testPath' }; const result = validateStorageOperationInput(input); expect(result).toEqual({ inputType: STORAGE_INPUT_PATH, @@ -24,7 +24,7 @@ describe('validateStorageOperationInput', () => { it('should return inputType as STORAGE_INPUT_PATH and objectKey as result of path function when input is path as function', () => { const input = { path: ({ identityId }: { identityId?: string }) => - `/testPath/${identityId}`, + `testPath/${identityId}`, }; const result = validateStorageOperationInput(input, '123'); expect(result).toEqual({ @@ -42,8 +42,8 @@ describe('validateStorageOperationInput', () => { }); }); - it('should throw an error when input path does not start with a /', () => { - const input = { path: 'test' } as any; + it('should throw an error when input path starts with a /', () => { + const input = { path: '/leading-slash-path' }; expect(() => validateStorageOperationInput(input)).toThrow( validationErrorMap[ StorageValidationErrorCode.InvalidStoragePathInput diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index c56299878c2..01954a896ec 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -63,6 +63,6 @@ export const validationErrorMap: AmplifyErrorMap = { message: 'Missing path or key parameter in Input.', }, [StorageValidationErrorCode.InvalidStoragePathInput]: { - message: 'Input `path` is missing a leading slash (/).', + message: 'Input `path` does not allow a leading slash (/).', }, }; diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 5de24ebd4c9..7990f798aba 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -20,8 +20,16 @@ interface CommonOptions { /** @deprecated This may be removed in the next major version. */ type ReadOptions = - | { accessLevel?: 'guest' | 'private' } - | { accessLevel: 'protected'; targetIdentityId?: string }; + | { + /** @deprecated This may be removed in the next major version. */ + accessLevel?: 'guest' | 'private'; + } + | { + /** @deprecated This may be removed in the next major version. */ + accessLevel: 'protected'; + /** @deprecated This may be removed in the next major version. */ + targetIdentityId?: string; + }; /** @deprecated This may be removed in the next major version. */ interface WriteOptions { diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts index 2c7f740fe53..12e71f10fdb 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -21,13 +21,13 @@ export const validateStorageOperationInput = ( const { path } = input; const objectKey = typeof path === 'string' ? path : path({ identityId }); assertValidationError( - objectKey.startsWith('/'), + !objectKey.startsWith('/'), StorageValidationErrorCode.InvalidStoragePathInput, ); return { inputType: STORAGE_INPUT_PATH, - objectKey: objectKey.slice(1), + objectKey, }; } else { return { inputType: STORAGE_INPUT_KEY, objectKey: input.key }; From 77b2b20d021de3804feb8041414e9c7b68ef0bbf Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Fri, 22 Mar 2024 12:32:40 -0500 Subject: [PATCH 09/33] chore: Enable pre-id publication for gen2-storage (#13159) --- .github/workflows/push-integ-test.yml | 1 - .github/workflows/push-preid-release.yml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml index 0fd20d26555..a56ca5db116 100644 --- a/.github/workflows/push-integ-test.yml +++ b/.github/workflows/push-integ-test.yml @@ -9,7 +9,6 @@ on: push: branches: - next/main - - gen2-storage jobs: e2e: diff --git a/.github/workflows/push-preid-release.yml b/.github/workflows/push-preid-release.yml index 9837290ed14..5a7d27ef8b8 100644 --- a/.github/workflows/push-preid-release.yml +++ b/.github/workflows/push-preid-release.yml @@ -9,7 +9,7 @@ on: push: branches: # Change this to your branch name where "example-preid" corresponds to the preid you want your changes released on - - feat/example-preid-branch/main + - gen2-storage jobs: e2e: @@ -35,4 +35,4 @@ jobs: # The preid should be detected from the branch name recommending feat/{PREID}/whatever as branch naming pattern # if your branch doesn't follow this pattern, you can override it here for your branch. with: - preid: ${{ needs.parse-preid.outputs.preid }} + preid: gen2-storage From 6456588bae501f92233ace4a6698d14db1f3557a Mon Sep 17 00:00:00 2001 From: erinleigh90 <106691284+erinleigh90@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:40:34 -0700 Subject: [PATCH 10/33] feat: add gen2 path parameter to getProperties and getUrl (#13144) * feat: add gen2 path to getProperties API * test: add unit tests for path parameter * nit: remove unnecessary colon * feat: add path to types * test: first pass to add path coverage * fix: adjust tests * chore: remove links from tsdocs * test: fix expected Key * chore: prettier fix * chore: increase getUrl bundle size * chore: clean up tsdocs in getProperties * chore: consolidate constants imports * test: remove leading slash from path * chore: address feedback * chore: fix test and bundle size check * chore: add back-tics to tsdocs * chore: use test.each instead of forEach * chore: fix deprecation and tsdocs * chore: add contextSpec to copy tsdocs * Update packages/storage/src/providers/s3/apis/copy.ts Co-authored-by: ashika112 <155593080+ashika112@users.noreply.github.com> * chore: another tsdoc fix * chore: add tsdocs to server getUrl * address feedback * address feedback * address feedback * chore: add overload for deprecation messaging to getUrl * address feedback --------- Co-authored-by: ashika112 <155593080+ashika112@users.noreply.github.com> --- packages/aws-amplify/package.json | 2 +- .../providers/s3/apis/getProperties.test.ts | 141 +++++++++++++++--- .../providers/s3/apis/getUrl.test.ts | 124 +++++++++++++-- .../src/providers/s3/apis/getProperties.ts | 53 +++++-- .../storage/src/providers/s3/apis/getUrl.ts | 70 ++++++--- .../s3/apis/internal/getProperties.ts | 28 ++-- .../src/providers/s3/apis/internal/getUrl.ts | 40 +++-- .../src/providers/s3/apis/server/copy.ts | 2 + .../providers/s3/apis/server/getProperties.ts | 54 ++++++- .../src/providers/s3/apis/server/getUrl.ts | 61 +++++++- .../storage/src/providers/s3/types/index.ts | 12 +- .../storage/src/providers/s3/types/inputs.ts | 29 +++- .../storage/src/providers/s3/types/options.ts | 33 ++-- .../storage/src/providers/s3/types/outputs.ts | 8 +- packages/storage/src/types/index.ts | 6 +- packages/storage/src/types/inputs.ts | 16 +- 16 files changed, 549 insertions(+), 130 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index dfbe9c86727..042b46881be 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -479,7 +479,7 @@ "name": "[Storage] getUrl (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getUrl }", - "limit": "14.50 kB" + "limit": "14.60 kB" }, { "name": "[Storage] list (S3)", diff --git a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts index 6ee31b031ed..a2f0df8e886 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -5,7 +5,10 @@ import { headObject } from '../../../../src/providers/s3/utils/client'; import { getProperties } from '../../../../src/providers/s3'; import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; -import { GetPropertiesOptions } from '../../../../src/providers/s3/types'; +import { + GetPropertiesOptionsKey, + GetPropertiesOptionsPath, +} from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -30,10 +33,12 @@ const credentials: AWSCredentials = { sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', }; +const key = 'key'; +const path = 'path'; const targetIdentityId = 'targetIdentityId'; const defaultIdentityId = 'defaultIdentityId'; -describe('getProperties api', () => { +describe('getProperties with key', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -48,9 +53,9 @@ describe('getProperties api', () => { }, }); }); - describe('getProperties happy path ', () => { + describe('Happy cases: With key', () => { const expected = { - key: 'key', + key, size: '100', contentType: 'text/plain', eTag: 'etag', @@ -63,7 +68,6 @@ describe('getProperties api', () => { region: 'region', userAgentValue: expect.any(String), }; - const key = 'key'; beforeEach(() => { mockHeadObject.mockReturnValueOnce({ ContentLength: '100', @@ -77,7 +81,7 @@ describe('getProperties api', () => { afterEach(() => { jest.clearAllMocks(); }); - [ + test.each([ { expectedKey: `public/${key}`, }, @@ -97,30 +101,133 @@ describe('getProperties api', () => { options: { accessLevel: 'protected', targetIdentityId }, expectedKey: `protected/${targetIdentityId}/${key}`, }, - ].forEach(({ options, expectedKey }) => { - const accessLevelMsg = options?.accessLevel ?? 'default'; - const targetIdentityIdMsg = options?.targetIdentityId - ? `and targetIdentityId` - : ''; - it(`should getProperties with ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { + ])( + 'should getProperties with key $expectedKey', + async ({ options, expectedKey }) => { const headObjectOptions = { Bucket: 'bucket', Key: expectedKey, }; - expect.assertions(3); expect( await getProperties({ key, - options: options as GetPropertiesOptions, + options: options as GetPropertiesOptionsKey, }), ).toEqual(expected); expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); + }, + ); + }); + + describe('Error cases : With key', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('getProperties should return a not found error', async () => { + mockHeadObject.mockRejectedValueOnce( + Object.assign(new Error(), { + $metadata: { httpStatusCode: 404 }, + name: 'NotFound', + }), + ); + expect.assertions(3); + try { + await getProperties({ key }); + } catch (error: any) { + expect(headObject).toHaveBeenCalledTimes(1); + expect(headObject).toHaveBeenCalledWith( + { + credentials, + region: 'region', + userAgentValue: expect.any(String), + }, + { + Bucket: 'bucket', + Key: `public/${key}`, + }, + ); + expect(error.$metadata.httpStatusCode).toBe(404); + } + }); + }); +}); + +describe('Happy cases: With path', () => { + beforeAll(() => { + mockFetchAuthSession.mockResolvedValue({ + credentials, + identityId: defaultIdentityId, + }); + mockGetConfig.mockReturnValue({ + Storage: { + S3: { + bucket, + region, + }, + }, + }); + }); + describe('getProperties with path', () => { + const expected = { + path, + size: '100', + contentType: 'text/plain', + eTag: 'etag', + metadata: { key: 'value' }, + lastModified: 'last-modified', + versionId: 'version-id', + }; + const config = { + credentials, + region: 'region', + useAccelerateEndpoint: true, + userAgentValue: expect.any(String), + }; + beforeEach(() => { + mockHeadObject.mockReturnValueOnce({ + ContentLength: '100', + ContentType: 'text/plain', + ETag: 'etag', + LastModified: 'last-modified', + Metadata: { key: 'value' }, + VersionId: 'version-id', }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + test.each([ + { + testPath: path, + expectedKey: path, + }, + { + testPath: () => path, + expectedKey: path, + }, + ])( + 'should getProperties with path $path and expectedKey $expectedKey', + async ({ testPath, expectedKey }) => { + const headObjectOptions = { + Bucket: 'bucket', + Key: expectedKey, + }; + expect( + await getProperties({ + path: testPath, + options: { + useAccelerateEndpoint: true, + } as GetPropertiesOptionsPath, + }), + ).toEqual(expected); + expect(headObject).toHaveBeenCalledTimes(1); + expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); + }, + ); }); - describe('getProperties error path', () => { + describe('Error cases : With path', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -133,7 +240,7 @@ describe('getProperties api', () => { ); expect.assertions(3); try { - await getProperties({ key: 'keyed' }); + await getProperties({ path }); } catch (error: any) { expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith( @@ -144,7 +251,7 @@ describe('getProperties api', () => { }, { Bucket: 'bucket', - Key: 'public/keyed', + Key: path, }, ); expect(error.$metadata.httpStatusCode).toBe(404); diff --git a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts index 1cdf1a59b72..28804743c7d 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -8,7 +8,10 @@ import { getPresignedGetObjectUrl, headObject, } from '../../../../src/providers/s3/utils/client'; -import { GetUrlOptions } from '../../../../src/providers/s3/types'; +import { + GetUrlOptionsKey, + GetUrlOptionsPath, +} from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -35,7 +38,7 @@ const credentials: AWSCredentials = { const targetIdentityId = 'targetIdentityId'; const defaultIdentityId = 'defaultIdentityId'; -describe('getUrl test', () => { +describe('getUrl test with key', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -51,7 +54,7 @@ describe('getUrl test', () => { }); }); - describe('getUrl happy path', () => { + describe('Happy cases: With key', () => { const config = { credentials, region, @@ -76,7 +79,7 @@ describe('getUrl test', () => { afterEach(() => { jest.clearAllMocks(); }); - [ + test.each([ { expectedKey: `public/${key}`, }, @@ -96,23 +99,19 @@ describe('getUrl test', () => { options: { accessLevel: 'protected', targetIdentityId }, expectedKey: `protected/${targetIdentityId}/${key}`, }, - ].forEach(({ options, expectedKey }) => { - const accessLevelMsg = options?.accessLevel ?? 'default'; - const targetIdentityIdMsg = options?.targetIdentityId - ? `and targetIdentityId` - : ''; - it(`should getUrl with ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { + ])( + 'should getUrl with key $expectedKey', + async ({ options, expectedKey }) => { const headObjectOptions = { Bucket: bucket, Key: expectedKey, }; - expect.assertions(4); const result = await getUrl({ key, options: { ...options, validateObjectExistence: true, - } as GetUrlOptions, + } as GetUrlOptionsKey, }); expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledTimes(1); @@ -120,10 +119,107 @@ describe('getUrl test', () => { expect(result.url).toEqual({ url: new URL('https://google.com'), }); + }, + ); + }); + describe('Error cases : With key', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + it('should return not found error when the object is not found', async () => { + (headObject as jest.Mock).mockImplementation(() => { + throw Object.assign(new Error(), { + $metadata: { httpStatusCode: 404 }, + name: 'NotFound', + }); }); + expect.assertions(2); + try { + await getUrl({ + key: 'invalid_key', + options: { validateObjectExistence: true }, + }); + } catch (error: any) { + expect(headObject).toHaveBeenCalledTimes(1); + expect(error.$metadata?.httpStatusCode).toBe(404); + } }); }); - describe('getUrl error path', () => { +}); + +describe('getUrl test with path', () => { + beforeAll(() => { + mockFetchAuthSession.mockResolvedValue({ + credentials, + identityId: defaultIdentityId, + }); + mockGetConfig.mockReturnValue({ + Storage: { + S3: { + bucket, + region, + }, + }, + }); + }); + + describe('Happy cases: With path', () => { + const config = { + credentials, + region, + userAgentValue: expect.any(String), + }; + beforeEach(() => { + (headObject as jest.Mock).mockImplementation(() => { + return { + Key: 'path', + ContentLength: '100', + ContentType: 'text/plain', + ETag: 'etag', + LastModified: 'last-modified', + Metadata: { key: 'value' }, + }; + }); + (getPresignedGetObjectUrl as jest.Mock).mockReturnValueOnce({ + url: new URL('https://google.com'), + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each([ + { + path: 'path', + expectedKey: 'path', + }, + { + path: () => 'path', + expectedKey: 'path', + }, + ])( + 'should getUrl with path $path and expectedKey $expectedKey', + async ({ path, expectedKey }) => { + const headObjectOptions = { + Bucket: bucket, + Key: expectedKey, + }; + const result = await getUrl({ + path, + options: { + validateObjectExistence: true, + } as GetUrlOptionsPath, + }); + expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); + expect(headObject).toHaveBeenCalledTimes(1); + expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); + expect(result.url).toEqual({ + url: new URL('https://google.com'), + }); + }, + ); + }); + describe('Error cases : With path', () => { afterAll(() => { jest.clearAllMocks(); }); @@ -137,7 +233,7 @@ describe('getUrl test', () => { expect.assertions(2); try { await getUrl({ - key: 'invalid_key', + path: 'invalid_key', options: { validateObjectExistence: true }, }); } catch (error: any) { diff --git a/packages/storage/src/providers/s3/apis/getProperties.ts b/packages/storage/src/providers/s3/apis/getProperties.ts index 4b98d529f55..227406bc369 100644 --- a/packages/storage/src/providers/s3/apis/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/getProperties.ts @@ -3,22 +3,45 @@ import { Amplify } from '@aws-amplify/core'; -import { GetPropertiesInput, GetPropertiesOutput, S3Exception } from '../types'; -import { StorageValidationErrorCode } from '../../../errors/types/validation'; +import { + GetPropertiesInput, + GetPropertiesInputKey, + GetPropertiesInputPath, + GetPropertiesOutput, + GetPropertiesOutputKey, + GetPropertiesOutputPath, +} from '../types'; import { getProperties as getPropertiesInternal } from './internal/getProperties'; -/** - * Gets the properties of a file. The properties include S3 system metadata and - * the user metadata that was provided when uploading the file. - * - * @param input - The GetPropertiesInput object. - * @returns Requested object properties. - * @throws A {@link S3Exception} when the underlying S3 service returned error. - * @throws A {@link StorageValidationErrorCode} when API call parameters are invalid. - */ -export const getProperties = ( +interface GetProperties { + /** + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param input - The `GetPropertiesInputPath` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ + (input: GetPropertiesInputPath): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. + * + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param input - The `GetPropertiesInputKey` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ + (input: GetPropertiesInputKey): Promise; +} + +export const getProperties: GetProperties = < + Output extends GetPropertiesOutput, +>( input: GetPropertiesInput, -): Promise => { - return getPropertiesInternal(Amplify, input); -}; +): Promise => getPropertiesInternal(Amplify, input) as Promise; diff --git a/packages/storage/src/providers/s3/apis/getUrl.ts b/packages/storage/src/providers/s3/apis/getUrl.ts index 708e601cc87..a49210063a3 100644 --- a/packages/storage/src/providers/s3/apis/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/getUrl.ts @@ -3,28 +3,54 @@ import { Amplify } from '@aws-amplify/core'; -import { StorageValidationErrorCode } from '../../../errors/types/validation'; -import { GetUrlInput, GetUrlOutput, S3Exception } from '../types'; -import { StorageError } from '../../../errors/StorageError'; +import { + GetUrlInput, + GetUrlInputKey, + GetUrlInputPath, + GetUrlOutput, +} from '../types'; import { getUrl as getUrlInternal } from './internal/getUrl'; -/** - * Get a temporary presigned URL to download the specified S3 object. - * The presigned URL expires when the associated role used to sign the request expires or - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * to true, this method will verify the given object already exists in S3 before returning a presigned - * URL, and will throw {@link StorageError} if the object does not exist. - * - * @param input - The GetUrlInput object. - * @returns Presigned URL and timestamp when the URL MAY expire. - * @throws service: {@link S3Exception} - thrown when checking for existence of the object - * @throws validation: {@link StorageValidationErrorCode } - Validation errors - * thrown either username or key are not defined. - * - */ -export const getUrl = (input: GetUrlInput): Promise => { - return getUrlInternal(Amplify, input); -}; +interface GetUrl { + /** + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param input - The `GetUrlInputPath` object. + * @returns Presigned URL and timestamp when the URL MAY expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ + (input: GetUrlInputPath): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. + * + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param input - The `GetUrlInputKey` object. + * @returns Presigned URL and timestamp when the URL MAY expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ + (input: GetUrlInputKey): Promise; +} + +export const getUrl: GetUrl = (input: GetUrlInput): Promise => + getUrlInternal(Amplify, input); diff --git a/packages/storage/src/providers/s3/apis/internal/getProperties.ts b/packages/storage/src/providers/s3/apis/internal/getProperties.ts index db854f8635b..e7f20187a36 100644 --- a/packages/storage/src/providers/s3/apis/internal/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/internal/getProperties.ts @@ -5,24 +5,31 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { GetPropertiesInput, GetPropertiesOutput } from '../../types'; -import { resolveS3ConfigAndInput } from '../../utils'; +import { + resolveS3ConfigAndInput, + validateStorageOperationInput, +} from '../../utils'; import { headObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; +import { STORAGE_INPUT_KEY } from '../../utils/constants'; export const getProperties = async ( amplify: AmplifyClassV6, input: GetPropertiesInput, action?: StorageAction, ): Promise => { - const { key, options } = input; - const { s3Config, bucket, keyPrefix } = await resolveS3ConfigAndInput( - amplify, - options, + const { options: getPropertiesOptions } = input; + const { s3Config, bucket, keyPrefix, identityId } = + await resolveS3ConfigAndInput(amplify, getPropertiesOptions); + const { inputType, objectKey } = validateStorageOperationInput( + input, + identityId, ); - const finalKey = `${keyPrefix}${key}`; + const finalKey = + inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey; - logger.debug(`get properties of ${key} from ${finalKey}`); + logger.debug(`get properties of ${objectKey} from ${finalKey}`); const response = await headObject( { ...s3Config, @@ -36,8 +43,7 @@ export const getProperties = async ( }, ); - return { - key, + const result = { contentType: response.ContentType, size: response.ContentLength, eTag: response.ETag, @@ -45,4 +51,8 @@ export const getProperties = async ( metadata: response.Metadata, versionId: response.VersionId, }; + + return inputType === STORAGE_INPUT_KEY + ? { key: objectKey, ...result } + : { path: objectKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/apis/internal/getUrl.ts b/packages/storage/src/providers/s3/apis/internal/getUrl.ts index 15110d535b7..ea2fd60d741 100644 --- a/packages/storage/src/providers/s3/apis/internal/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/internal/getUrl.ts @@ -7,11 +7,15 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { GetUrlInput, GetUrlOutput } from '../../types'; import { StorageValidationErrorCode } from '../../../../errors/types/validation'; import { getPresignedGetObjectUrl } from '../../utils/client'; -import { resolveS3ConfigAndInput } from '../../utils'; +import { + resolveS3ConfigAndInput, + validateStorageOperationInput, +} from '../../utils'; import { assertValidationError } from '../../../../errors/utils/assertValidationError'; import { DEFAULT_PRESIGN_EXPIRATION, MAX_URL_EXPIRATION, + STORAGE_INPUT_KEY, } from '../../utils/constants'; import { getProperties } from './getProperties'; @@ -20,18 +24,32 @@ export const getUrl = async ( amplify: AmplifyClassV6, input: GetUrlInput, ): Promise => { - const { key, options } = input; + const { options: getUrlOptions } = input; + const { s3Config, keyPrefix, bucket, identityId } = + await resolveS3ConfigAndInput(amplify, getUrlOptions); + const { inputType, objectKey } = validateStorageOperationInput( + input, + identityId, + ); - if (options?.validateObjectExistence) { - await getProperties(amplify, { key, options }, StorageAction.GetUrl); - } + const finalKey = + inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey; - const { s3Config, keyPrefix, bucket } = await resolveS3ConfigAndInput( - amplify, - options, - ); + if (getUrlOptions?.validateObjectExistence) { + await getProperties( + amplify, + { + options: getUrlOptions, + ...((inputType === STORAGE_INPUT_KEY + ? { key: input.key } + : { path: input.path }) as GetUrlInput), + }, + StorageAction.GetUrl, + ); + } - let urlExpirationInSec = options?.expiresIn ?? DEFAULT_PRESIGN_EXPIRATION; + let urlExpirationInSec = + getUrlOptions?.expiresIn ?? DEFAULT_PRESIGN_EXPIRATION; const awsCredExpiration = s3Config.credentials?.expiration; if (awsCredExpiration) { const awsCredExpirationInSec = Math.floor( @@ -54,7 +72,7 @@ export const getUrl = async ( }, { Bucket: bucket, - Key: `${keyPrefix}${key}`, + Key: finalKey, }, ), expiresAt: new Date(Date.now() + urlExpirationInSec * 1000), diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index be7f6a3d012..308bd05a89a 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -19,6 +19,7 @@ interface Copy { /** * Copy an object from a source to a destination object within the same bucket. * + * @param contextSpec - The isolated server context. * @param input - The CopyInputPath object. * @returns Output containing the destination object path. * @throws service: `S3Exception` - Thrown when checking for existence of the object @@ -36,6 +37,7 @@ interface Copy { * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across * different accessLevel or identityId (if source object's accessLevel is 'protected'). * + * @param contextSpec - The isolated server context. * @param input - The CopyInputKey object. * @returns Output containing the destination object key. * @throws service: `S3Exception` - Thrown when checking for existence of the object diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts index d56ee3d77f4..a3c7e0c254e 100644 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/server/getProperties.ts @@ -6,15 +6,57 @@ import { getAmplifyServerContext, } from '@aws-amplify/core/internals/adapter-core'; -import { GetPropertiesInput, GetPropertiesOutput } from '../../types'; +import { + GetPropertiesInput, + GetPropertiesInputKey, + GetPropertiesInputPath, + GetPropertiesOutput, + GetPropertiesOutputKey, + GetPropertiesOutputPath, +} from '../../types'; import { getProperties as getPropertiesInternal } from '../internal/getProperties'; -export const getProperties = ( +interface GetProperties { + /** + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetPropertiesInputPath` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: GetPropertiesInputPath, + ): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. + * + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetPropertiesInputKey` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: GetPropertiesInputKey, + ): Promise; +} + +export const getProperties: GetProperties = < + Output extends GetPropertiesOutput, +>( contextSpec: AmplifyServer.ContextSpec, input: GetPropertiesInput, -): Promise => { - return getPropertiesInternal( +): Promise => + getPropertiesInternal( getAmplifyServerContext(contextSpec).amplify, input, - ); -}; + ) as Promise; diff --git a/packages/storage/src/providers/s3/apis/server/getUrl.ts b/packages/storage/src/providers/s3/apis/server/getUrl.ts index 8cdfe2ffb4e..7843b6323cf 100644 --- a/packages/storage/src/providers/s3/apis/server/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/server/getUrl.ts @@ -6,12 +6,63 @@ import { getAmplifyServerContext, } from '@aws-amplify/core/internals/adapter-core'; -import { GetUrlInput, GetUrlOutput } from '../../types'; +import { + GetUrlInput, + GetUrlInputKey, + GetUrlInputPath, + GetUrlOutput, +} from '../../types'; import { getUrl as getUrlInternal } from '../internal/getUrl'; -export const getUrl = async ( +interface GetUrl { + /** + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetUrlInputPath` object. + * @returns Presigned URL and timestamp when the URL MAY expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: GetUrlInputPath, + ): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. + * + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetUrlInputKey` object. + * @returns Presigned URL and timestamp when the URL MAY expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: GetUrlInputKey, + ): Promise; +} +export const getUrl: GetUrl = async ( contextSpec: AmplifyServer.ContextSpec, input: GetUrlInput, -): Promise => { - return getUrlInternal(getAmplifyServerContext(contextSpec).amplify, input); -}; +): Promise => + getUrlInternal(getAmplifyServerContext(contextSpec).amplify, input); diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 559bb111afa..e8306cd507f 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 export { - GetUrlOptions, + GetUrlOptionsKey, + GetUrlOptionsPath, UploadDataOptions, - GetPropertiesOptions, + GetPropertiesOptionsKey, + GetPropertiesOptionsPath, ListAllOptions, ListPaginateOptions, RemoveOptions, @@ -23,6 +25,8 @@ export { ListAllOutput, ListPaginateOutput, GetPropertiesOutput, + GetPropertiesOutputKey, + GetPropertiesOutputPath, CopyOutput, CopyOutputKey, CopyOutputPath, @@ -33,7 +37,11 @@ export { CopyInputKey, CopyInputPath, GetPropertiesInput, + GetPropertiesInputKey, + GetPropertiesInputPath, GetUrlInput, + GetUrlInputKey, + GetUrlInputPath, ListAllInput, ListPaginateInput, RemoveInput, diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index abdd54a841c..22f3cd73095 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -8,8 +8,10 @@ import { StorageCopyInputPath, StorageDownloadDataInputKey, StorageDownloadDataInputPath, - StorageGetPropertiesInput, - StorageGetUrlInput, + StorageGetPropertiesInputKey, + StorageGetPropertiesInputPath, + StorageGetUrlInputKey, + StorageGetUrlInputPath, StorageListInput, StorageRemoveInput, StorageUploadDataInput, @@ -19,8 +21,10 @@ import { CopySourceOptionsKey, DownloadDataOptionsKey, DownloadDataOptionsPath, - GetPropertiesOptions, - GetUrlOptions, + GetPropertiesOptionsKey, + GetPropertiesOptionsPath, + GetUrlOptionsKey, + GetUrlOptionsPath, ListAllOptions, ListPaginateOptions, RemoveOptions, @@ -43,13 +47,24 @@ export type CopyInputPath = StorageCopyInputPath; /** * Input type for S3 getProperties API. */ -export type GetPropertiesInput = - StorageGetPropertiesInput; +export type GetPropertiesInput = StrictUnion< + GetPropertiesInputKey | GetPropertiesInputPath +>; + +/** @deprecated Use {@link GetPropertiesInputPath} instead. */ +export type GetPropertiesInputKey = + StorageGetPropertiesInputKey; +export type GetPropertiesInputPath = + StorageGetPropertiesInputPath; /** * Input type for S3 getUrl API. */ -export type GetUrlInput = StorageGetUrlInput; +export type GetUrlInput = StrictUnion; + +/** @deprecated Use {@link GetUrlInputPath} instead. */ +export type GetUrlInputKey = StorageGetUrlInputKey; +export type GetUrlInputPath = StorageGetUrlInputPath; /** * Input type for S3 list API. Lists all bucket objects. diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 7990f798aba..92f32ef0187 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -57,7 +57,9 @@ interface TransferOptions { /** * Input options type for S3 getProperties API. */ -export type GetPropertiesOptions = ReadOptions & CommonOptions; +/** @deprecated Use {@link GetPropertiesOptionsPath} instead. */ +export type GetPropertiesOptionsKey = ReadOptions & CommonOptions; +export type GetPropertiesOptionsPath = CommonOptions; /** * Input options type for S3 getProperties API. @@ -81,19 +83,22 @@ export type ListPaginateOptions = StorageListPaginateOptions & /** * Input options type for S3 getUrl API. */ -export type GetUrlOptions = ReadOptions & - CommonOptions & { - /** - * Whether to head object to make sure the object existence before downloading. - * @default false - */ - validateObjectExistence?: boolean; - /** - * Number of seconds till the URL expires. - * @default 900 (15 minutes) - */ - expiresIn?: number; - }; +export type GetUrlOptions = CommonOptions & { + /** + * Whether to head object to make sure the object existence before downloading. + * @default false + */ + validateObjectExistence?: boolean; + /** + * Number of seconds till the URL expires. + * @default 900 (15 minutes) + */ + expiresIn?: number; +}; + +/** @deprecated Use {@link GetUrlOptionsPath} instead. */ +export type GetUrlOptionsKey = ReadOptions & GetUrlOptions; +export type GetUrlOptionsPath = GetUrlOptions; /** * Input options type for S3 downloadData API. diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index c99f442f22a..d9aba46f502 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -61,10 +61,16 @@ export type GetUrlOutput = StorageGetUrlOutput; */ export type UploadDataOutput = UploadTask; +/** @deprecated Use {@link GetPropertiesOutputPath} instead. */ +export type GetPropertiesOutputKey = ItemKey; +export type GetPropertiesOutputPath = ItemPath; + /** * Output type for S3 getProperties API. */ -export type GetPropertiesOutput = ItemKey; +export type GetPropertiesOutput = + | GetPropertiesOutputKey + | GetPropertiesOutputPath; /** * Output type for S3 list API. Lists all bucket objects. diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index db2fac1628d..3722b7df804 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -10,14 +10,16 @@ export { export { StorageOperationInput, StorageListInput, - StorageGetPropertiesInput, + StorageGetPropertiesInputKey, + StorageGetPropertiesInputPath, StorageRemoveInput, StorageDownloadDataInputKey, StorageDownloadDataInputPath, StorageUploadDataInput, StorageCopyInputKey, StorageCopyInputPath, - StorageGetUrlInput, + StorageGetUrlInputKey, + StorageGetUrlInputPath, StorageUploadDataPayload, } from './inputs'; export { diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 57fd33b7c45..117b11aba19 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -40,8 +40,12 @@ export interface StorageOperationInput { options?: Options; } -export type StorageGetPropertiesInput = - StorageOperationInput; +/** @deprecated Use {@link StorageGetPropertiesInputPath} instead. */ +export type StorageGetPropertiesInputKey = + StorageOperationInputKey & StorageOperationInput; + +export type StorageGetPropertiesInputPath = StorageOperationInputPath & + StorageOperationOptionsInput; export interface StorageRemoveInput { key: string; @@ -55,8 +59,12 @@ export interface StorageListInput< options?: Options; } -export type StorageGetUrlInput = - StorageOperationInput; +/** @deprecated Use {@link StorageGetUrlInputPath} instead. */ +export type StorageGetUrlInputKey = + StorageOperationInputKey & StorageOperationInput; + +export type StorageGetUrlInputPath = StorageOperationInputPath & + StorageOperationOptionsInput; export type StorageUploadDataInput = StorageOperationInput & { From bef4777ec96d3e2de6f390addedc7bde8016a692 Mon Sep 17 00:00:00 2001 From: ManojNB Date: Mon, 25 Mar 2024 14:44:44 -0700 Subject: [PATCH 11/33] feat(storage): remove api takes path or key (#13115) --- .../__tests__/providers/s3/apis/list.test.ts | 2 +- .../providers/s3/apis/remove.test.ts | 125 ++++++++++++------ .../src/providers/s3/apis/internal/remove.ts | 38 ++++-- .../storage/src/providers/s3/apis/remove.ts | 47 +++++-- .../src/providers/s3/apis/server/remove.ts | 51 ++++++- .../storage/src/providers/s3/types/index.ts | 4 + .../storage/src/providers/s3/types/inputs.ts | 18 ++- .../storage/src/providers/s3/types/outputs.ts | 13 +- .../s3/utils/validateStorageOperationInput.ts | 1 + packages/storage/src/types/index.ts | 3 +- packages/storage/src/types/inputs.ts | 9 +- 11 files changed, 238 insertions(+), 73 deletions(-) diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index 681b4ff1af9..d96f588b985 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -55,7 +55,7 @@ const listResultItem = { lastModified, size, }; -const mockListObjectsV2ApiWithPages = pages => { +const mockListObjectsV2ApiWithPages = (pages: number) => { let methodCalls = 0; mockListObject.mockClear(); mockListObject.mockImplementation(async (_, input) => { diff --git a/packages/storage/__tests__/providers/s3/apis/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/remove.test.ts index 0f2e1a65b17..aa1cdaa2b3c 100644 --- a/packages/storage/__tests__/providers/s3/apis/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/remove.test.ts @@ -6,6 +6,7 @@ import { Amplify } from '@aws-amplify/core'; import { deleteObject } from '../../../../src/providers/s3/utils/client'; import { remove } from '../../../../src/providers/s3/apis'; import { StorageOptions } from '../../../../src/types'; +import { StorageValidationErrorCode } from '../../../../src/errors/types/validation'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -26,7 +27,7 @@ const key = 'key'; const bucket = 'bucket'; const region = 'region'; const defaultIdentityId = 'defaultIdentityId'; -const removeResult = { key }; +const removeResultKey = { key }; const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -53,45 +54,85 @@ describe('remove API', () => { }, }); }); - describe('Happy Path Cases:', () => { - beforeEach(() => { - mockDeleteObject.mockImplementation(() => { - return { - Metadata: { key: 'value' }, - }; + describe('Happy Cases', () => { + describe('With Key', () => { + beforeEach(() => { + mockDeleteObject.mockImplementation(() => { + return { + Metadata: { key: 'value' }, + }; + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + [ + { + expectedKey: `public/${key}`, + }, + { + options: { accessLevel: 'guest' }, + expectedKey: `public/${key}`, + }, + { + options: { accessLevel: 'private' }, + expectedKey: `private/${defaultIdentityId}/${key}`, + }, + { + options: { accessLevel: 'protected' }, + expectedKey: `protected/${defaultIdentityId}/${key}`, + }, + ].forEach(({ options, expectedKey }) => { + const accessLevel = options?.accessLevel ?? 'default'; + + it(`should remove object with ${accessLevel} accessLevel`, async () => { + expect.assertions(3); + expect( + await remove({ key, options: options as StorageOptions }), + ).toEqual(removeResultKey); + expect(deleteObject).toHaveBeenCalledTimes(1); + expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { + Bucket: bucket, + Key: expectedKey, + }); + }); }); }); - afterEach(() => { - jest.clearAllMocks(); - }); - [ - { - expectedKey: `public/${key}`, - }, - { - options: { accessLevel: 'guest' }, - expectedKey: `public/${key}`, - }, - { - options: { accessLevel: 'private' }, - expectedKey: `private/${defaultIdentityId}/${key}`, - }, - { - options: { accessLevel: 'protected' }, - expectedKey: `protected/${defaultIdentityId}/${key}`, - }, - ].forEach(({ options, expectedKey }) => { - const accessLevel = options?.accessLevel ?? 'default'; + describe('With Path', () => { + const resolvePath = (path: string | Function) => + typeof path === 'string' + ? path + : path({ identityId: defaultIdentityId }); + beforeEach(() => { + mockDeleteObject.mockImplementation(() => { + return { + Metadata: { key: 'value' }, + }; + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + [ + { + path: `public/${key}`, + }, + { + path: ({ identityId }: any) => `protected/${identityId}/${key}`, + }, + ].forEach(({ path }) => { + const removeResultPath = { + path: resolvePath(path), + }; - it(`should remove object with ${accessLevel} accessLevel`, async () => { - expect.assertions(3); - expect( - await remove({ key, options: options as StorageOptions }), - ).toEqual(removeResult); - expect(deleteObject).toHaveBeenCalledTimes(1); - expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { - Bucket: bucket, - Key: expectedKey, + it(`should remove object for the given path`, async () => { + expect.assertions(3); + expect(await remove({ path })).toEqual(removeResultPath); + expect(deleteObject).toHaveBeenCalledTimes(1); + expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { + Bucket: bucket, + Key: resolvePath(path), + }); }); }); }); @@ -121,5 +162,15 @@ describe('remove API', () => { expect(error.$metadata.httpStatusCode).toBe(404); } }); + it('should throw InvalidStorageOperationInput error when the path is empty', async () => { + expect.assertions(1); + try { + await remove({ path: '' }); + } catch (error: any) { + expect(error.name).toBe( + StorageValidationErrorCode.InvalidStorageOperationInput, + ); + } + }); }); }); diff --git a/packages/storage/src/providers/s3/apis/internal/remove.ts b/packages/storage/src/providers/s3/apis/internal/remove.ts index 7eae6bc5854..0b38095b7ce 100644 --- a/packages/storage/src/providers/s3/apis/internal/remove.ts +++ b/packages/storage/src/providers/s3/apis/internal/remove.ts @@ -5,23 +5,37 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { RemoveInput, RemoveOutput } from '../../types'; -import { resolveS3ConfigAndInput } from '../../utils'; +import { + resolveS3ConfigAndInput, + validateStorageOperationInput, +} from '../../utils'; import { deleteObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; +import { STORAGE_INPUT_KEY } from '../../utils/constants'; export const remove = async ( amplify: AmplifyClassV6, input: RemoveInput, ): Promise => { - const { key, options = {} } = input; - const { s3Config, keyPrefix, bucket } = await resolveS3ConfigAndInput( - amplify, - options, + const { options = {} } = input ?? {}; + const { s3Config, keyPrefix, bucket, identityId } = + await resolveS3ConfigAndInput(amplify, options); + + const { inputType, objectKey } = validateStorageOperationInput( + input, + identityId, ); - const finalKey = `${keyPrefix}${key}`; - logger.debug(`remove "${key}" from "${finalKey}".`); + let finalKey; + if (inputType === STORAGE_INPUT_KEY) { + finalKey = `${keyPrefix}${objectKey}`; + logger.debug(`remove "${objectKey}" from "${finalKey}".`); + } else { + finalKey = objectKey; + logger.debug(`removing object in path "${finalKey}"`); + } + await deleteObject( { ...s3Config, @@ -33,7 +47,11 @@ export const remove = async ( }, ); - return { - key, - }; + return inputType === STORAGE_INPUT_KEY + ? { + key: objectKey, + } + : { + path: objectKey, + }; }; diff --git a/packages/storage/src/providers/s3/apis/remove.ts b/packages/storage/src/providers/s3/apis/remove.ts index bda77060f5f..3688dfb4041 100644 --- a/packages/storage/src/providers/s3/apis/remove.ts +++ b/packages/storage/src/providers/s3/apis/remove.ts @@ -3,18 +3,41 @@ import { Amplify } from '@aws-amplify/core'; -import { RemoveInput, RemoveOutput, S3Exception } from '../types'; -import { StorageValidationErrorCode } from '../../../errors/types/validation'; +import { + RemoveInput, + RemoveInputKey, + RemoveInputPath, + RemoveOutput, + RemoveOutputKey, + RemoveOutputPath, +} from '../types'; import { remove as removeInternal } from './internal/remove'; -/** - * Remove a file from your S3 bucket. - * @param input - The RemoveInput object. - * @return Output containing the removed object key - * @throws service: {@link S3Exception} - S3 service errors thrown while getting properties - * @throws validation: {@link StorageValidationErrorCode } - Validation errors thrown - */ -export const remove = (input: RemoveInput): Promise => { - return removeInternal(Amplify, input); -}; +interface RemoveApi { + /** + * Remove a file from your S3 bucket. + * @param input - The `RemoveInputPath` object. + * @return Output containing the removed object path. + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no path or path is empty or path has a leading slash. + */ + (input: RemoveInputPath): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. + * + * Remove a file from your S3 bucket. + * @param input - The `RemoveInputKey` object. + * @return Output containing the removed object key + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no key or its empty. + */ + (input: RemoveInputKey): Promise; +} + +export const remove: RemoveApi = ( + input: RemoveInput, +): Promise => removeInternal(Amplify, input) as Promise; diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts index 8dbd45bfd19..5ef81d0f526 100644 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ b/packages/storage/src/providers/s3/apis/server/remove.ts @@ -6,12 +6,53 @@ import { getAmplifyServerContext, } from '@aws-amplify/core/internals/adapter-core'; -import { RemoveInput, RemoveOutput } from '../../types'; +import { + RemoveInput, + RemoveInputKey, + RemoveInputPath, + RemoveOutput, + RemoveOutputKey, + RemoveOutputPath, +} from '../../types'; import { remove as removeInternal } from '../internal/remove'; -export const remove = ( +interface RemoveApi { + /** + * Remove a file from your S3 bucket. + * @param input - The `RemoveInputPath` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @return Output containing the removed object path. + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no path or path is empty or path has a leading slash. + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: RemoveInputPath, + ): Promise; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. + * + * Remove a file from your S3 bucket. + * @param input - The `RemoveInputKey` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @return Output containing the removed object key + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no key or its empty. + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: RemoveInputKey, + ): Promise; +} + +export const remove: RemoveApi = ( contextSpec: AmplifyServer.ContextSpec, input: RemoveInput, -): Promise => { - return removeInternal(getAmplifyServerContext(contextSpec).amplify, input); -}; +): Promise => + removeInternal( + getAmplifyServerContext(contextSpec).amplify, + input, + ) as Promise; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index e8306cd507f..8aa5b900587 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -31,6 +31,8 @@ export { CopyOutputKey, CopyOutputPath, RemoveOutput, + RemoveOutputKey, + RemoveOutputPath, } from './outputs'; export { CopyInput, @@ -44,6 +46,8 @@ export { GetUrlInputPath, ListAllInput, ListPaginateInput, + RemoveInputKey, + RemoveInputPath, RemoveInput, DownloadDataInput, DownloadDataInputKey, diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index 22f3cd73095..bec0b9e17db 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -13,7 +13,8 @@ import { StorageGetUrlInputKey, StorageGetUrlInputPath, StorageListInput, - StorageRemoveInput, + StorageRemoveInputKey, + StorageRemoveInputPath, StorageUploadDataInput, } from '../../../types'; import { @@ -76,10 +77,23 @@ export type ListAllInput = StorageListInput; */ export type ListPaginateInput = StorageListInput; +/** + * @deprecated Use {@link RemoveInputPath} instead. + * Input type with key for S3 remove API. + */ +export type RemoveInputKey = StorageRemoveInputKey; + +/** + * Input type with path for S3 remove API. + */ +export type RemoveInputPath = StorageRemoveInputPath< + Omit +>; + /** * Input type for S3 remove API. */ -export type RemoveInput = StorageRemoveInput; +export type RemoveInput = StrictUnion; /** * Input type for S3 downloadData API. diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index d9aba46f502..7fe3cd5ade6 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -95,4 +95,15 @@ export type CopyOutput = StrictUnion; /** * Output type for S3 remove API. */ -export type RemoveOutput = Pick; +export type RemoveOutput = StrictUnion; + +/** + * @deprecated Use {@link RemoveOutputPath} instead. + * Output helper type with key for S3 remove API. + */ +export type RemoveOutputKey = Pick; + +/** + * Output helper type with path for S3 remove API. + */ +export type RemoveOutputPath = Pick; diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts index 12e71f10fdb..a14781a25eb 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -20,6 +20,7 @@ export const validateStorageOperationInput = ( if (isInputWithPath(input)) { const { path } = input; const objectKey = typeof path === 'string' ? path : path({ identityId }); + assertValidationError( !objectKey.startsWith('/'), StorageValidationErrorCode.InvalidStoragePathInput, diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 3722b7df804..daa3eb466d9 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -12,7 +12,8 @@ export { StorageListInput, StorageGetPropertiesInputKey, StorageGetPropertiesInputPath, - StorageRemoveInput, + StorageRemoveInputPath, + StorageRemoveInputKey, StorageDownloadDataInputKey, StorageDownloadDataInputPath, StorageUploadDataInput, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 117b11aba19..6139f95877d 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -47,10 +47,11 @@ export type StorageGetPropertiesInputKey = export type StorageGetPropertiesInputPath = StorageOperationInputPath & StorageOperationOptionsInput; -export interface StorageRemoveInput { - key: string; - options?: Options; -} +export type StorageRemoveInputKey = StorageOperationInputKey & + StorageOperationOptionsInput; + +export type StorageRemoveInputPath = StorageOperationInputPath & + StorageOperationOptionsInput; export interface StorageListInput< Options extends StorageListAllOptions | StorageListPaginateOptions, From db028e7a821e0feb124f7adf87476b69ee3b0438 Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Tue, 26 Mar 2024 10:57:21 -0500 Subject: [PATCH 12/33] chore: Add validation to ensure key & path not specified at the same time (#13163) --- packages/aws-amplify/package.json | 6 +++--- .../apis/utils/validateStorageOperationInput.test.ts | 11 ++++++++++- packages/storage/src/errors/types/validation.ts | 3 ++- .../s3/utils/validateStorageOperationInput.ts | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 042b46881be..e3c275cdb58 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -461,19 +461,19 @@ "name": "[Storage] copy (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ copy }", - "limit": "13.61 kB" + "limit": "13.69 kB" }, { "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "14.23 kB" + "limit": "14.32 kB" }, { "name": "[Storage] getProperties (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getProperties }", - "limit": "13.50 kB" + "limit": "13.53 kB" }, { "name": "[Storage] getUrl (S3)", diff --git a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts index cf9f38318d1..14b1f6204f0 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts @@ -51,7 +51,7 @@ describe('validateStorageOperationInput', () => { ); }); - it('should throw an error when input is invalid', () => { + it('should throw an error when key and path are not specified', () => { const input = { invalid: 'test' } as any; expect(() => validateStorageOperationInput(input)).toThrow( validationErrorMap[ @@ -59,4 +59,13 @@ describe('validateStorageOperationInput', () => { ].message, ); }); + + it('should throw an error when both key & path are specified', () => { + const input = { path: 'testPath/object', key: 'key' } as any; + expect(() => validateStorageOperationInput(input)).toThrow( + validationErrorMap[ + StorageValidationErrorCode.InvalidStorageOperationInput + ].message, + ); + }); }); diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index 01954a896ec..d2a999d4c40 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -60,7 +60,8 @@ export const validationErrorMap: AmplifyErrorMap = { 'Upload source type can only be a `Blob`, `File`, `ArrayBuffer`, or `string`.', }, [StorageValidationErrorCode.InvalidStorageOperationInput]: { - message: 'Missing path or key parameter in Input.', + message: + 'Path or key parameter must be specified in the input. Both can not be specified at the same time.', }, [StorageValidationErrorCode.InvalidStoragePathInput]: { message: 'Input `path` does not allow a leading slash (/).', diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts index a14781a25eb..12ff4eb0c2e 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -13,7 +13,10 @@ export const validateStorageOperationInput = ( identityId?: string, ) => { assertValidationError( - !!(input as Input).key || !!(input as Input).path, + // Key present without a path + (!!(input as Input).key && !(input as Input).path) || + // Path present without a key + (!(input as Input).key && !!(input as Input).path), StorageValidationErrorCode.InvalidStorageOperationInput, ); From 7da2b89404ba47b1a3f774a4b799e591b8cbebfb Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Tue, 26 Mar 2024 11:28:38 -0500 Subject: [PATCH 13/33] chore: Update bundle size limit for `getUrl` (#13180) chore: Update bundle size limit for `getUrl`. --- packages/aws-amplify/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index e3c275cdb58..c0c06609614 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -479,7 +479,7 @@ "name": "[Storage] getUrl (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getUrl }", - "limit": "14.60 kB" + "limit": "14.61 kB" }, { "name": "[Storage] list (S3)", From 872a8171d4b30a8b751e08bd64170d074beb9739 Mon Sep 17 00:00:00 2001 From: ManojNB Date: Tue, 26 Mar 2024 16:59:49 -0700 Subject: [PATCH 14/33] feat(storage): list API to accept both prefix and path (#13100) --- packages/aws-amplify/package.json | 8 +- .../__tests__/providers/s3/apis/list.test.ts | 229 ++++++++++++++---- ...ateStorageOperationInputWithPrefix.test.ts | 70 ++++++ .../storage/src/errors/types/validation.ts | 4 + .../src/providers/s3/apis/internal/list.ts | 145 +++++++++-- .../storage/src/providers/s3/apis/list.ts | 66 +++-- .../src/providers/s3/apis/server/list.ts | 81 +++++-- .../storage/src/providers/s3/types/index.ts | 21 +- .../storage/src/providers/s3/types/inputs.ts | 40 ++- .../storage/src/providers/s3/types/options.ts | 25 +- .../storage/src/providers/s3/types/outputs.ts | 47 +++- .../s3/utils/client/utils/parsePayload.ts | 2 +- .../src/providers/s3/utils/constants.ts | 1 + .../storage/src/providers/s3/utils/index.ts | 1 + ...validateStorageOperationInputWithPrefix.ts | 46 ++++ packages/storage/src/types/index.ts | 3 +- packages/storage/src/types/inputs.ts | 21 +- packages/storage/src/types/outputs.ts | 3 + 18 files changed, 673 insertions(+), 140 deletions(-) create mode 100644 packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInputWithPrefix.test.ts create mode 100644 packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index c0c06609614..b52f306a08f 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -473,19 +473,19 @@ "name": "[Storage] getProperties (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getProperties }", - "limit": "13.53 kB" + "limit": "13.55 kB" }, { "name": "[Storage] getUrl (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getUrl }", - "limit": "14.61 kB" + "limit": "14.63 kB" }, { "name": "[Storage] list (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ list }", - "limit": "14.00 kB" + "limit": "14.2 kB" }, { "name": "[Storage] remove (S3)", @@ -497,7 +497,7 @@ "name": "[Storage] uploadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ uploadData }", - "limit": "18.50 kB" + "limit": "18.52 kB" } ] } diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index d96f588b985..7f1446c7c6c 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -6,9 +6,10 @@ import { Amplify } from '@aws-amplify/core'; import { listObjectsV2 } from '../../../../src/providers/s3/utils/client'; import { list } from '../../../../src/providers/s3'; import { - ListAllOptions, - ListPaginateOptions, + ListAllOptionsPrefix, + ListPaginateOptionsPrefix, } from '../../../../src/providers/s3/types'; +import { StorageValidationErrorCode } from '../../../../src/errors/types/validation'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -26,7 +27,6 @@ const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; const mockListObject = listObjectsV2 as jest.Mock; const key = 'path/itemsKey'; -const path = key; const bucket = 'bucket'; const region = 'region'; const nextToken = 'nextToken'; @@ -89,7 +89,7 @@ describe('list API', () => { }, }); }); - describe('Happy Cases:', () => { + describe('Prefix: Happy Cases:', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -99,33 +99,37 @@ describe('list API', () => { expectedPath: `public/`, }, { - path, - expectedPath: `public/${path}`, + options: { accessLevel: 'guest' }, + expectedPath: `public/`, + }, + { + key, + expectedPath: `public/${key}`, }, { - path, + key, options: { accessLevel: 'guest' }, - expectedPath: `public/${path}`, + expectedPath: `public/${key}`, }, { - path, + key, options: { accessLevel: 'private' }, - expectedPath: `private/${defaultIdentityId}/${path}`, + expectedPath: `private/${defaultIdentityId}/${key}`, }, { - path, + key, options: { accessLevel: 'protected' }, - expectedPath: `protected/${defaultIdentityId}/${path}`, + expectedPath: `protected/${defaultIdentityId}/${key}`, }, { - path, + key, options: { accessLevel: 'protected', targetIdentityId }, - expectedPath: `protected/${targetIdentityId}/${path}`, + expectedPath: `protected/${targetIdentityId}/${key}`, }, ]; - accessLevelTests.forEach(({ path, options, expectedPath }) => { - const pathMsg = path ? 'custom' : 'default'; + accessLevelTests.forEach(({ key, options, expectedPath }) => { + const pathMsg = key ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` @@ -139,14 +143,11 @@ describe('list API', () => { NextContinuationToken: nextToken, }; }); - expect.assertions(4); let response = await list({ - prefix: path, - options: options as ListPaginateOptions, + prefix: key, + options: options as ListPaginateOptionsPrefix, }); - expect(response.items).toEqual([ - { ...listResultItem, key: path ?? '' }, - ]); + expect(response.items).toEqual([{ ...listResultItem, key: key ?? '' }]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { @@ -157,8 +158,8 @@ describe('list API', () => { }); }); - accessLevelTests.forEach(({ path, options, expectedPath }) => { - const pathMsg = path ? 'custom' : 'default'; + accessLevelTests.forEach(({ key, options, expectedPath }) => { + const pathMsg = key ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` @@ -172,19 +173,16 @@ describe('list API', () => { NextContinuationToken: nextToken, }; }); - expect.assertions(4); const customPageSize = 5; const response = await list({ - prefix: path, + prefix: key, options: { - ...(options as ListPaginateOptions), + ...(options as ListPaginateOptionsPrefix), pageSize: customPageSize, nextToken: nextToken, }, }); - expect(response.items).toEqual([ - { ...listResultItem, key: path ?? '' }, - ]); + expect(response.items).toEqual([{ ...listResultItem, key: key ?? '' }]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { @@ -196,8 +194,8 @@ describe('list API', () => { }); }); - accessLevelTests.forEach(({ path, options, expectedPath }) => { - const pathMsg = path ? 'custom' : 'default'; + accessLevelTests.forEach(({ key, options, expectedPath }) => { + const pathMsg = key ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` @@ -206,13 +204,12 @@ describe('list API', () => { mockListObject.mockImplementationOnce(() => { return {}; }); - expect.assertions(3); let response = await list({ - prefix: path, - options: options as ListPaginateOptions, + prefix: key, + options: options as ListPaginateOptionsPrefix, }); expect(response.items).toEqual([]); - // + expect(response.nextToken).toEqual(undefined); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, @@ -222,21 +219,20 @@ describe('list API', () => { }); }); - accessLevelTests.forEach(({ path, options, expectedPath }) => { - const pathMsg = path ? 'custom' : 'default'; + accessLevelTests.forEach(({ key, options, expectedPath }) => { + const pathMsg = key ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` : ''; it(`should list all objects having three pages with ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { - expect.assertions(5); mockListObjectsV2ApiWithPages(3); const result = await list({ - prefix: path, - options: { ...options, listAll: true } as ListAllOptions, + prefix: key, + options: { ...options, listAll: true } as ListAllOptionsPrefix, }); - const listResult = { ...listResultItem, key: path ?? '' }; + const listResult = { ...listResultItem, key: key ?? '' }; expect(result.items).toEqual([listResult, listResult, listResult]); expect(result).not.toHaveProperty(nextToken); @@ -269,6 +265,153 @@ describe('list API', () => { }); }); + describe('Path: Happy Cases:', () => { + const resolvePath = (path: string | Function) => + typeof path === 'string' ? path : path({ identityId: defaultIdentityId }); + afterEach(() => { + jest.clearAllMocks(); + mockListObject.mockClear(); + }); + const pathAsFunctionAndStringTests = [ + { + path: `public/${key}`, + }, + { + path: ({ identityId }: any) => `protected/${identityId}/${key}`, + }, + ]; + + it.each(pathAsFunctionAndStringTests)( + 'should list objects with pagination, default pageSize, custom path', + async ({ path }) => { + mockListObject.mockImplementationOnce(() => { + return { + Contents: [ + { + ...listObjectClientBaseResultItem, + Key: resolvePath(path), + }, + ], + NextContinuationToken: nextToken, + }; + }); + let response = await list({ + path, + }); + expect(response.items).toEqual([ + { ...listResultItem, path: resolvePath(path) }, + ]); + expect(response.nextToken).toEqual(nextToken); + expect(listObjectsV2).toHaveBeenCalledTimes(1); + expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { + Bucket: bucket, + MaxKeys: 1000, + Prefix: resolvePath(path), + }); + }, + ); + + it.each(pathAsFunctionAndStringTests)( + 'should list objects with pagination using custom pageSize, nextToken and custom path: ${path}', + async ({ path }) => { + mockListObject.mockImplementationOnce(() => { + return { + Contents: [ + { + ...listObjectClientBaseResultItem, + Key: resolvePath(path), + }, + ], + NextContinuationToken: nextToken, + }; + }); + const customPageSize = 5; + const response = await list({ + path, + options: { + pageSize: customPageSize, + nextToken: nextToken, + }, + }); + expect(response.items).toEqual([ + { ...listResultItem, path: resolvePath(path) ?? '' }, + ]); + expect(response.nextToken).toEqual(nextToken); + expect(listObjectsV2).toHaveBeenCalledTimes(1); + expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { + Bucket: bucket, + Prefix: resolvePath(path), + ContinuationToken: nextToken, + MaxKeys: customPageSize, + }); + }, + ); + + it.each(pathAsFunctionAndStringTests)( + 'should list objects with zero results with custom path: ${path}', + async ({ path }) => { + mockListObject.mockImplementationOnce(() => { + return {}; + }); + let response = await list({ + path, + }); + expect(response.items).toEqual([]); + + expect(response.nextToken).toEqual(undefined); + expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { + Bucket: bucket, + MaxKeys: 1000, + Prefix: resolvePath(path), + }); + }, + ); + + it.each(pathAsFunctionAndStringTests)( + 'should list all objects having three pages with custom path: ${path}', + async ({ path }) => { + mockListObjectsV2ApiWithPages(3); + const result = await list({ + path, + options: { listAll: true }, + }); + + const listResult = { + ...listResultItem, + path: resolvePath(path), + }; + expect(result.items).toEqual([listResult, listResult, listResult]); + expect(result).not.toHaveProperty(nextToken); + + // listing three times for three pages + expect(listObjectsV2).toHaveBeenCalledTimes(3); + + // first input recieves undefined as the Continuation Token + expect(listObjectsV2).toHaveBeenNthCalledWith( + 1, + listObjectClientConfig, + { + Bucket: bucket, + Prefix: resolvePath(path), + MaxKeys: 1000, + ContinuationToken: undefined, + }, + ); + // last input recieves TEST_TOKEN as the Continuation Token + expect(listObjectsV2).toHaveBeenNthCalledWith( + 3, + listObjectClientConfig, + { + Bucket: bucket, + Prefix: resolvePath(path), + MaxKeys: 1000, + ContinuationToken: nextToken, + }, + ); + }, + ); + }); + describe('Error Cases:', () => { afterEach(() => { jest.clearAllMocks(); @@ -280,10 +423,10 @@ describe('list API', () => { name: 'NotFound', }), ); - expect.assertions(3); try { await list({}); } catch (error: any) { + expect.assertions(3); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, diff --git a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInputWithPrefix.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInputWithPrefix.test.ts new file mode 100644 index 00000000000..3be7aa1b50d --- /dev/null +++ b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInputWithPrefix.test.ts @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + StorageValidationErrorCode, + validationErrorMap, +} from '../../../../../src/errors/types/validation'; +import { validateStorageOperationInputWithPrefix } from '../../../../../src/providers/s3/utils'; +import { + STORAGE_INPUT_PATH, + STORAGE_INPUT_PREFIX, +} from '../../../../../src/providers/s3/utils/constants'; + +describe('validateStorageOperationInputWithPrefix', () => { + it('should return inputType as STORAGE_INPUT_PATH and objectKey as testPath when input is path as string', () => { + const input = { path: 'testPath' }; + const result = validateStorageOperationInputWithPrefix(input); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'testPath', + }); + }); + + it('should return inputType as STORAGE_INPUT_PATH and objectKey as result of path function when input is path as function', () => { + const input = { + path: ({ identityId }: { identityId?: string }) => + `testPath/${identityId}`, + }; + const result = validateStorageOperationInputWithPrefix(input, '123'); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'testPath/123', + }); + }); + + it('should return inputType as STORAGE_INPUT_PREFIX and objectKey as testKey when input is prefix', () => { + const input = { prefix: 'testKey' }; + const result = validateStorageOperationInputWithPrefix(input); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PREFIX, + objectKey: 'testKey', + }); + }); + + it('should take a default prefix when input has invalid objects', () => { + const input = { invalid: 'test' } as any; + const result = validateStorageOperationInputWithPrefix(input); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PREFIX, + objectKey: '', + }); + }); + + it('should throw an error when input path starts with a /', () => { + const input = { path: '/test' } as any; + expect(() => validateStorageOperationInputWithPrefix(input)).toThrow( + validationErrorMap[StorageValidationErrorCode.InvalidStoragePathInput] + .message, + ); + }); + + it('should throw an error when input has both path and prefix', () => { + const input = { prefix: 'testPrefix', path: 'test' } as any; + expect(() => validateStorageOperationInputWithPrefix(input)).toThrow( + validationErrorMap[ + StorageValidationErrorCode.InvalidStorageOperationPrefixInput + ].message, + ); + }); +}); diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index d2a999d4c40..d72b9852162 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -13,6 +13,7 @@ export enum StorageValidationErrorCode { NoDestinationPath = 'NoDestinationPath', NoBucket = 'NoBucket', NoRegion = 'NoRegion', + InvalidStorageOperationPrefixInput = 'InvalidStorageOperationPrefixInput', InvalidStorageOperationInput = 'InvalidStorageOperationInput', InvalidStoragePathInput = 'InvalidStoragePathInput', InvalidUploadSource = 'InvalidUploadSource', @@ -63,6 +64,9 @@ export const validationErrorMap: AmplifyErrorMap = { message: 'Path or key parameter must be specified in the input. Both can not be specified at the same time.', }, + [StorageValidationErrorCode.InvalidStorageOperationPrefixInput]: { + message: 'Both path and prefix can not be specified at the same time.', + }, [StorageValidationErrorCode.InvalidStoragePathInput]: { message: 'Input `path` does not allow a leading slash (/).', }, diff --git a/packages/storage/src/providers/s3/apis/internal/list.ts b/packages/storage/src/providers/s3/apis/internal/list.ts index 1d49ed5942b..c11a8a440f4 100644 --- a/packages/storage/src/providers/s3/apis/internal/list.ts +++ b/packages/storage/src/providers/s3/apis/internal/list.ts @@ -7,11 +7,19 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { ListAllInput, ListAllOutput, - ListOutputItem, + ListAllOutputPath, + ListAllOutputPrefix, + ListOutputItemKey, + ListOutputItemPath, ListPaginateInput, ListPaginateOutput, + ListPaginateOutputPath, + ListPaginateOutputPrefix, } from '../../types'; -import { resolveS3ConfigAndInput } from '../../utils'; +import { + resolveS3ConfigAndInput, + validateStorageOperationInputWithPrefix, +} from '../../utils'; import { ResolvedS3Config } from '../../types/options'; import { ListObjectsV2Input, @@ -20,25 +28,34 @@ import { } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; +import { STORAGE_INPUT_PREFIX } from '../../utils/constants'; const MAX_PAGE_SIZE = 1000; interface ListInputArgs { s3Config: ResolvedS3Config; listParams: ListObjectsV2Input; - prefix: string; + generatedPrefix?: string; } export const list = async ( amplify: AmplifyClassV6, - input?: ListAllInput | ListPaginateInput, + input: ListAllInput | ListPaginateInput, ): Promise => { - const { options = {}, prefix: path = '' } = input ?? {}; + const { options = {} } = input; const { s3Config, bucket, - keyPrefix: prefix, + keyPrefix: generatedPrefix, + identityId, } = await resolveS3ConfigAndInput(amplify, options); + + const { inputType, objectKey } = validateStorageOperationInputWithPrefix( + input, + identityId, + ); + const isInputWithPrefix = inputType === STORAGE_INPUT_PREFIX; + // @ts-expect-error pageSize and nextToken should not coexist with listAll if (options?.listAll && (options?.pageSize || options?.nextToken)) { const anyOptions = options as any; @@ -50,27 +67,45 @@ export const list = async ( } const listParams = { Bucket: bucket, - Prefix: `${prefix}${path}`, + Prefix: isInputWithPrefix ? `${generatedPrefix}${objectKey}` : objectKey, MaxKeys: options?.listAll ? undefined : options?.pageSize, ContinuationToken: options?.listAll ? undefined : options?.nextToken, }; logger.debug(`listing items from "${listParams.Prefix}"`); - return options.listAll - ? _listAll({ s3Config, listParams, prefix }) - : _list({ s3Config, listParams, prefix }); + const listInputArgs: ListInputArgs = { + s3Config, + listParams, + }; + if (options.listAll) { + if (isInputWithPrefix) { + return _listAllPrefix({ + ...listInputArgs, + generatedPrefix, + }); + } else { + return _listAllPath(listInputArgs); + } + } else { + if (inputType === STORAGE_INPUT_PREFIX) { + return _listPrefix({ ...listInputArgs, generatedPrefix }); + } else { + return _listPath(listInputArgs); + } + } }; -const _listAll = async ({ +/** @deprecated Use {@link _listAllPath} instead. */ +const _listAllPrefix = async ({ s3Config, listParams, - prefix, -}: ListInputArgs): Promise => { - const listResult: ListOutputItem[] = []; + generatedPrefix, +}: ListInputArgs): Promise => { + const listResult: ListOutputItemKey[] = []; let continuationToken = listParams.ContinuationToken; do { - const { items: pageResults, nextToken: pageNextToken } = await _list({ - prefix, + const { items: pageResults, nextToken: pageNextToken } = await _listPrefix({ + generatedPrefix, s3Config, listParams: { ...listParams, @@ -87,11 +122,12 @@ const _listAll = async ({ }; }; -const _list = async ({ +/** @deprecated Use {@link _listPath} instead. */ +const _listPrefix = async ({ s3Config, listParams, - prefix, -}: ListInputArgs): Promise => { + generatedPrefix, +}: ListInputArgs): Promise => { const listParamsClone = { ...listParams }; if (!listParamsClone.MaxKeys || listParamsClone.MaxKeys > MAX_PAGE_SIZE) { logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`); @@ -112,15 +148,74 @@ const _list = async ({ }; } - const listResult = response.Contents.map(item => ({ - key: item.Key!.substring(prefix.length), - eTag: item.ETag, - lastModified: item.LastModified, - size: item.Size, - })); + return { + items: response.Contents.map(item => ({ + key: generatedPrefix + ? item.Key!.substring(generatedPrefix.length) + : item.Key!, + eTag: item.ETag, + lastModified: item.LastModified, + size: item.Size, + })), + nextToken: response.NextContinuationToken, + }; +}; + +const _listAllPath = async ({ + s3Config, + listParams, +}: ListInputArgs): Promise => { + const listResult: ListOutputItemPath[] = []; + let continuationToken = listParams.ContinuationToken; + do { + const { items: pageResults, nextToken: pageNextToken } = await _listPath({ + s3Config, + listParams: { + ...listParams, + ContinuationToken: continuationToken, + MaxKeys: MAX_PAGE_SIZE, + }, + }); + listResult.push(...pageResults); + continuationToken = pageNextToken; + } while (continuationToken); return { items: listResult, + }; +}; + +const _listPath = async ({ + s3Config, + listParams, +}: ListInputArgs): Promise => { + const listParamsClone = { ...listParams }; + if (!listParamsClone.MaxKeys || listParamsClone.MaxKeys > MAX_PAGE_SIZE) { + logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`); + listParamsClone.MaxKeys = MAX_PAGE_SIZE; + } + + const response: ListObjectsV2Output = await listObjectsV2( + { + ...s3Config, + userAgentValue: getStorageUserAgentValue(StorageAction.List), + }, + listParamsClone, + ); + + if (!response?.Contents) { + return { + items: [], + }; + } + + return { + items: response.Contents.map(item => ({ + path: item.Key!, + eTag: item.ETag, + lastModified: item.LastModified, + size: item.Size, + })), nextToken: response.NextContinuationToken, }; }; diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index 0778252e34d..7fce787b9ea 100644 --- a/packages/storage/src/providers/s3/apis/list.ts +++ b/packages/storage/src/providers/s3/apis/list.ts @@ -1,41 +1,67 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - import { Amplify } from '@aws-amplify/core'; import { ListAllInput, + ListAllInputPath, + ListAllInputPrefix, ListAllOutput, + ListAllOutputPath, + ListAllOutputPrefix, ListPaginateInput, + ListPaginateInputPath, + ListPaginateInputPrefix, ListPaginateOutput, - S3Exception, + ListPaginateOutputPath, + ListPaginateOutputPrefix, } from '../types'; -import { StorageValidationErrorCode } from '../../../errors/types/validation'; import { list as listInternal } from './internal/list'; interface ListApi { /** - * List files with given prefix in pages - * pageSize defaulted to 1000. Additionally, the result will include a nextToken if there are more items to retrieve. - * @param input - The ListPaginateInput object. - * @returns A list of keys and metadata with - * @throws service: {@link S3Exception} - S3 service errors thrown when checking for existence of bucket - * @throws validation: {@link StorageValidationErrorCode } - thrown when there are issues with credentials + * List files in pages with the given `path`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateInputPath` object. + * @returns A list of objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ + (input: ListPaginateInputPath): Promise; + /** + * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllInputPath` object. + * @returns A list of all objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ + (input: ListAllInputPath): Promise; + /** + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List files in pages with the given `prefix`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateInputPrefix` object. + * @returns A list of objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input?: ListPaginateInput): Promise; + (input?: ListPaginateInputPrefix): Promise; /** - * List all files from S3. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The ListAllInput object. - * @returns A list of keys and metadata for all objects in path - * @throws service: {@link S3Exception} - S3 service errors thrown when checking for existence of bucket - * @throws validation: {@link StorageValidationErrorCode } - thrown when there are issues with credentials + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllInputPrefix` object. + * @returns A list of all objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input?: ListAllInput): Promise; + (input?: ListAllInputPrefix): Promise; } -export const list: ListApi = ( +export const list: ListApi = < + Output extends ListAllOutput | ListPaginateOutput, +>( input?: ListAllInput | ListPaginateInput, -): Promise => { - return listInternal(Amplify, input ?? {}); -}; +): Promise => listInternal(Amplify, input ?? {}) as Promise; diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts index 1c15a98af88..bc10e4cf41b 100644 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ b/packages/storage/src/providers/s3/apis/server/list.ts @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - import { AmplifyServer, getAmplifyServerContext, @@ -8,46 +7,82 @@ import { import { ListAllInput, + ListAllInputPath, + ListAllInputPrefix, ListAllOutput, + ListAllOutputPath, + ListAllOutputPrefix, ListPaginateInput, + ListPaginateInputPath, + ListPaginateInputPrefix, ListPaginateOutput, - S3Exception, + ListPaginateOutputPath, + ListPaginateOutputPrefix, } from '../../types'; import { list as listInternal } from '../internal/list'; -import { StorageValidationErrorCode } from '../../../../errors/types/validation'; interface ListApi { /** - * Lists bucket objects with pagination. - * @param {ListPaginateInput} input The input object - * @return {Promise} - Promise resolves to list of keys and metadata with - * pageSize defaulting to 1000. Additionally the result will include a nextToken if there are more items to retrieve - * @throws service: {@link S3Exception} - S3 service errors thrown when checking for existence of bucket - * @throws validation: {@link StorageValidationErrorCode } - thrown when there are issues with credentials + * List files in pages with the given `path`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateInputPath` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @returns A list of objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: ListPaginateInputPath, + ): Promise; + /** + * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllInputPath` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @returns A list of all objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ + ( + contextSpec: AmplifyServer.ContextSpec, + input: ListAllInputPath, + ): Promise; + /** + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List files in pages with the given `prefix`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateInputPrefix` object. + * @returns A list of objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ ( contextSpec: AmplifyServer.ContextSpec, - input?: ListPaginateInput, - ): Promise; + input?: ListPaginateInputPrefix, + ): Promise; /** - * Lists all bucket objects. - * @param {ListAllInput} input The input object - * @return {Promise} - Promise resolves to list of keys and metadata for all objects in path - * @throws service: {@link S3Exception} - S3 service errors thrown when checking for existence of bucket - * @throws validation: {@link StorageValidationErrorCode } - thrown when there are issues with credentials + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllInputPrefix` object. + * @returns A list of all objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ ( contextSpec: AmplifyServer.ContextSpec, - input?: ListAllInput, - ): Promise; + input?: ListAllInputPrefix, + ): Promise; } -export const list: ListApi = ( +export const list: ListApi = < + Output extends ListAllOutput | ListPaginateOutput, +>( contextSpec: AmplifyServer.ContextSpec, input?: ListAllInput | ListPaginateInput, -): Promise => { - return listInternal( +): Promise => + listInternal( getAmplifyServerContext(contextSpec).amplify, input ?? {}, - ); -}; + ) as Promise; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 8aa5b900587..b3bbb836b65 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -7,8 +7,10 @@ export { UploadDataOptions, GetPropertiesOptionsKey, GetPropertiesOptionsPath, - ListAllOptions, - ListPaginateOptions, + ListAllOptionsPrefix, + ListPaginateOptionsPrefix, + ListAllOptionsPath, + ListPaginateOptionsPath, RemoveOptions, DownloadDataOptionsPath, DownloadDataOptionsKey, @@ -21,9 +23,14 @@ export { DownloadDataOutputPath, GetUrlOutput, UploadDataOutput, - ListOutputItem, + ListOutputItemKey, + ListOutputItemPath, ListAllOutput, ListPaginateOutput, + ListAllOutputPrefix, + ListAllOutputPath, + ListPaginateOutputPath, + ListPaginateOutputPrefix, GetPropertiesOutput, GetPropertiesOutputKey, GetPropertiesOutputPath, @@ -44,8 +51,6 @@ export { GetUrlInput, GetUrlInputKey, GetUrlInputPath, - ListAllInput, - ListPaginateInput, RemoveInputKey, RemoveInputPath, RemoveInput, @@ -53,5 +58,11 @@ export { DownloadDataInputKey, DownloadDataInputPath, UploadDataInput, + ListAllInput, + ListPaginateInput, + ListAllInputPath, + ListPaginateInputPath, + ListAllInputPrefix, + ListPaginateInputPrefix, } from './inputs'; export { S3Exception } from './errors'; diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index bec0b9e17db..4dd9c1522e2 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -12,7 +12,8 @@ import { StorageGetPropertiesInputPath, StorageGetUrlInputKey, StorageGetUrlInputPath, - StorageListInput, + StorageListInputPath, + StorageListInputPrefix, StorageRemoveInputKey, StorageRemoveInputPath, StorageUploadDataInput, @@ -26,8 +27,10 @@ import { GetPropertiesOptionsPath, GetUrlOptionsKey, GetUrlOptionsPath, - ListAllOptions, - ListPaginateOptions, + ListAllOptionsPath, + ListAllOptionsPrefix, + ListPaginateOptionsPath, + ListPaginateOptionsPrefix, RemoveOptions, UploadDataOptions, } from '../types'; @@ -70,12 +73,38 @@ export type GetUrlInputPath = StorageGetUrlInputPath; /** * Input type for S3 list API. Lists all bucket objects. */ -export type ListAllInput = StorageListInput; +export type ListAllInput = StrictUnion; /** * Input type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateInput = StorageListInput; +export type ListPaginateInput = StrictUnion< + ListPaginateInputPath | ListPaginateInputPrefix +>; + +/** + * Input type for S3 list API. Lists all bucket objects. + */ +export type ListAllInputPath = StorageListInputPath; + +/** + * Input type for S3 list API. Lists bucket objects with pagination. + */ +export type ListPaginateInputPath = + StorageListInputPath; + +/** + * @deprecated Use {@link ListAllInputPath} instead. + * Input type for S3 list API. Lists all bucket objects. + */ +export type ListAllInputPrefix = StorageListInputPrefix; + +/** + * @deprecated Use {@link ListPaginateInputPath} instead. + * Input type for S3 list API. Lists bucket objects with pagination. + */ +export type ListPaginateInputPrefix = + StorageListInputPrefix; /** * @deprecated Use {@link RemoveInputPath} instead. @@ -101,7 +130,6 @@ export type RemoveInput = StrictUnion; export type DownloadDataInput = StrictUnion< DownloadDataInputKey | DownloadDataInputPath >; - /** @deprecated Use {@link DownloadDataInputPath} instead. */ export type DownloadDataInputKey = StorageDownloadDataInputKey; diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 92f32ef0187..b9bfe0685f9 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -67,19 +67,36 @@ export type GetPropertiesOptionsPath = CommonOptions; export type RemoveOptions = WriteOptions & CommonOptions; /** - * Input options type for S3 list API. + * @deprecated Use {@link ListAllOptionsPath} instead. + * Input options type with prefix for S3 list all API. */ -export type ListAllOptions = StorageListAllOptions & +export type ListAllOptionsPrefix = StorageListAllOptions & ReadOptions & CommonOptions; /** - * Input options type for S3 list API. + * @deprecated Use {@link ListPaginateOptionsPath} instead. + * Input options type with prefix for S3 list API to paginate items. */ -export type ListPaginateOptions = StorageListPaginateOptions & +export type ListPaginateOptionsPrefix = StorageListPaginateOptions & ReadOptions & CommonOptions; +/** + * Input options type with path for S3 list all API. + */ +export type ListAllOptionsPath = Omit & + CommonOptions; + +/** + * Input options type with path for S3 list API to paginate items. + */ +export type ListPaginateOptionsPath = Omit< + StorageListPaginateOptions, + 'accessLevel' +> & + CommonOptions; + /** * Input options type for S3 getUrl API. */ diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index 7fe3cd5ade6..43bbbc7f3bd 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -27,10 +27,25 @@ export interface ItemBase { contentType?: string; } +/** + * @deprecated Use {@link ListOutputItemPath} instead. + * type for S3 list item with key. + */ +export type ListOutputItemKey = Omit; + +/** + * type for S3 list item with path. + */ +export type ListOutputItemPath = Omit; + /** * @deprecated Use {@link ItemPath} instead. */ export type ItemKey = ItemBase & StorageItemKey; + +/** + * type for S3 list item with path. + */ export type ItemPath = ItemBase & StorageItemPath; /** @@ -75,12 +90,40 @@ export type GetPropertiesOutput = /** * Output type for S3 list API. Lists all bucket objects. */ -export type ListAllOutput = StorageListOutput; +export type ListAllOutput = StrictUnion< + ListAllOutputPath | ListAllOutputPrefix +>; + +/** + * Output type for S3 list API. Lists bucket objects with pagination. + */ +export type ListPaginateOutput = StrictUnion< + ListPaginateOutputPath | ListPaginateOutputPrefix +>; + +/** + * @deprecated Use {@link ListAllOutputPath} instead. + * Output type for S3 list API. Lists all bucket objects. + */ +export type ListAllOutputPrefix = StorageListOutput; + +/** + * Output type for S3 list API. Lists all bucket objects. + */ +export type ListAllOutputPath = StorageListOutput; + +/** + * @deprecated Use {@link ListPaginateOutputPath} instead. + * Output type for S3 list API. Lists bucket objects with pagination. + */ +export type ListPaginateOutputPrefix = StorageListOutput & { + nextToken?: string; +}; /** * Output type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateOutput = StorageListOutput & { +export type ListPaginateOutputPath = StorageListOutput & { nextToken?: string; }; diff --git a/packages/storage/src/providers/s3/utils/client/utils/parsePayload.ts b/packages/storage/src/providers/s3/utils/client/utils/parsePayload.ts index 242fa99aff4..9da44dcbdd0 100644 --- a/packages/storage/src/providers/s3/utils/client/utils/parsePayload.ts +++ b/packages/storage/src/providers/s3/utils/client/utils/parsePayload.ts @@ -39,7 +39,7 @@ export const parseXmlBody = async (response: HttpResponse): Promise => { try { return parser.parse(data); } catch (error) { - throw new Error('Failed to parse XML response.'); + throw new Error(`Failed to parse XML response: ${error}`); } } diff --git a/packages/storage/src/providers/s3/utils/constants.ts b/packages/storage/src/providers/s3/utils/constants.ts index 0aa063cef4d..482343e5494 100644 --- a/packages/storage/src/providers/s3/utils/constants.ts +++ b/packages/storage/src/providers/s3/utils/constants.ts @@ -20,5 +20,6 @@ export const DEFAULT_QUEUE_SIZE = 4; export const UPLOADS_STORAGE_KEY = '__uploadInProgress'; +export const STORAGE_INPUT_PREFIX = 'prefix'; export const STORAGE_INPUT_KEY = 'key'; export const STORAGE_INPUT_PATH = 'path'; diff --git a/packages/storage/src/providers/s3/utils/index.ts b/packages/storage/src/providers/s3/utils/index.ts index 1b7584ad08f..cd6b9753019 100644 --- a/packages/storage/src/providers/s3/utils/index.ts +++ b/packages/storage/src/providers/s3/utils/index.ts @@ -5,4 +5,5 @@ export { calculateContentMd5 } from './md5'; export { resolveS3ConfigAndInput } from './resolveS3ConfigAndInput'; export { createDownloadTask, createUploadTask } from './transferTask'; export { validateStorageOperationInput } from './validateStorageOperationInput'; +export { validateStorageOperationInputWithPrefix } from './validateStorageOperationInputWithPrefix'; export { isInputWithPath } from './isInputWithPath'; diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts new file mode 100644 index 00000000000..04a08c3aa97 --- /dev/null +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + StorageOperationInputPath, + StorageOperationInputWithPrefixPath, +} from '../../../types/inputs'; +import { assertValidationError } from '../../../errors/utils/assertValidationError'; +import { StorageValidationErrorCode } from '../../../errors/types/validation'; + +import { STORAGE_INPUT_PATH, STORAGE_INPUT_PREFIX } from './constants'; + +// Local assertion function with StorageOperationInputWithPrefixPath as Input +const _isInputWithPath = ( + input: StorageOperationInputWithPrefixPath, +): input is StorageOperationInputPath => { + return input.path !== undefined; +}; + +export const validateStorageOperationInputWithPrefix = ( + input: StorageOperationInputWithPrefixPath, + identityId?: string, +) => { + // Validate prefix & path not present at the same time + assertValidationError( + !(input.prefix && input.path), + StorageValidationErrorCode.InvalidStorageOperationPrefixInput, + ); + if (_isInputWithPath(input)) { + const { path } = input; + const objectKey = typeof path === 'string' ? path : path({ identityId }); + + // Assert on no leading slash in the path parameter + assertValidationError( + !objectKey.startsWith('/'), + StorageValidationErrorCode.InvalidStoragePathInput, + ); + + return { + inputType: STORAGE_INPUT_PATH, + objectKey, + }; + } else { + return { inputType: STORAGE_INPUT_PREFIX, objectKey: input.prefix ?? '' }; + } +}; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index daa3eb466d9..77d3082b2e0 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -9,9 +9,10 @@ export { } from './common'; export { StorageOperationInput, - StorageListInput, StorageGetPropertiesInputKey, StorageGetPropertiesInputPath, + StorageListInputPrefix, + StorageListInputPath, StorageRemoveInputPath, StorageRemoveInputKey, StorageDownloadDataInputKey, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 6139f95877d..1877958c905 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -14,7 +14,9 @@ import { export type StorageOperationInputType = StrictUnion< StorageOperationInputKey | StorageOperationInputPath >; - +export type StorageOperationInputWithPrefixPath = StrictUnion< + StorageOperationInputPath | StorageOperationInputPrefix +>; /** @deprecated Use {@link StorageOperationInputPath} instead. */ export interface StorageOperationInputKey { /** @deprecated Use `path` instead. */ @@ -23,6 +25,11 @@ export interface StorageOperationInputKey { export interface StorageOperationInputPath { path: string | (({ identityId }: { identityId?: string }) => string); } +/** @deprecated Use {@link StorageOperationInputPath} instead. */ +export interface StorageOperationInputPrefix { + /** @deprecated Use `path` instead. */ + prefix?: string; +} export interface StorageOperationOptionsInput { options?: Options; } @@ -53,12 +60,14 @@ export type StorageRemoveInputKey = StorageOperationInputKey & export type StorageRemoveInputPath = StorageOperationInputPath & StorageOperationOptionsInput; -export interface StorageListInput< +/** @deprecated Use {@link StorageListInputPath} instead. */ +export type StorageListInputPrefix< Options extends StorageListAllOptions | StorageListPaginateOptions, -> { - prefix?: string; - options?: Options; -} +> = StorageOperationInputPrefix & StorageOperationOptionsInput; + +export type StorageListInputPath< + Options extends StorageListAllOptions | StorageListPaginateOptions, +> = StorageOperationInputPath & StorageOperationOptionsInput; /** @deprecated Use {@link StorageGetUrlInputPath} instead. */ export type StorageGetUrlInputKey = diff --git a/packages/storage/src/types/outputs.ts b/packages/storage/src/types/outputs.ts index 0d4307fd73b..2e83f60a20d 100644 --- a/packages/storage/src/types/outputs.ts +++ b/packages/storage/src/types/outputs.ts @@ -65,5 +65,8 @@ export interface StorageGetUrlOutput { export type StorageUploadOutput = Item; export interface StorageListOutput { + /** + * List of items returned by the list API. + */ items: Item[]; } From 68a88bb4be020e60e08bf7325edd8a55a03cd177 Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Wed, 27 Mar 2024 14:01:11 -0500 Subject: [PATCH 15/33] feat(storage): uploadData path parameter support (#13099) --- packages/aws-amplify/package.json | 2 +- .../s3/apis/uploadData/index.test.ts | 119 +++- .../apis/uploadData/multipartHandlers.test.ts | 509 +++++++++++++++++- .../s3/apis/uploadData/putObjectJob.test.ts | 96 +++- .../src/providers/s3/apis/uploadData/index.ts | 177 ++++-- .../uploadData/multipart/initialUpload.ts | 7 +- .../apis/uploadData/multipart/uploadCache.ts | 13 +- .../uploadData/multipart/uploadHandlers.ts | 63 ++- .../s3/apis/uploadData/putObjectJob.ts | 30 +- .../storage/src/providers/s3/types/index.ts | 7 +- .../storage/src/providers/s3/types/inputs.ts | 16 +- .../storage/src/providers/s3/types/options.ts | 7 +- .../storage/src/providers/s3/types/outputs.ts | 6 +- packages/storage/src/types/index.ts | 3 +- packages/storage/src/types/inputs.ts | 19 +- 15 files changed, 970 insertions(+), 104 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index b52f306a08f..d022535b24d 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -497,7 +497,7 @@ "name": "[Storage] uploadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ uploadData }", - "limit": "18.52 kB" + "limit": "18.79 kB" } ] } diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts index 22569bf6671..df818c254c8 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts @@ -15,6 +15,7 @@ jest.mock('../../../../../src/providers/s3/utils/'); jest.mock('../../../../../src/providers/s3/apis/uploadData/putObjectJob'); jest.mock('../../../../../src/providers/s3/apis/uploadData/multipart'); +const testPath = 'testPath/object'; const mockCreateUploadTask = createUploadTask as jest.Mock; const mockPutObjectJob = putObjectJob as jest.Mock; const mockGetMultipartUploadHandlers = ( @@ -26,7 +27,8 @@ const mockGetMultipartUploadHandlers = ( onCancel: jest.fn(), }); -describe('uploadData', () => { +/* TODO Remove suite when `key` parameter is removed */ +describe('uploadData with key', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -54,7 +56,7 @@ describe('uploadData', () => { }); }); - describe('use putObject', () => { + describe('use putObject for small uploads', () => { const smallData = { size: 5 * 1024 * 1024 } as any; it('should use putObject if data size is <= 5MB', async () => { uploadData({ @@ -83,7 +85,7 @@ describe('uploadData', () => { }); }); - describe('use multipartUpload', () => { + describe('use multipartUpload for large uploads', () => { const biggerData = { size: 5 * 1024 * 1024 + 1 } as any; it('should use multipartUpload if data size is > 5MB', async () => { uploadData({ @@ -121,3 +123,114 @@ describe('uploadData', () => { }); }); }); + +describe('uploadData with path', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('validation', () => { + it('should throw if data size is too big', async () => { + expect(() => + uploadData({ + path: testPath, + data: { size: MAX_OBJECT_SIZE + 1 } as any, + }), + ).toThrow( + expect.objectContaining( + validationErrorMap[StorageValidationErrorCode.ObjectIsTooLarge], + ), + ); + }); + + it('should NOT throw if data size is unknown', async () => { + uploadData({ + path: testPath, + data: {} as any, + }); + expect(mockCreateUploadTask).toHaveBeenCalled(); + }); + }); + + describe('use putObject for small uploads', () => { + const smallData = { size: 5 * 1024 * 1024 } as any; + + test.each([ + { + path: testPath, + }, + { + path: () => testPath, + }, + ])( + 'should use putObject if data size is <= 5MB when path is $path', + async ({ path }) => { + const testInput = { + path, + data: smallData, + } + + uploadData(testInput); + + expect(mockPutObjectJob).toHaveBeenCalledWith( + testInput, + expect.any(AbortSignal), + expect.any(Number) + ); + expect(mockGetMultipartUploadHandlers).not.toHaveBeenCalled(); + } + ); + + it('should use uploadTask', async () => { + mockPutObjectJob.mockReturnValueOnce('putObjectJob'); + mockCreateUploadTask.mockReturnValueOnce('uploadTask'); + + const task = uploadData({ + path: testPath, + data: smallData, + }); + + expect(task).toBe('uploadTask'); + expect(mockCreateUploadTask).toHaveBeenCalledWith( + expect.objectContaining({ + job: 'putObjectJob', + onCancel: expect.any(Function), + isMultipartUpload: false, + }), + ); + }); + }); + + describe('use multipartUpload for large uploads', () => { + const biggerData = { size: 5 * 1024 * 1024 + 1 } as any; + it('should use multipartUpload if data size is > 5MB', async () => { + const testInput = { + path: testPath, + data: biggerData, + } + + uploadData(testInput); + + expect(mockPutObjectJob).not.toHaveBeenCalled(); + expect(mockGetMultipartUploadHandlers).toHaveBeenCalledWith(testInput, expect.any(Number)); + }); + + it('should use uploadTask', async () => { + mockCreateUploadTask.mockReturnValueOnce('uploadTask'); + const task = uploadData({ + path: testPath, + data: biggerData, + }); + expect(task).toBe('uploadTask'); + expect(mockCreateUploadTask).toHaveBeenCalledWith( + expect.objectContaining({ + job: expect.any(Function), + onCancel: expect.any(Function), + onResume: expect.any(Function), + onPause: expect.any(Function), + isMultipartUpload: true, + }), + ); + }); + }); +}); 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 65b4dd473a6..96e680088bb 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts @@ -36,6 +36,8 @@ const region = 'region'; const defaultKey = 'key'; const defaultContentType = 'application/octet-stream'; const defaultCacheKey = '8388608_application/octet-stream_bucket_public_key'; +const testPath = 'testPath/object'; +const testPathCacheKey = `8388608_${defaultContentType}_${bucket}_custom_${testPath}`; const mockCreateMultipartUpload = createMultipartUpload as jest.Mock; const mockUploadPart = uploadPart as jest.Mock; @@ -116,7 +118,8 @@ const resetS3Mocks = () => { mockListParts.mockReset(); }; -describe('getMultipartUploadHandlers', () => { +/* TODO Remove suite when `key` parameter is removed */ +describe('getMultipartUploadHandlers with key', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -629,3 +632,507 @@ describe('getMultipartUploadHandlers', () => { }); }); }); + +describe('getMultipartUploadHandlers with path', () => { + beforeAll(() => { + mockFetchAuthSession.mockResolvedValue({ + credentials, + identityId: defaultIdentityId, + }); + (Amplify.getConfig as jest.Mock).mockReturnValue({ + Storage: { + S3: { + bucket, + region, + }, + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + resetS3Mocks(); + }); + + it('should return multipart upload handlers', async () => { + const multipartUploadHandlers = getMultipartUploadHandlers( + { + path: testPath, + data: { size: 5 * 1024 * 1024 } as any, + }, + 5 * 1024 * 1024, + ); + expect(multipartUploadHandlers).toEqual({ + multipartUploadJob: expect.any(Function), + onPause: expect.any(Function), + onResume: expect.any(Function), + onCancel: expect.any(Function), + }); + }); + + describe('upload', () => { + const getBlob = (size: number) => new Blob(['1'.repeat(size)]); + [ + { + path: testPath, + expectedKey: testPath, + }, + { + path: ({identityId}: any) => `testPath/${identityId}/object`, + expectedKey: `testPath/${defaultIdentityId}/object`, + }, + ].forEach(({ path, expectedKey }) => { + it.each([ + ['file', new File([getBlob(8 * MB)], 'someName')], + ['blob', getBlob(8 * MB)], + ['string', 'Ü'.repeat(4 * MB)], + ['arrayBuffer', new ArrayBuffer(8 * MB)], + ['arrayBufferView', new Uint8Array(8 * MB)], + ])( + `should upload a %s type body that splits into 2 parts to path ${expectedKey}`, + async (_, twoPartsPayload) => { + mockMultipartUploadSuccess(); + const { multipartUploadJob } = getMultipartUploadHandlers({ + path: path, + data: twoPartsPayload, + }); + const result = await multipartUploadJob(); + expect(mockCreateMultipartUpload).toHaveBeenCalledWith( + expect.objectContaining({ + credentials, + region, + abortSignal: expect.any(AbortSignal), + }), + expect.objectContaining({ + Bucket: bucket, + Key: expectedKey, + ContentType: defaultContentType, + }), + ); + expect(result).toEqual( + expect.objectContaining({ path: expectedKey, eTag: 'etag' }), + ); + expect(mockCreateMultipartUpload).toHaveBeenCalledTimes(1); + expect(mockUploadPart).toHaveBeenCalledTimes(2); + expect(mockCompleteMultipartUpload).toHaveBeenCalledTimes(1); + }, + ); + }); + + it('should throw if unsupported payload type is provided', async () => { + mockMultipartUploadSuccess(); + const { multipartUploadJob } = getMultipartUploadHandlers({ + path: testPath, + data: 1 as any, + }); + await expect(multipartUploadJob()).rejects.toThrow( + expect.objectContaining( + validationErrorMap[StorageValidationErrorCode.InvalidUploadSource], + ), + ); + }); + + it('should upload a body that exceeds the size of default part size and parts count', async () => { + let buffer: ArrayBuffer; + const file = { + __proto__: File.prototype, + name: 'some file', + lastModified: 0, + size: 100_000 * MB, + type: 'text/plain', + slice: jest.fn().mockImplementation((start, end) => { + if (end - start !== buffer?.byteLength) { + buffer = new ArrayBuffer(end - start); + } + return buffer; + }), + } as any as File; + mockMultipartUploadSuccess(); + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: file, + }, + file.size, + ); + await multipartUploadJob(); + expect(file.slice).toHaveBeenCalledTimes(10_000); // S3 limit of parts count + expect(mockCreateMultipartUpload).toHaveBeenCalledTimes(1); + expect(mockUploadPart).toHaveBeenCalledTimes(10_000); + expect(mockCompleteMultipartUpload).toHaveBeenCalledTimes(1); + expect(mockUploadPart.mock.calls[0][1].Body.byteLength).toEqual(10 * MB); // The part size should be adjusted from default 5MB to 10MB. + }); + + it('should throw error when remote and local file sizes do not match upon completed upload', async () => { + expect.assertions(1); + mockMultipartUploadSuccess(disableAssertion); + mockHeadObject.mockReset(); + mockHeadObject.mockResolvedValue({ + ContentLength: 1, + }); + + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(8 * MB), + }, + 8 * MB, + ); + try { + await multipartUploadJob(); + fail('should throw error'); + } catch (e: any) { + expect(e.message).toEqual( + `Upload failed. Expected object size ${8 * MB}, but got 1.`, + ); + } + }); + + it('should handle error case: create multipart upload request failed', async () => { + expect.assertions(1); + mockMultipartUploadSuccess(); + mockCreateMultipartUpload.mockReset(); + mockCreateMultipartUpload.mockRejectedValueOnce(new Error('error')); + + const { multipartUploadJob } = getMultipartUploadHandlers({ + path: testPath, + data: new ArrayBuffer(8 * MB), + }); + await expect(multipartUploadJob()).rejects.toThrow('error'); + }); + + it('should handle error case: finish multipart upload failed', async () => { + expect.assertions(1); + mockMultipartUploadSuccess(disableAssertion); + mockCompleteMultipartUpload.mockReset(); + mockCompleteMultipartUpload.mockRejectedValueOnce(new Error('error')); + + const { multipartUploadJob } = getMultipartUploadHandlers({ + path: testPath, + data: new ArrayBuffer(8 * MB), + }); + await expect(multipartUploadJob()).rejects.toThrow('error'); + }); + + it('should handle error case: upload a body that splits in two parts but second part fails', async () => { + expect.assertions(3); + mockMultipartUploadSuccess(disableAssertion); + mockUploadPart.mockReset(); + mockUploadPart.mockResolvedValueOnce({ + ETag: `etag-1`, + PartNumber: 1, + }); + mockUploadPart.mockRejectedValueOnce(new Error('error')); + + const { multipartUploadJob } = getMultipartUploadHandlers({ + path: testPath, + data: new ArrayBuffer(8 * MB), + }); + await expect(multipartUploadJob()).rejects.toThrow('error'); + expect(mockUploadPart).toHaveBeenCalledTimes(2); + expect(mockCompleteMultipartUpload).not.toHaveBeenCalled(); + }); + }); + + describe('upload caching', () => { + const mockDefaultStorage = defaultStorage as jest.Mocked< + typeof defaultStorage + >; + beforeEach(() => { + mockDefaultStorage.getItem.mockReset(); + mockDefaultStorage.setItem.mockReset(); + }); + + it('should send createMultipartUpload request if the upload task is not cached', async () => { + mockMultipartUploadSuccess(); + const size = 8 * MB; + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(size), + }, + size, + ); + await multipartUploadJob(); + // 1 for caching upload task; 1 for remove cache after upload is completed + expect(mockDefaultStorage.setItem).toHaveBeenCalledTimes(2); + expect(mockCreateMultipartUpload).toHaveBeenCalledTimes(1); + expect(mockListParts).not.toHaveBeenCalled(); + }); + + it('should send createMultipartUpload request if the upload task is cached but outdated', async () => { + mockDefaultStorage.getItem.mockResolvedValue( + JSON.stringify({ + [testPathCacheKey]: { + uploadId: 'uploadId', + bucket, + key: testPath, + lastTouched: Date.now() - 2 * 60 * 60 * 1000, // 2 hours ago + }, + }), + ); + mockMultipartUploadSuccess(); + mockListParts.mockResolvedValueOnce({ Parts: [] }); + const size = 8 * MB; + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(size), + }, + size, + ); + await multipartUploadJob(); + expect(mockCreateMultipartUpload).toHaveBeenCalledTimes(1); + expect(mockListParts).not.toHaveBeenCalled(); + expect(mockUploadPart).toHaveBeenCalledTimes(2); + expect(mockCompleteMultipartUpload).toHaveBeenCalledTimes(1); + }); + + it('should cache the upload with file including file lastModified property', async () => { + mockMultipartUploadSuccess(); + mockListParts.mockResolvedValueOnce({ Parts: [] }); + const size = 8 * MB; + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new File([new ArrayBuffer(size)], 'someName'), + }, + size, + ); + await multipartUploadJob(); + // 1 for caching upload task; 1 for remove cache after upload is completed + expect(mockDefaultStorage.setItem).toHaveBeenCalledTimes(2); + const cacheValue = JSON.parse( + mockDefaultStorage.setItem.mock.calls[0][1], + ); + + // \d{13} is the file lastModified property of a file + const lastModifiedRegex = /someName_\d{13}_/; + + expect(Object.keys(cacheValue)).toEqual([ + expect.stringMatching(new RegExp(lastModifiedRegex.source + testPathCacheKey)), + ]); + }); + + it('should send listParts request if the upload task is cached', async () => { + mockDefaultStorage.getItem.mockResolvedValue( + JSON.stringify({ + [testPathCacheKey]: { + uploadId: 'uploadId', + bucket, + key: testPath, + lastModified: Date.now(), + }, + }), + ); + mockMultipartUploadSuccess(); + mockListParts.mockResolvedValueOnce({ Parts: [] }); + const size = 8 * MB; + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(size), + }, + size, + ); + await multipartUploadJob(); + expect(mockCreateMultipartUpload).not.toHaveBeenCalled(); + expect(mockListParts).toHaveBeenCalledTimes(1); + expect(mockUploadPart).toHaveBeenCalledTimes(2); + expect(mockCompleteMultipartUpload).toHaveBeenCalledTimes(1); + }); + + it('should cache upload task if new upload task is created', async () => { + mockMultipartUploadSuccess(); + mockListParts.mockResolvedValueOnce({ Parts: [] }); + const size = 8 * MB; + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(size), + }, + size, + ); + await multipartUploadJob(); + // 1 for caching upload task; 1 for remove cache after upload is completed + expect(mockDefaultStorage.setItem).toHaveBeenCalledTimes(2); + expect(mockDefaultStorage.setItem.mock.calls[0][0]).toEqual( + UPLOADS_STORAGE_KEY, + ); + const cacheValue = JSON.parse( + mockDefaultStorage.setItem.mock.calls[0][1], + ); + expect(Object.keys(cacheValue)).toEqual([ + expect.stringMatching(new RegExp(testPathCacheKey)), + ]); + }); + + it('should remove from cache if upload task is completed', async () => { + mockMultipartUploadSuccess(); + mockListParts.mockResolvedValueOnce({ Parts: [] }); + const size = 8 * MB; + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(size), + }, + size, + ); + await multipartUploadJob(); + // 1 for caching upload task; 1 for remove cache after upload is completed + expect(mockDefaultStorage.setItem).toHaveBeenCalledTimes(2); + expect(mockDefaultStorage.setItem).toHaveBeenNthCalledWith( + 2, + UPLOADS_STORAGE_KEY, + JSON.stringify({}), + ); + }); + + it('should remove from cache if upload task is canceled', async () => { + expect.assertions(2); + mockMultipartUploadSuccess(disableAssertion); + mockListParts.mockResolvedValueOnce({ Parts: [] }); + const size = 8 * MB; + const { multipartUploadJob, onCancel } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(size), + }, + size, + ); + const uploadJobPromise = multipartUploadJob(); + await uploadJobPromise; + // 1 for caching upload task; 1 for remove cache after upload is completed + expect(mockDefaultStorage.setItem).toHaveBeenCalledTimes(2); + expect(mockDefaultStorage.setItem).toHaveBeenNthCalledWith( + 2, + UPLOADS_STORAGE_KEY, + JSON.stringify({}), + ); + }); + }); + + describe('cancel()', () => { + it('should abort in-flight uploadPart requests and throw if upload is canceled', async () => { + const { multipartUploadJob, onCancel } = getMultipartUploadHandlers({ + path: testPath, + data: new ArrayBuffer(8 * MB), + }); + let partCount = 0; + mockMultipartUploadCancellation(() => { + partCount++; + if (partCount === 2) { + onCancel(); // Cancel upload at the the last uploadPart call + } + }); + try { + await multipartUploadJob(); + fail('should throw error'); + } catch (error: any) { + expect(error).toBeInstanceOf(CanceledError); + expect(error.message).toBe('Upload is canceled by user'); + } + expect(mockAbortMultipartUpload).toHaveBeenCalledTimes(1); + expect(mockUploadPart).toHaveBeenCalledTimes(2); + expect(mockUploadPart.mock.calls[0][0].abortSignal?.aborted).toBe(true); + expect(mockUploadPart.mock.calls[1][0].abortSignal?.aborted).toBe(true); + }); + }); + + describe('pause() & resume()', () => { + it('should abort in-flight uploadPart requests if upload is paused', async () => { + const { multipartUploadJob, onPause, onResume } = + getMultipartUploadHandlers({ + path: testPath, + data: new ArrayBuffer(8 * MB), + }); + let partCount = 0; + mockMultipartUploadCancellation(() => { + partCount++; + if (partCount === 2) { + onPause(); // Pause upload at the the last uploadPart call + } + }); + const uploadPromise = multipartUploadJob(); + onResume(); + await uploadPromise; + expect(mockUploadPart).toHaveBeenCalledTimes(2); + expect(mockUploadPart.mock.calls[0][0].abortSignal?.aborted).toBe(true); + expect(mockUploadPart.mock.calls[1][0].abortSignal?.aborted).toBe(true); + }); + }); + + describe('upload progress', () => { + it('should send progress for in-flight upload parts', async () => { + const onProgress = jest.fn(); + mockMultipartUploadSuccess(); + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(8 * MB), + options: { + onProgress, + }, + }, + 8 * MB, + ); + await multipartUploadJob(); + expect(onProgress).toHaveBeenCalledTimes(4); // 2 simulated onProgress events per uploadPart call are all tracked + expect(onProgress).toHaveBeenNthCalledWith(1, { + totalBytes: 8388608, + transferredBytes: 2621440, + }); + expect(onProgress).toHaveBeenNthCalledWith(2, { + totalBytes: 8388608, + transferredBytes: 5242880, + }); + expect(onProgress).toHaveBeenNthCalledWith(3, { + totalBytes: 8388608, + transferredBytes: 6815744, + }); + expect(onProgress).toHaveBeenNthCalledWith(4, { + totalBytes: 8388608, + transferredBytes: 8388608, + }); + }); + + it('should send progress for cached upload parts', async () => { + mockMultipartUploadSuccess(); + + const mockDefaultStorage = defaultStorage as jest.Mocked< + typeof defaultStorage + >; + mockDefaultStorage.getItem.mockResolvedValue( + JSON.stringify({ + [testPathCacheKey]: { + uploadId: 'uploadId', + bucket, + key: testPath, + }, + }), + ); + mockListParts.mockResolvedValue({ + Parts: [{ PartNumber: 1 }], + }); + + const onProgress = jest.fn(); + const { multipartUploadJob } = getMultipartUploadHandlers( + { + path: testPath, + data: new ArrayBuffer(8 * MB), + options: { + onProgress, + }, + }, + 8 * MB, + ); + await multipartUploadJob(); + expect(onProgress).toHaveBeenCalledTimes(3); + // The first part's 5 MB progress is reported even though no uploadPart call is made. + expect(onProgress).toHaveBeenNthCalledWith(1, { + totalBytes: 8388608, + transferredBytes: 5242880, + }); + }); + }); +}); 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 cc61a4e2bd6..e429b4a9fb3 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts @@ -25,6 +25,8 @@ jest.mock('@aws-amplify/core', () => ({ }, }, })); + +const testPath = 'testPath/object'; const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -51,9 +53,8 @@ mockPutObject.mockResolvedValue({ VersionId: 'versionId', }); -// TODO[AllanZhengYP]: add more unit tests to cover different access level combination. -// TODO[AllanZhengYP]: add more unit tests to cover validations errors and service errors. -describe('putObjectJob', () => { +/* TODO Remove suite when `key` parameter is removed */ +describe('putObjectJob with key', () => { it('should supply the correct parameters to putObject API handler', async () => { const abortController = new AbortController(); const key = 'key'; @@ -130,3 +131,92 @@ describe('putObjectJob', () => { expect(calculateContentMd5).toHaveBeenCalledWith('data'); }); }); + +describe('putObjectJob with path', () => { + test.each([ + { + path: testPath, + expectedKey: testPath, + }, + { + path: () => testPath, + expectedKey: testPath, + }, + ])( + 'should supply the correct parameters to putObject API handler when path is $path', + async ({ path, expectedKey }) => { + const abortController = new AbortController(); + const data = 'data'; + const contentType = 'contentType'; + const contentDisposition = 'contentDisposition'; + const contentEncoding = 'contentEncoding'; + const metadata = { key: 'value' }; + const onProgress = jest.fn(); + const useAccelerateEndpoint = true; + + const job = putObjectJob( + { + path, + data, + options: { + contentDisposition, + contentEncoding, + contentType, + metadata, + onProgress, + useAccelerateEndpoint, + }, + }, + abortController.signal, + ); + const result = await job(); + expect(result).toEqual({ + path: expectedKey, + eTag: 'eTag', + versionId: 'versionId', + contentType: 'contentType', + metadata: { key: 'value' }, + size: undefined, + }); + expect(mockPutObject).toHaveBeenCalledWith( + { + credentials, + region: 'region', + abortSignal: abortController.signal, + onUploadProgress: expect.any(Function), + useAccelerateEndpoint: true, + userAgentValue: expect.any(String), + }, + { + Bucket: 'bucket', + Key: expectedKey, + Body: data, + ContentType: contentType, + ContentDisposition: contentDisposition, + ContentEncoding: contentEncoding, + Metadata: metadata, + ContentMD5: undefined, + }, + ); + } + ); + + it('should set ContentMD5 if object lock is enabled', async () => { + Amplify.libraryOptions = { + Storage: { + S3: { + isObjectLockEnabled: true, + }, + }, + }; + const job = putObjectJob( + { + path: testPath, + data: 'data', + }, + new AbortController().signal, + ); + await job(); + expect(calculateContentMd5).toHaveBeenCalledWith('data'); + }); +}); diff --git a/packages/storage/src/providers/s3/apis/uploadData/index.ts b/packages/storage/src/providers/s3/apis/uploadData/index.ts index 2c1926cb1e0..a1de4b1f40f 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/index.ts @@ -1,7 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { S3Exception, UploadDataInput, UploadDataOutput } from '../../types'; +import { + UploadDataInput, + UploadDataInputKey, + UploadDataInputPath, + UploadDataOutput, + UploadDataOutputKey, + UploadDataOutputPath, +} from '../../types'; import { createUploadTask } from '../../utils'; import { assertValidationError } from '../../../../errors/utils/assertValidationError'; import { StorageValidationErrorCode } from '../../../../errors/types/validation'; @@ -11,55 +18,119 @@ import { byteLength } from './byteLength'; import { putObjectJob } from './putObjectJob'; import { getMultipartUploadHandlers } from './multipart'; -/** - * Upload data to specified S3 object. By default, it uses single PUT operation to upload if the data is less than 5MB. - * Otherwise, it uses multipart upload to upload the data. If the data length is unknown, it uses multipart upload. - * - * Limitations: - * * Maximum object size is 5TB. - * * Maximum object size if the size cannot be determined before upload is 50GB. - * - * @param input - The UploadDataInput object. - * @returns A cancelable and resumable task exposing result promise from `result` - * property. - * @throws service: {@link S3Exception} - thrown when checking for existence of the object - * @throws validation: {@link StorageValidationErrorCode } - Validation errors. - * - * @example - * ```ts - * // Upload a file to s3 bucket - * await uploadData({ key, data: file, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * @example - * ```ts - * // Cancel a task - * const uploadTask = uploadData({ key, data: file }); - * //... - * uploadTask.cancel(); - * try { - * await uploadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - * - * @example - * ```ts - * // Pause and resume a task - * const uploadTask = uploadData({ key, data: file }); - * //... - * uploadTask.pause(); - * //... - * uploadTask.resume(); - * //... - * await uploadTask.result; - * ``` - */ -export const uploadData = (input: UploadDataInput): UploadDataOutput => { +interface UploadData { + /** + * Upload data to the specified S3 object path. By default uses single PUT operation to upload if the payload is less than 5MB. + * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. + * + * Limitations: + * * Maximum object size is 5TB. + * * Maximum object size if the size cannot be determined before upload is 50GB. + * + * @throws Service: `S3Exception` thrown when checking for existence of the object. + * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. + * + * @param input - A `UploadDataInputPath` object. + * + * @returns A cancelable and resumable task exposing result promise from `result` + * property. + * + * @example + * ```ts + * // Upload a file to s3 bucket + * await uploadData({ path, data: file, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * + * @example + * ```ts + * // Cancel a task + * const uploadTask = uploadData({ path, data: file }); + * //... + * uploadTask.cancel(); + * try { + * await uploadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + * + * @example + * ```ts + * // Pause and resume a task + * const uploadTask = uploadData({ path, data: file }); + * //... + * uploadTask.pause(); + * //... + * uploadTask.resume(); + * //... + * await uploadTask.result; + * ``` + */ + (input: UploadDataInputPath): UploadDataOutputPath; + + /** + * Upload data to the specified S3 object key. By default uses single PUT operation to upload if the payload is less than 5MB. + * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. + * + * Limitations: + * * Maximum object size is 5TB. + * * Maximum object size if the size cannot be determined before upload is 50GB. + * + * @deprecated The `key` and `accessLevel` parameters are deprecated and will be removed in next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/upload/#uploaddata | path} instead. + * + * @throws Service: `S3Exception` thrown when checking for existence of the object. + * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. + * + * @param input - A UploadDataInputKey object. + * + * @returns A cancelable and resumable task exposing result promise from the `result` property. + * + * @example + * ```ts + * // Upload a file to s3 bucket + * await uploadData({ key, data: file, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * + * @example + * ```ts + * // Cancel a task + * const uploadTask = uploadData({ key, data: file }); + * //... + * uploadTask.cancel(); + * try { + * await uploadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + * + * @example + * ```ts + * // Pause and resume a task + * const uploadTask = uploadData({ key, data: file }); + * //... + * uploadTask.pause(); + * //... + * uploadTask.resume(); + * //... + * await uploadTask.result; + * ``` + */ + (input: UploadDataInputKey): UploadDataOutputKey; +} + +export const uploadData: UploadData = ( + input: UploadDataInput, +): Output => { const { data } = input; const dataByteLength = byteLength(data); @@ -69,6 +140,7 @@ export const uploadData = (input: UploadDataInput): UploadDataOutput => { ); if (dataByteLength && dataByteLength <= DEFAULT_PART_SIZE) { + // Single part upload const abortController = new AbortController(); return createUploadTask({ @@ -77,8 +149,9 @@ export const uploadData = (input: UploadDataInput): UploadDataOutput => { onCancel: (message?: string) => { abortController.abort(message); }, - }); + }) as Output; } else { + // Multipart upload const { multipartUploadJob, onPause, onResume, onCancel } = getMultipartUploadHandlers(input, dataByteLength); @@ -90,6 +163,6 @@ export const uploadData = (input: UploadDataInput): UploadDataOutput => { }, onPause, onResume, - }); + }) as Output; } }; diff --git a/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts b/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts index f9b402c2e86..1179b89c08b 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/initialUpload.ts @@ -18,8 +18,8 @@ interface LoadOrCreateMultipartUploadOptions { s3Config: ResolvedS3Config; data: StorageUploadDataPayload; bucket: string; - accessLevel: StorageAccessLevel; - keyPrefix: string; + accessLevel?: StorageAccessLevel; + keyPrefix?: string; key: string; contentType?: string; contentDisposition?: string; @@ -54,7 +54,7 @@ export const loadOrCreateMultipartUpload = async ({ metadata, abortSignal, }: LoadOrCreateMultipartUploadOptions): Promise => { - const finalKey = keyPrefix + key; + const finalKey = keyPrefix !== undefined ? keyPrefix + key : key; let cachedUpload: | { @@ -75,6 +75,7 @@ export const loadOrCreateMultipartUpload = async ({ accessLevel, key, }); + const cachedUploadParts = await findCachedUploadParts({ s3Config, cacheKey: uploadCacheKey, diff --git a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadCache.ts b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadCache.ts index 1d8148223b1..e5619655f3b 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadCache.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadCache.ts @@ -94,7 +94,7 @@ interface UploadsCacheKeyOptions { size: number; contentType?: string; bucket: string; - accessLevel: StorageAccessLevel; + accessLevel?: StorageAccessLevel; key: string; file?: File; } @@ -112,10 +112,19 @@ export const getUploadsCacheKey = ({ accessLevel, key, }: UploadsCacheKeyOptions) => { + let levelStr; const resolvedContentType = contentType ?? file?.type ?? 'application/octet-stream'; - const levelStr = accessLevel === 'guest' ? 'public' : accessLevel; + + // If no access level is defined, we're using custom gen2 access rules + if (accessLevel === undefined) { + levelStr = 'custom'; + } else { + levelStr = accessLevel === 'guest' ? 'public' : accessLevel; + } + const baseId = `${size}_${resolvedContentType}_${bucket}_${levelStr}_${key}`; + if (file) { return `${file.name}_${file.lastModified}_${baseId}`; } else { 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 7aa62f08390..3ccfef1b9dd 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts @@ -5,13 +5,17 @@ import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { UploadDataInput } from '../../../types'; -import { resolveS3ConfigAndInput } from '../../../utils'; -import { ItemKey as S3Item } from '../../../types/outputs'; +import { + resolveS3ConfigAndInput, + validateStorageOperationInput, +} from '../../../utils'; +import { ItemKey, ItemPath } from '../../../types/outputs'; import { DEFAULT_ACCESS_LEVEL, DEFAULT_QUEUE_SIZE, + STORAGE_INPUT_KEY, } from '../../../utils/constants'; -import { ResolvedS3Config } from '../../../types/options'; +import { ResolvedS3Config, UploadDataOptionsKey } from '../../../types/options'; import { StorageError } from '../../../../../errors/StorageError'; import { CanceledError } from '../../../../../errors/CanceledError'; import { @@ -36,10 +40,10 @@ import { getDataChunker } from './getDataChunker'; * @internal */ export const getMultipartUploadHandlers = ( - { options: uploadDataOptions, key, data }: UploadDataInput, + uploadDataInput: UploadDataInput, size?: number, ) => { - let resolveCallback: ((value: S3Item) => void) | undefined; + let resolveCallback: ((value: ItemKey | ItemPath) => void) | undefined; let rejectCallback: ((reason?: any) => void) | undefined; let inProgressUpload: | { @@ -49,43 +53,62 @@ export const getMultipartUploadHandlers = ( | undefined; let resolvedS3Config: ResolvedS3Config | undefined; let abortController: AbortController | undefined; + let resolvedAccessLevel: StorageAccessLevel | undefined; let resolvedBucket: string | undefined; let resolvedKeyPrefix: string | undefined; + let resolvedIdentityId: string | undefined; let uploadCacheKey: string | undefined; + let finalKey: string; // Special flag that differentiates HTTP requests abort error caused by pause() from ones caused by cancel(). // The former one should NOT cause the upload job to throw, but cancels any pending HTTP requests. // This should be replaced by a special abort reason. However,the support of this API is lagged behind. let isAbortSignalFromPause = false; - const startUpload = async (): Promise => { + const startUpload = async (): Promise => { + const { options: uploadDataOptions, data } = uploadDataInput; const resolvedS3Options = await resolveS3ConfigAndInput( Amplify, uploadDataOptions, ); + abortController = new AbortController(); + isAbortSignalFromPause = false; resolvedS3Config = resolvedS3Options.s3Config; resolvedBucket = resolvedS3Options.bucket; - resolvedKeyPrefix = resolvedS3Options.keyPrefix; + resolvedIdentityId = resolvedS3Options.identityId; - abortController = new AbortController(); - isAbortSignalFromPause = false; + const { inputType, objectKey } = validateStorageOperationInput( + uploadDataInput, + resolvedIdentityId, + ); const { contentDisposition, contentEncoding, contentType = 'application/octet-stream', metadata, - accessLevel, onProgress, } = uploadDataOptions ?? {}; + finalKey = objectKey; + + // Resolve "key" specific options + if (inputType === STORAGE_INPUT_KEY) { + const accessLevel = (uploadDataOptions as UploadDataOptionsKey) + ?.accessLevel; + + resolvedKeyPrefix = resolvedS3Options.keyPrefix; + finalKey = resolvedKeyPrefix + objectKey; + resolvedAccessLevel = resolveAccessLevel(accessLevel); + } + if (!inProgressUpload) { const { uploadId, cachedParts } = await loadOrCreateMultipartUpload({ s3Config: resolvedS3Config, - accessLevel: resolveAccessLevel(accessLevel), + accessLevel: resolvedAccessLevel, bucket: resolvedBucket, keyPrefix: resolvedKeyPrefix, - key, + key: objectKey, contentType, contentDisposition, contentEncoding, @@ -100,15 +123,14 @@ export const getMultipartUploadHandlers = ( }; } - const finalKey = resolvedKeyPrefix + key; uploadCacheKey = size ? getUploadsCacheKey({ file: data instanceof File ? data : undefined, - accessLevel: resolveAccessLevel(uploadDataOptions?.accessLevel), + accessLevel: resolvedAccessLevel, contentType: uploadDataOptions?.contentType, bucket: resolvedBucket!, size, - key, + key: objectKey, }) : undefined; @@ -186,12 +208,15 @@ export const getMultipartUploadHandlers = ( await removeCachedUpload(uploadCacheKey); } - return { - key, + const result = { eTag, contentType, metadata, }; + + return inputType === STORAGE_INPUT_KEY + ? { key: objectKey, ...result } + : { path: objectKey, ...result }; }; const startUploadWithResumability = () => @@ -208,7 +233,7 @@ export const getMultipartUploadHandlers = ( }); const multipartUploadJob = () => - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { resolveCallback = resolve; rejectCallback = reject; startUploadWithResumability(); @@ -232,7 +257,7 @@ export const getMultipartUploadHandlers = ( // 3. clear multipart upload on server side. await abortMultipartUpload(resolvedS3Config!, { Bucket: resolvedBucket, - Key: resolvedKeyPrefix! + key, + Key: finalKey, UploadId: inProgressUpload?.uploadId, }); }; diff --git a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts index 30819b854d8..0ded295f83e 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts @@ -5,10 +5,15 @@ import { Amplify } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { UploadDataInput } from '../../types'; -import { calculateContentMd5, resolveS3ConfigAndInput } from '../../utils'; -import { ItemKey as S3Item } from '../../types/outputs'; +import { + calculateContentMd5, + resolveS3ConfigAndInput, + validateStorageOperationInput, +} from '../../utils'; +import { ItemKey, ItemPath } from '../../types/outputs'; import { putObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; +import { STORAGE_INPUT_KEY } from '../../utils/constants'; /** * Get a function the returns a promise to call putObject API to S3. @@ -17,15 +22,21 @@ import { getStorageUserAgentValue } from '../../utils/userAgent'; */ export const putObjectJob = ( - { options: uploadDataOptions, key, data }: UploadDataInput, + uploadDataInput: UploadDataInput, abortSignal: AbortSignal, totalLength?: number, ) => - async (): Promise => { - const { bucket, keyPrefix, s3Config, isObjectLockEnabled } = + async (): Promise => { + const { options: uploadDataOptions, data } = uploadDataInput; + const { bucket, keyPrefix, s3Config, isObjectLockEnabled, identityId } = await resolveS3ConfigAndInput(Amplify, uploadDataOptions); + const { inputType, objectKey } = validateStorageOperationInput( + uploadDataInput, + identityId, + ); - const finalKey = keyPrefix + key; + const finalKey = + inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey; const { contentDisposition, contentEncoding, @@ -55,12 +66,15 @@ export const putObjectJob = }, ); - return { - key, + const result = { eTag, versionId, contentType, metadata, size: totalLength, }; + + return inputType === STORAGE_INPUT_KEY + ? { key: objectKey, ...result } + : { path: objectKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index b3bbb836b65..59ce5f8e966 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -4,7 +4,8 @@ export { GetUrlOptionsKey, GetUrlOptionsPath, - UploadDataOptions, + UploadDataOptionsPath, + UploadDataOptionsKey, GetPropertiesOptionsKey, GetPropertiesOptionsPath, ListAllOptionsPrefix, @@ -23,6 +24,8 @@ export { DownloadDataOutputPath, GetUrlOutput, UploadDataOutput, + UploadDataOutputKey, + UploadDataOutputPath, ListOutputItemKey, ListOutputItemPath, ListAllOutput, @@ -58,6 +61,8 @@ export { DownloadDataInputKey, DownloadDataInputPath, UploadDataInput, + UploadDataInputPath, + UploadDataInputKey, ListAllInput, ListPaginateInput, ListAllInputPath, diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index 4dd9c1522e2..dd956fc9747 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -16,7 +16,8 @@ import { StorageListInputPrefix, StorageRemoveInputKey, StorageRemoveInputPath, - StorageUploadDataInput, + StorageUploadDataInputKey, + StorageUploadDataInputPath, } from '../../../types'; import { CopyDestinationOptionsKey, @@ -32,7 +33,8 @@ import { ListPaginateOptionsPath, ListPaginateOptionsPrefix, RemoveOptions, - UploadDataOptions, + UploadDataOptionsKey, + UploadDataOptionsPath, } from '../types'; // TODO: support use accelerate endpoint option @@ -139,4 +141,12 @@ export type DownloadDataInputPath = /** * Input type for S3 uploadData API. */ -export type UploadDataInput = StorageUploadDataInput; +export type UploadDataInput = StrictUnion< + UploadDataInputKey | UploadDataInputPath +>; + +/** @deprecated Use {@link UploadDataInputPath} instead. */ +export type UploadDataInputKey = + StorageUploadDataInputKey; +export type UploadDataInputPath = + StorageUploadDataInputPath; diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index b9bfe0685f9..94456a04e86 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -128,8 +128,7 @@ export type DownloadDataOptions = CommonOptions & export type DownloadDataOptionsKey = ReadOptions & DownloadDataOptions; export type DownloadDataOptionsPath = DownloadDataOptions; -export type UploadDataOptions = WriteOptions & - CommonOptions & +export type UploadDataOptions = CommonOptions & TransferOptions & { /** * The default content-disposition header value of the file when downloading it. @@ -153,6 +152,10 @@ export type UploadDataOptions = WriteOptions & metadata?: Record; }; +/** @deprecated Use {@link UploadDataOptionsPath} instead. */ +export type UploadDataOptionsKey = WriteOptions & UploadDataOptions; +export type UploadDataOptionsPath = UploadDataOptions; + /** @deprecated This may be removed in the next major version. */ export type CopySourceOptionsKey = ReadOptions & { /** @deprecated This may be removed in the next major version. */ diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index 43bbbc7f3bd..5e624077854 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -71,10 +71,14 @@ export type DownloadDataOutput = DownloadDataOutputKey | DownloadDataOutputPath; */ export type GetUrlOutput = StorageGetUrlOutput; +/** @deprecated Use {@link UploadDataOutputPath} instead. */ +export type UploadDataOutputKey = UploadTask; +export type UploadDataOutputPath = UploadTask; + /** * Output type for S3 uploadData API. */ -export type UploadDataOutput = UploadTask; +export type UploadDataOutput = UploadDataOutputKey | UploadDataOutputPath; /** @deprecated Use {@link GetPropertiesOutputPath} instead. */ export type GetPropertiesOutputKey = ItemKey; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 77d3082b2e0..aa4210cb72c 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -17,7 +17,8 @@ export { StorageRemoveInputKey, StorageDownloadDataInputKey, StorageDownloadDataInputPath, - StorageUploadDataInput, + StorageUploadDataInputKey, + StorageUploadDataInputPath, StorageCopyInputKey, StorageCopyInputPath, StorageGetUrlInputKey, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 1877958c905..aa3447ec0d9 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -25,11 +25,13 @@ export interface StorageOperationInputKey { export interface StorageOperationInputPath { path: string | (({ identityId }: { identityId?: string }) => string); } + /** @deprecated Use {@link StorageOperationInputPath} instead. */ export interface StorageOperationInputPrefix { /** @deprecated Use `path` instead. */ prefix?: string; } + export interface StorageOperationOptionsInput { options?: Options; } @@ -76,10 +78,15 @@ export type StorageGetUrlInputKey = export type StorageGetUrlInputPath = StorageOperationInputPath & StorageOperationOptionsInput; -export type StorageUploadDataInput = - StorageOperationInput & { - data: StorageUploadDataPayload; - }; +/** @deprecated Use {@link StorageUploadDataInputPath} instead. */ +export type StorageUploadDataInputKey = + StorageOperationInputKey & + StorageOperationOptionsInput & + StorageUploadDataInputPayload; + +export type StorageUploadDataInputPath = StorageOperationInputPath & + StorageOperationOptionsInput & + StorageUploadDataInputPayload; /** @deprecated Use {@link StorageCopyInputPath} instead. */ export interface StorageCopyInputKey< @@ -113,3 +120,7 @@ export type StorageUploadDataPayload = | ArrayBufferView | ArrayBuffer | string; + +export interface StorageUploadDataInputPayload { + data: StorageUploadDataPayload; +} From 6104ab781f6bd6515545c6c60539385bf18cb991 Mon Sep 17 00:00:00 2001 From: Francisco Rodriguez Date: Thu, 28 Mar 2024 14:13:50 -0700 Subject: [PATCH 16/33] Feat: Amplify Outputs parse (#13150) --- .../__tests__/initSingleton.test.ts | 94 +++++ packages/aws-amplify/src/initSingleton.ts | 7 +- .../__tests__/parseAmplifyOutputs.test.ts | 252 +++++++++++++ packages/core/src/libraryUtils.ts | 2 + packages/core/src/parseAmplifyOutputs.ts | 347 ++++++++++++++++++ packages/core/src/singleton/API/types.ts | 4 +- .../src/singleton/AmplifyOutputs/types.ts | 116 ++++++ packages/core/src/singleton/Auth/types.ts | 4 +- yarn.lock | 6 +- 9 files changed, 825 insertions(+), 7 deletions(-) create mode 100644 packages/core/__tests__/parseAmplifyOutputs.test.ts create mode 100644 packages/core/src/parseAmplifyOutputs.ts create mode 100644 packages/core/src/singleton/AmplifyOutputs/types.ts diff --git a/packages/aws-amplify/__tests__/initSingleton.test.ts b/packages/aws-amplify/__tests__/initSingleton.test.ts index dec181ca74a..3c5dbcae216 100644 --- a/packages/aws-amplify/__tests__/initSingleton.test.ts +++ b/packages/aws-amplify/__tests__/initSingleton.test.ts @@ -13,6 +13,7 @@ import { } from '../src/auth/cognito'; import { Amplify } from '../src'; +import { AmplifyOutputs } from '@aws-amplify/core/internals/utils'; jest.mock('@aws-amplify/core'); jest.mock('../src/auth/cognito', () => ({ @@ -68,6 +69,99 @@ describe('initSingleton (DefaultAmplify)', () => { mockAmplifySingletonGetConfig.mockReset(); }); + describe('Amplify configure with AmplifyOutputs format', () => { + it('should use AmplifyOutputs config type', () => { + const amplifyOutputs: AmplifyOutputs = { + 'version': '1', + 'storage': { + 'aws_region': 'us-east-1', + 'bucket_name': 'my-bucket-name' + }, + 'auth': { + 'user_pool_id': 'us-east-1:', + 'user_pool_client_id': 'xxxx', + 'aws_region': 'us-east-1', + 'identity_pool_id': 'test' + }, + 'analytics': { + amazon_pinpoint: { + app_id: 'xxxxx', + aws_region: 'us-east-1' + } + }, + 'geo': { + aws_region: 'us-east-1', + maps: { + items: { 'map1': { name: 'map1', style: 'color' } }, + default: 'map1' + }, + geofence_collections: { + items: ['a', 'b', 'c'], + default: 'a' + }, + search_indices: { + items: ['a', 'b', 'c'], + default: 'a' + } + } + }; + + Amplify.configure(amplifyOutputs); + + expect(AmplifySingleton.configure).toHaveBeenCalledWith({ + Storage: { + S3: { + bucket: 'my-bucket-name', + region: 'us-east-1' + }, + }, + Auth: { + Cognito: { + identityPoolId: 'test', + userPoolId: 'us-east-1:', + userPoolClientId: 'xxxx' + } + }, + Analytics: { + Pinpoint: { + appId: 'xxxxx', + region: 'us-east-1', + }, + }, + Geo: { + LocationService: { + 'geofenceCollections': { + 'default': 'a', + 'items': [ + 'a', + 'b', + 'c', + ], + }, + 'maps': { + 'default': 'map1', + 'items': { + 'map1': { + 'name': 'map1', + 'style': 'color' + }, + }, + }, + 'region': 'us-east-1', + 'searchIndices': { + 'default': 'a', + 'items': [ + 'a', + 'b', + 'c', + ], + }, + }, + } + }, expect.anything()); + }) + }); + describe('DefaultAmplify.configure()', () => { it('should take the legacy CLI shaped config object for configuring the underlying Amplify Singleton', () => { const mockLegacyConfig = { diff --git a/packages/aws-amplify/src/initSingleton.ts b/packages/aws-amplify/src/initSingleton.ts index 785ed9fd214..fd6d29e2635 100644 --- a/packages/aws-amplify/src/initSingleton.ts +++ b/packages/aws-amplify/src/initSingleton.ts @@ -8,8 +8,11 @@ import { defaultStorage, } from '@aws-amplify/core'; import { + AmplifyOutputs, LegacyConfig, + isAmplifyOutputs, parseAWSExports, + parseAmplifyOutputs, } from '@aws-amplify/core/internals/utils'; import { @@ -31,13 +34,15 @@ export const DefaultAmplify = { * Amplify.configure(config); */ configure( - resourceConfig: ResourcesConfig | LegacyConfig, + resourceConfig: ResourcesConfig | LegacyConfig | AmplifyOutputs, libraryOptions?: LibraryOptions, ): void { let resolvedResourceConfig: ResourcesConfig; if (Object.keys(resourceConfig).some(key => key.startsWith('aws_'))) { resolvedResourceConfig = parseAWSExports(resourceConfig); + } else if (isAmplifyOutputs(resourceConfig)) { + resolvedResourceConfig = parseAmplifyOutputs(resourceConfig); } else { resolvedResourceConfig = resourceConfig as ResourcesConfig; } diff --git a/packages/core/__tests__/parseAmplifyOutputs.test.ts b/packages/core/__tests__/parseAmplifyOutputs.test.ts new file mode 100644 index 00000000000..d226874a493 --- /dev/null +++ b/packages/core/__tests__/parseAmplifyOutputs.test.ts @@ -0,0 +1,252 @@ +import { AmplifyOutputs, parseAmplifyOutputs } from '../src/libraryUtils'; + +describe('parseAmplifyOutputs tests', () => { + describe('auth tests', () => { + it('should parse auth happy path (all enabled)', () => { + const amplifyOutputs: AmplifyOutputs = { + 'version': '1', + 'auth': { + 'user_pool_id': 'us-east-1:', + 'user_pool_client_id': 'xxxx', + 'aws_region': 'us-east-1', + 'identity_pool_id': 'test', + 'oauth': { + domain: 'https://cognito.com...', + redirect_sign_in_uri: ['http://localhost:3000/welcome'], + redirect_sign_out_uri: ['http://localhost:3000/come-back-soon'], + response_type: 'code', + scopes: ['profile', '...'], + identity_providers: ['GOOGLE'], + }, + 'password_policy': { + 'min_length': 8, + 'require_lowercase': true, + 'require_uppercase': true, + 'require_symbols': true, + 'require_numbers': true + }, + 'standard_required_attributes': ['email'], + 'username_attributes': ['EMAIL'], + 'user_verification_mechanisms': ['EMAIL'], + 'unauthenticated_identities_enabled': true, + 'mfa_configuration': 'OPTIONAL', + 'mfa_methods': ['SMS'] + }, + }; + + const result = parseAmplifyOutputs(amplifyOutputs); + expect(result).toEqual({ + 'Auth': { + 'Cognito': { + 'allowGuestAccess': true, + 'identityPoolId': 'test', + 'mfa': { + 'smsEnabled': true, + 'status': 'optional', + 'totpEnabled': false, + }, + 'passwordFormat': { + 'minLength': 8, + 'requireLowercase': true, + 'requireNumbers': true, + 'requireSpecialCharacters': true, + 'requireUppercase': true, + }, + 'userAttributes': { + 'email': { + 'required': true, + }, + }, + 'userPoolClientId': 'xxxx', + 'userPoolId': 'us-east-1:', + 'loginWith': { + 'email': true, + 'oauth': { + 'domain': 'https://cognito.com...', + 'providers': [ + 'Google', + ], + 'redirectSignIn': [ + 'http://localhost:3000/welcome', + ], + 'redirectSignOut': [ + 'http://localhost:3000/come-back-soon', + ], + 'responseType': 'code', + 'scopes': [ + 'profile', + '...', + ] + } + } + } + } + }); + }); + }); + + describe('storage tests', () => { + it('should parse storage happy path', () => { + const amplifyOutputs: AmplifyOutputs = { + 'version': '1', + 'storage': { + aws_region: 'us-west-2', + bucket_name: 'storage-bucket-test' + } + }; + + const result = parseAmplifyOutputs(amplifyOutputs); + + expect(result).toEqual({ + Storage: { + S3: { + 'bucket': 'storage-bucket-test', + 'region': 'us-west-2', + } + } + }) + }) + }); + + describe('analytics tests', () => { + it('should parse all providers', () => { + const amplifyOutputs: AmplifyOutputs = { + 'version': '1', + 'analytics': { + amazon_pinpoint: { + app_id: 'xxxxx', + aws_region: 'us-east-1' + } + } + }; + + const result = parseAmplifyOutputs(amplifyOutputs); + + expect(result).toEqual({ + 'Analytics': { + 'Pinpoint': { + 'appId': 'xxxxx', + 'region': 'us-east-1', + } + } + }) + }); + }); + + describe('geo tests', () => { + it('should parse LocationService config', () => { + const amplifyOutputs: AmplifyOutputs = { + 'version': '1', + 'geo': { + aws_region: 'us-east-1', + maps: { + items: { + 'map1': { name: 'map1', style: 'color' } + }, + default: 'map1' + }, + geofence_collections: { + items: ['a', 'b', 'c'], + default: 'a' + }, + search_indices: { + items: ['a', 'b', 'c'], + default: 'a' + } + } + }; + const result = parseAmplifyOutputs(amplifyOutputs); + expect(result).toEqual({ + 'Geo': { + 'LocationService': { + 'geofenceCollections': { + 'default': 'a', + 'items': [ + 'a', + 'b', + 'c', + ], + }, + 'maps': { + 'default': 'map1', + 'items': { + 'map1': { + style: 'color', + name: 'map1' + }, + }, + }, + 'region': 'us-east-1', + 'searchIndices': { + 'default': 'a', + 'items': [ + 'a', + 'b', + 'c', + ], + }, + }, + } + }) + }) + + }); + + describe('data tests', () => { + it('should configure data', () => { + const amplifyOutputs: AmplifyOutputs = { + 'version': '1', + 'data': { + aws_region: 'us-west-2', + url: 'https://api.appsyncaws.com/graphql', + authorization_types: ['API_KEY'], + default_authorization_type: 'API_KEY', + api_key: 'da-xxxx' + } + }; + + const result = parseAmplifyOutputs(amplifyOutputs); + expect(result).toEqual({ + API: { + 'GraphQL': { + endpoint: 'https://api.appsyncaws.com/graphql', + region: 'us-west-2', + apiKey: 'da-xxxx', + defaultAuthMode: 'apiKey' + } + } + }); + }); + + describe('notifications tests', () => { + it('should configure notifications', () => { + const amplifyOutputs: AmplifyOutputs = { + 'version': '1', + 'notifications': { + aws_region: 'us-west-2', + pinpoint_app_id: 'appid123', + channels: ['APNS', 'EMAIL', 'FCM', 'IN_APP_MESSAGING', 'SMS'], + } + }; + + const result = parseAmplifyOutputs(amplifyOutputs); + expect(result).toEqual({ + 'Notifications': { + 'InAppMessaging': { + 'Pinpoint': { + 'appId': 'appid123', + 'region': 'us-west-2', + }, + }, + 'PushNotification': { + 'Pinpoint': { + 'appId': 'appid123', + 'region': 'us-west-2', + }, + } + } + }); + }); + }) + }) +}) diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index a11eb0cf1c4..4e1f576834e 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -21,7 +21,9 @@ export { deDupeAsyncFunction, } from './utils'; export { parseAWSExports } from './parseAWSExports'; +export { isAmplifyOutputs, parseAmplifyOutputs } from './parseAmplifyOutputs'; export { LegacyConfig } from './singleton/types'; +export { AmplifyOutputs } from './singleton/AmplifyOutputs/types'; export { ADD_OAUTH_LISTENER } from './singleton/constants'; export { amplifyUuid } from './utils/amplifyUuid'; export { AmplifyUrl, AmplifyUrlSearchParams } from './utils/amplifyUrl'; diff --git a/packages/core/src/parseAmplifyOutputs.ts b/packages/core/src/parseAmplifyOutputs.ts new file mode 100644 index 00000000000..badd70aef02 --- /dev/null +++ b/packages/core/src/parseAmplifyOutputs.ts @@ -0,0 +1,347 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* This is because JSON schema contains keys with snake_case */ +/* eslint-disable camelcase */ + +/* Does not like exahaustive checks */ +/* eslint-disable no-case-declarations */ + +import { + APIConfig, + APIGraphQLConfig, + GraphQLAuthMode, +} from './singleton/API/types'; +import { + CognitoUserPoolConfigMfaStatus, + OAuthProvider, +} from './singleton/Auth/types'; +import { NotificationsConfig } from './singleton/Notifications/types'; +import { + AmplifyOutputs, + AmplifyOutputsAnalyticsProperties, + AmplifyOutputsAuthMFAConfiguration, + AmplifyOutputsAuthProperties, + AmplifyOutputsDataProperties, + AmplifyOutputsGeoProperties, + AmplifyOutputsNotificationsProperties, + AmplifyOutputsOAuthIdentityProvider, + AmplifyOutputsStorageProperties, + AuthType, +} from './singleton/AmplifyOutputs/types'; +import { + AnalyticsConfig, + AuthConfig, + GeoConfig, + LegacyConfig, + ResourcesConfig, + StorageConfig, +} from './singleton/types'; + +export function isAmplifyOutputs( + config: ResourcesConfig | LegacyConfig | AmplifyOutputs, +): config is AmplifyOutputs { + // version format initially will be '1' but is expected to be something like x.y where x is major and y minor version + const { version } = config as AmplifyOutputs; + + if (!version) { + return false; + } + + return version.startsWith('1'); +} + +function parseStorage( + amplifyOutputsStorageProperties?: AmplifyOutputsStorageProperties, +): StorageConfig | undefined { + if (!amplifyOutputsStorageProperties) { + return undefined; + } + + const { bucket_name, aws_region } = amplifyOutputsStorageProperties; + + return { + S3: { + bucket: bucket_name, + region: aws_region, + }, + }; +} + +function parseAuth( + amplifyOutputsAuthProperties?: AmplifyOutputsAuthProperties, +): AuthConfig | undefined { + if (!amplifyOutputsAuthProperties) { + return undefined; + } + + const { + user_pool_id, + user_pool_client_id, + identity_pool_id, + password_policy, + mfa_configuration, + mfa_methods, + unauthenticated_identities_enabled, + oauth, + username_attributes, + standard_required_attributes, + } = amplifyOutputsAuthProperties; + + const authConfig = { + Cognito: { + userPoolId: user_pool_id, + userPoolClientId: user_pool_client_id, + }, + } as AuthConfig; + + if (identity_pool_id) { + authConfig.Cognito = { + ...authConfig.Cognito, + identityPoolId: identity_pool_id, + }; + } + + if (password_policy) { + authConfig.Cognito.passwordFormat = { + requireLowercase: password_policy.require_lowercase, + requireNumbers: password_policy.require_numbers, + requireUppercase: password_policy.require_uppercase, + requireSpecialCharacters: password_policy.require_symbols, + minLength: password_policy.min_length ?? 6, + }; + } + + if (mfa_configuration) { + authConfig.Cognito.mfa = { + status: getMfaStatus(mfa_configuration), + smsEnabled: mfa_methods?.includes('SMS'), + totpEnabled: mfa_methods?.includes('TOTP'), + }; + } + + if (unauthenticated_identities_enabled) { + authConfig.Cognito.allowGuestAccess = unauthenticated_identities_enabled; + } + + if (oauth) { + authConfig.Cognito.loginWith = { + oauth: { + domain: oauth.domain, + redirectSignIn: oauth.redirect_sign_in_uri, + redirectSignOut: oauth.redirect_sign_out_uri, + responseType: oauth.response_type, + scopes: oauth.scopes, + providers: getOAuthProviders(oauth.identity_providers), + }, + }; + } + + if (username_attributes?.includes('EMAIL')) { + authConfig.Cognito.loginWith = { + ...authConfig.Cognito.loginWith, + email: true, + }; + } + + if (username_attributes?.includes('PHONE_NUMBER')) { + authConfig.Cognito.loginWith = { + ...authConfig.Cognito.loginWith, + phone: true, + }; + } + + if (username_attributes?.includes('USERNAME')) { + authConfig.Cognito.loginWith = { + ...authConfig.Cognito.loginWith, + username: true, + }; + } + + if (standard_required_attributes) { + authConfig.Cognito.userAttributes = standard_required_attributes.reduce( + (acc, curr) => ({ ...acc, [curr]: { required: true } }), + {}, + ); + } + + return authConfig; +} + +export function parseAnalytics( + amplifyOutputsAnalyticsProperties?: AmplifyOutputsAnalyticsProperties, +): AnalyticsConfig | undefined { + if (!amplifyOutputsAnalyticsProperties?.amazon_pinpoint) { + return undefined; + } + + const { amazon_pinpoint } = amplifyOutputsAnalyticsProperties; + + return { + Pinpoint: { + appId: amazon_pinpoint.app_id, + region: amazon_pinpoint.aws_region, + }, + }; +} + +function parseGeo( + amplifyOutputsAnalyticsProperties?: AmplifyOutputsGeoProperties, +): GeoConfig | undefined { + if (!amplifyOutputsAnalyticsProperties) { + return undefined; + } + + const { aws_region, geofence_collections, maps, search_indices } = + amplifyOutputsAnalyticsProperties; + + return { + LocationService: { + region: aws_region, + searchIndices: search_indices, + geofenceCollections: geofence_collections, + maps, + }, + }; +} + +function parseData( + amplifyOutputsDataProperties?: AmplifyOutputsDataProperties, +): APIConfig | undefined { + if (!amplifyOutputsDataProperties) { + return undefined; + } + + const { + aws_region, + default_authorization_type, + url, + api_key, + model_introspection, + } = amplifyOutputsDataProperties; + + const GraphQL: APIGraphQLConfig = { + endpoint: url, + defaultAuthMode: getGraphQLAuthMode(default_authorization_type), + region: aws_region, + apiKey: api_key, + modelIntrospection: model_introspection, + }; + + return { + GraphQL, + }; +} + +function parseNotifications( + amplifyOutputsNotificationsProperties?: AmplifyOutputsNotificationsProperties, +): NotificationsConfig | undefined { + if (!amplifyOutputsNotificationsProperties) { + return undefined; + } + + const { aws_region, channels, pinpoint_app_id } = + amplifyOutputsNotificationsProperties; + + const hasInAppMessaging = channels.includes('IN_APP_MESSAGING'); + const hasPushNotification = + channels.includes('APNS') || channels.includes('FCM'); + + if (!(hasInAppMessaging || hasPushNotification)) { + return undefined; + } + + // At this point, we know the Amplify outputs contains at least one supported channel + const notificationsConfig: NotificationsConfig = {} as NotificationsConfig; + + if (hasInAppMessaging) { + notificationsConfig.InAppMessaging = { + Pinpoint: { + appId: pinpoint_app_id, + region: aws_region, + }, + }; + } + + if (hasPushNotification) { + notificationsConfig.PushNotification = { + Pinpoint: { + appId: pinpoint_app_id, + region: aws_region, + }, + }; + } + + return notificationsConfig; +} + +export function parseAmplifyOutputs( + amplifyOutputs: AmplifyOutputs, +): ResourcesConfig { + const resourcesConfig: ResourcesConfig = {}; + + if (amplifyOutputs.storage) { + resourcesConfig.Storage = parseStorage(amplifyOutputs.storage); + } + + if (amplifyOutputs.auth) { + resourcesConfig.Auth = parseAuth(amplifyOutputs.auth); + } + + if (amplifyOutputs.analytics) { + resourcesConfig.Analytics = parseAnalytics(amplifyOutputs.analytics); + } + + if (amplifyOutputs.geo) { + resourcesConfig.Geo = parseGeo(amplifyOutputs.geo); + } + + if (amplifyOutputs.data) { + resourcesConfig.API = parseData(amplifyOutputs.data); + } + + if (amplifyOutputs.notifications) { + resourcesConfig.Notifications = parseNotifications( + amplifyOutputs.notifications, + ); + } + + return resourcesConfig; +} + +const authModeNames: Record = { + AMAZON_COGNITO_USER_POOLS: 'userPool', + API_KEY: 'apiKey', + AWS_IAM: 'iam', + AWS_LAMBDA: 'lambda', + OPENID_CONNECT: 'oidc', +}; + +function getGraphQLAuthMode(authType: AuthType): GraphQLAuthMode { + return authModeNames[authType]; +} + +const providerNames: Record< + AmplifyOutputsOAuthIdentityProvider, + OAuthProvider +> = { + GOOGLE: 'Google', + LOGIN_WITH_AMAZON: 'Amazon', + FACEBOOK: 'Facebook', + SIGN_IN_WITH_APPLE: 'Apple', +}; + +function getOAuthProviders( + providers: AmplifyOutputsOAuthIdentityProvider[] = [], +): OAuthProvider[] { + return providers.map(provider => providerNames[provider]); +} + +function getMfaStatus( + mfaConfiguration: AmplifyOutputsAuthMFAConfiguration, +): CognitoUserPoolConfigMfaStatus { + if (mfaConfiguration === 'OPTIONAL') return 'optional'; + if (mfaConfiguration === 'REQUIRED') return 'on'; + + return 'off'; +} diff --git a/packages/core/src/singleton/API/types.ts b/packages/core/src/singleton/API/types.ts index 5a9e4b5de41..419d4410354 100644 --- a/packages/core/src/singleton/API/types.ts +++ b/packages/core/src/singleton/API/types.ts @@ -18,7 +18,7 @@ export interface LibraryAPIOptions { }; } -interface APIGraphQLConfig { +export interface APIGraphQLConfig { /** * Required GraphQL endpoint, must be a valid URL string. */ @@ -47,7 +47,7 @@ interface APIGraphQLConfig { modelIntrospection?: ModelIntrospectionSchema; } -interface APIRestConfig { +export interface APIRestConfig { /** * Required REST endpoint, must be a valid URL string. */ diff --git a/packages/core/src/singleton/AmplifyOutputs/types.ts b/packages/core/src/singleton/AmplifyOutputs/types.ts new file mode 100644 index 00000000000..c28b2c53513 --- /dev/null +++ b/packages/core/src/singleton/AmplifyOutputs/types.ts @@ -0,0 +1,116 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ModelIntrospectionSchema } from '../API/types'; +import { AuthStandardAttributeKey } from '../Auth/types'; + +export type AmplifyOutputsOAuthIdentityProvider = + | 'GOOGLE' + | 'FACEBOOK' + | 'LOGIN_WITH_AMAZON' + | 'SIGN_IN_WITH_APPLE'; + +type AmplifyOutputsAuthUsernameAttribute = + | 'EMAIL' + | 'PHONE_NUMBER' + | 'USERNAME'; + +type AmplifyOutputsAuthUserVerificationMethod = 'EMAIL' | 'PHONE_NUMBER'; + +export type AmplifyOutputsAuthMFAConfiguration = + | 'OPTIONAL' + | 'REQUIRED' + | 'NONE'; + +export type AmplifyOutputsAuthMFAMethod = 'SMS' | 'TOTP'; + +export interface AmplifyOutputsAuthProperties { + aws_region: string; + authentication_flow_type?: 'USER_SRP_AUTH' | 'CUSTOM_AUTH'; + user_pool_id: string; + user_pool_client_id: string; + identity_pool_id?: string; + password_policy?: { + min_length: number; + require_numbers: boolean; + require_lowercase: boolean; + require_uppercase: boolean; + require_symbols: boolean; + }; + oauth?: { + identity_providers: AmplifyOutputsOAuthIdentityProvider[]; + domain: string; + scopes: string[]; + redirect_sign_in_uri: string[]; + redirect_sign_out_uri: string[]; + response_type: 'code' | 'token'; + }; + standard_required_attributes?: AuthStandardAttributeKey[]; + username_attributes?: AmplifyOutputsAuthUsernameAttribute[]; + user_verification_mechanisms?: AmplifyOutputsAuthUserVerificationMethod[]; + unauthenticated_identities_enabled?: boolean; + mfa_configuration?: AmplifyOutputsAuthMFAConfiguration; + mfa_methods?: AmplifyOutputsAuthMFAMethod[]; +} + +export interface AmplifyOutputsStorageProperties { + aws_region: string; + bucket_name: string; +} + +export interface AmplifyOutputsGeoProperties { + aws_region: string; + maps?: { + items: Record; + default: string; + }; + search_indices?: { items: string[]; default: string }; + geofence_collections?: { items: string[]; default: string }; +} + +export interface AmplifyOutputsAnalyticsProperties { + amazon_pinpoint?: { + aws_region: string; + app_id: string; + }; +} + +export type AuthType = + | 'AMAZON_COGNITO_USER_POOLS' + | 'API_KEY' + | 'AWS_IAM' + | 'AWS_LAMBDA' + | 'OPENID_CONNECT'; + +export interface AmplifyOutputsDataProperties { + aws_region: string; + url: string; + default_authorization_type: AuthType; + authorization_types: AuthType[]; + model_introspection?: ModelIntrospectionSchema; + api_key?: string; + conflict_resolution_mode?: 'AUTO_MERGE' | 'OPTIMISTIC_CONCURRENCY' | 'LAMBDA'; +} + +type AmplifyOutputsNotificationChannel = + | 'IN_APP_MESSAGING' + | 'FCM' + | 'APNS' + | 'EMAIL' + | 'SMS'; + +export interface AmplifyOutputsNotificationsProperties { + aws_region: string; + pinpoint_app_id: string; + channels: AmplifyOutputsNotificationChannel[]; +} + +export interface AmplifyOutputs { + version?: string; + storage?: AmplifyOutputsStorageProperties; + auth?: AmplifyOutputsAuthProperties; + analytics?: AmplifyOutputsAnalyticsProperties; + geo?: AmplifyOutputsGeoProperties; + data?: AmplifyOutputsDataProperties; + notifications?: AmplifyOutputsNotificationsProperties; +} diff --git a/packages/core/src/singleton/Auth/types.ts b/packages/core/src/singleton/Auth/types.ts index f449e1f08e3..239810e8771 100644 --- a/packages/core/src/singleton/Auth/types.ts +++ b/packages/core/src/singleton/Auth/types.ts @@ -145,6 +145,8 @@ export interface AuthUserPoolConfig { }; } +export type CognitoUserPoolConfigMfaStatus = 'on' | 'off' | 'optional'; + export interface CognitoUserPoolConfig { userPoolClientId: string; userPoolId: string; @@ -158,7 +160,7 @@ export interface CognitoUserPoolConfig { }; userAttributes?: AuthConfigUserAttributes; mfa?: { - status?: 'on' | 'off' | 'optional'; + status?: CognitoUserPoolConfigMfaStatus; totpEnabled?: boolean; smsEnabled?: boolean; }; diff --git a/yarn.lock b/yarn.lock index d8a5b696e2f..ebad4b49012 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8087,9 +8087,9 @@ flow-parser@^0.206.0: integrity sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w== follow-redirects@^1.15.4: - version "1.15.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" - integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-each@^0.3.3: version "0.3.3" From a94480dfc58c521cbbb81b3a534f8543727831dc Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Tue, 2 Apr 2024 09:27:35 -0700 Subject: [PATCH 17/33] fix(storage): refactor storage API types (#13207) * fix(storage): refactor copy API types * fix(storage): refactor remove API types * fix(storage): refactor getUrl API types * fix(storage): refactor getProperties API types * fix(storage): refactor list API types * fix(storage): refactor download API types * fix(storage): refactor upload API types * verify all - update missed StorageOperationInputPrefix type * revert StorageOperationInputWithType type --------- Co-authored-by: Ashwin Kumar --- .../__tests__/providers/s3/apis/copy.test.ts | 8 +- .../providers/s3/apis/downloadData.test.ts | 8 +- .../providers/s3/apis/getProperties.test.ts | 8 +- .../providers/s3/apis/getUrl.test.ts | 8 +- .../__tests__/providers/s3/apis/list.test.ts | 12 +- .../storage/src/providers/s3/apis/copy.ts | 16 +- .../src/providers/s3/apis/downloadData.ts | 22 +-- .../src/providers/s3/apis/getProperties.ts | 16 +- .../storage/src/providers/s3/apis/getUrl.ts | 12 +- .../src/providers/s3/apis/internal/copy.ts | 18 +-- .../src/providers/s3/apis/internal/list.ts | 80 +++++----- .../storage/src/providers/s3/apis/list.ts | 32 ++-- .../storage/src/providers/s3/apis/remove.ts | 16 +- .../src/providers/s3/apis/server/copy.ts | 20 +-- .../providers/s3/apis/server/getProperties.ts | 20 +-- .../src/providers/s3/apis/server/getUrl.ts | 12 +- .../src/providers/s3/apis/server/list.ts | 40 ++--- .../src/providers/s3/apis/server/remove.ts | 20 +-- .../src/providers/s3/apis/uploadData/index.ts | 16 +- .../uploadData/multipart/uploadHandlers.ts | 17 +- .../s3/apis/uploadData/putObjectJob.ts | 4 +- .../storage/src/providers/s3/types/index.ts | 92 +++++------ .../storage/src/providers/s3/types/inputs.ts | 146 +++++++++--------- .../storage/src/providers/s3/types/options.ts | 43 +++--- .../storage/src/providers/s3/types/outputs.ts | 96 ++++++------ .../src/providers/s3/utils/isInputWithPath.ts | 4 +- ...validateStorageOperationInputWithPrefix.ts | 4 +- packages/storage/src/types/index.ts | 32 ++-- packages/storage/src/types/inputs.ts | 83 +++++----- packages/storage/src/types/outputs.ts | 8 +- 30 files changed, 469 insertions(+), 444 deletions(-) diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index 24dda7bcd3f..8221377fea9 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -8,8 +8,8 @@ import { StorageValidationErrorCode } from '../../../../src/errors/types/validat import { copyObject } from '../../../../src/providers/s3/utils/client'; import { copy } from '../../../../src/providers/s3/apis'; import { - CopySourceOptionsKey, - CopyDestinationOptionsKey, + CopySourceOptionsWithKey, + CopyDestinationOptionsWithKey, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -165,11 +165,11 @@ describe('copy API', () => { expect( await copy({ source: { - ...(source as CopySourceOptionsKey), + ...(source as CopySourceOptionsWithKey), key: sourceKey, }, destination: { - ...(destination as CopyDestinationOptionsKey), + ...(destination as CopyDestinationOptionsWithKey), key: destinationKey, }, }), diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 88e9cb21eb7..3904d9436dc 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -10,8 +10,8 @@ import { validateStorageOperationInput, } from '../../../../src/providers/s3/utils'; import { - DownloadDataOptionsKey, - DownloadDataOptionsPath, + DownloadDataOptionsWithKey, + DownloadDataOptionsWithPath, } from '../../../../src/providers/s3/types'; import { STORAGE_INPUT_KEY, @@ -107,7 +107,7 @@ describe('downloadData with key', () => { ...options, useAccelerateEndpoint: true, onProgress, - } as DownloadDataOptionsKey, + } as DownloadDataOptionsWithKey, }); const job = mockCreateDownloadTask.mock.calls[0][0].job; await job(); @@ -235,7 +235,7 @@ describe('downloadData with path', () => { options: { useAccelerateEndpoint: true, onProgress, - } as DownloadDataOptionsPath, + } as DownloadDataOptionsWithPath, }); const job = mockCreateDownloadTask.mock.calls[0][0].job; await job(); diff --git a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts index a2f0df8e886..151ae7cebd2 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -6,8 +6,8 @@ import { getProperties } from '../../../../src/providers/s3'; import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { - GetPropertiesOptionsKey, - GetPropertiesOptionsPath, + GetPropertiesOptionsWithKey, + GetPropertiesOptionsWithPath, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -111,7 +111,7 @@ describe('getProperties with key', () => { expect( await getProperties({ key, - options: options as GetPropertiesOptionsKey, + options: options as GetPropertiesOptionsWithKey, }), ).toEqual(expected); expect(headObject).toHaveBeenCalledTimes(1); @@ -218,7 +218,7 @@ describe('Happy cases: With path', () => { path: testPath, options: { useAccelerateEndpoint: true, - } as GetPropertiesOptionsPath, + } as GetPropertiesOptionsWithPath, }), ).toEqual(expected); expect(headObject).toHaveBeenCalledTimes(1); diff --git a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts index 28804743c7d..a68c8c3516b 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -9,8 +9,8 @@ import { headObject, } from '../../../../src/providers/s3/utils/client'; import { - GetUrlOptionsKey, - GetUrlOptionsPath, + GetUrlOptionsWithKey, + GetUrlOptionsWithPath, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -111,7 +111,7 @@ describe('getUrl test with key', () => { options: { ...options, validateObjectExistence: true, - } as GetUrlOptionsKey, + } as GetUrlOptionsWithKey, }); expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledTimes(1); @@ -208,7 +208,7 @@ describe('getUrl test with path', () => { path, options: { validateObjectExistence: true, - } as GetUrlOptionsPath, + } as GetUrlOptionsWithPath, }); expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledTimes(1); diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index 7f1446c7c6c..1db5fa5a6dc 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -6,8 +6,8 @@ import { Amplify } from '@aws-amplify/core'; import { listObjectsV2 } from '../../../../src/providers/s3/utils/client'; import { list } from '../../../../src/providers/s3'; import { - ListAllOptionsPrefix, - ListPaginateOptionsPrefix, + ListAllOptionsWithPrefix, + ListPaginateOptionsWithPrefix, } from '../../../../src/providers/s3/types'; import { StorageValidationErrorCode } from '../../../../src/errors/types/validation'; @@ -145,7 +145,7 @@ describe('list API', () => { }); let response = await list({ prefix: key, - options: options as ListPaginateOptionsPrefix, + options: options as ListPaginateOptionsWithPrefix, }); expect(response.items).toEqual([{ ...listResultItem, key: key ?? '' }]); expect(response.nextToken).toEqual(nextToken); @@ -177,7 +177,7 @@ describe('list API', () => { const response = await list({ prefix: key, options: { - ...(options as ListPaginateOptionsPrefix), + ...(options as ListPaginateOptionsWithPrefix), pageSize: customPageSize, nextToken: nextToken, }, @@ -206,7 +206,7 @@ describe('list API', () => { }); let response = await list({ prefix: key, - options: options as ListPaginateOptionsPrefix, + options: options as ListPaginateOptionsWithPrefix, }); expect(response.items).toEqual([]); @@ -229,7 +229,7 @@ describe('list API', () => { mockListObjectsV2ApiWithPages(3); const result = await list({ prefix: key, - options: { ...options, listAll: true } as ListAllOptionsPrefix, + options: { ...options, listAll: true } as ListAllOptionsWithPrefix, }); const listResult = { ...listResultItem, key: key ?? '' }; diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index 1978d9595b3..f71c11dfb56 100644 --- a/packages/storage/src/providers/s3/apis/copy.ts +++ b/packages/storage/src/providers/s3/apis/copy.ts @@ -5,11 +5,11 @@ import { Amplify } from '@aws-amplify/core'; import { CopyInput, - CopyInputKey, - CopyInputPath, + CopyInputWithKey, + CopyInputWithPath, CopyOutput, - CopyOutputKey, - CopyOutputPath, + CopyOutputWithKey, + CopyOutputWithPath, } from '../types'; import { copy as copyInternal } from './internal/copy'; @@ -18,13 +18,13 @@ interface Copy { /** * Copy an object from a source to a destination object within the same bucket. * - * @param input - The CopyInputPath object. + * @param input - The CopyInputWithPath object. * @returns Output containing the destination object path. * @throws service: `S3Exception` - Thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Thrown when * source or destination path is not defined. */ - (input: CopyInputPath): Promise; + (input: CopyInputWithPath): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. @@ -32,13 +32,13 @@ interface Copy { * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across * different accessLevel or identityId (if source object's accessLevel is 'protected'). * - * @param input - The CopyInputKey object. + * @param input - The CopyInputWithKey object. * @returns Output containing the destination object key. * @throws service: `S3Exception` - Thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Thrown when * source or destination key is not defined. */ - (input: CopyInputKey): Promise; + (input: CopyInputWithKey): Promise; } export const copy: Copy = ( diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index e083ada06e8..6dcd17b7fab 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -6,11 +6,11 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { DownloadDataInput, - DownloadDataInputKey, - DownloadDataInputPath, + DownloadDataInputWithKey, + DownloadDataInputWithPath, DownloadDataOutput, - DownloadDataOutputKey, - DownloadDataOutputPath, + DownloadDataOutputWithKey, + DownloadDataOutputWithPath, } from '../types'; import { resolveS3ConfigAndInput } from '../utils/resolveS3ConfigAndInput'; import { createDownloadTask, validateStorageOperationInput } from '../utils'; @@ -19,8 +19,8 @@ import { getStorageUserAgentValue } from '../utils/userAgent'; import { logger } from '../../../utils'; import { StorageDownloadDataOutput, - StorageItemKey, - StorageItemPath, + StorageItemWithKey, + StorageItemWithPath, } from '../../../types'; import { STORAGE_INPUT_KEY } from '../utils/constants'; @@ -28,7 +28,7 @@ interface DownloadData { /** * Download S3 object data to memory * - * @param input - The DownloadDataInputPath object. + * @param input - The DownloadDataInputWithPath object. * @returns A cancelable task exposing result promise from `result` property. * @throws service: `S3Exception` - thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Validation errors @@ -55,14 +55,14 @@ interface DownloadData { * } *``` */ - (input: DownloadDataInputPath): DownloadDataOutputPath; + (input: DownloadDataInputWithPath): DownloadDataOutputWithPath; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. * * Download S3 object data to memory * - * @param input - The DownloadDataInputKey object. + * @param input - The DownloadDataInputWithKey object. * @returns A cancelable task exposing result promise from `result` property. * @throws service: `S3Exception` - thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Validation errors @@ -89,7 +89,7 @@ interface DownloadData { * } *``` */ - (input: DownloadDataInputKey): DownloadDataOutputKey; + (input: DownloadDataInputWithKey): DownloadDataOutputWithKey; } export const downloadData: DownloadData = ( @@ -110,7 +110,7 @@ export const downloadData: DownloadData = ( const downloadDataJob = (downloadDataInput: DownloadDataInput, abortSignal: AbortSignal) => async (): Promise< - StorageDownloadDataOutput + StorageDownloadDataOutput > => { const { options: downloadDataOptions } = downloadDataInput; const { bucket, keyPrefix, s3Config, identityId } = diff --git a/packages/storage/src/providers/s3/apis/getProperties.ts b/packages/storage/src/providers/s3/apis/getProperties.ts index 227406bc369..35afaa2e014 100644 --- a/packages/storage/src/providers/s3/apis/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/getProperties.ts @@ -5,11 +5,11 @@ import { Amplify } from '@aws-amplify/core'; import { GetPropertiesInput, - GetPropertiesInputKey, - GetPropertiesInputPath, + GetPropertiesInputWithKey, + GetPropertiesInputWithPath, GetPropertiesOutput, - GetPropertiesOutputKey, - GetPropertiesOutputPath, + GetPropertiesOutputWithKey, + GetPropertiesOutputWithPath, } from '../types'; import { getProperties as getPropertiesInternal } from './internal/getProperties'; @@ -19,12 +19,12 @@ interface GetProperties { * Gets the properties of a file. The properties include S3 system metadata and * the user metadata that was provided when uploading the file. * - * @param input - The `GetPropertiesInputPath` object. + * @param input - The `GetPropertiesInputWithPath` object. * @returns Requested object properties. * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ - (input: GetPropertiesInputPath): Promise; + (input: GetPropertiesInputWithPath): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. @@ -32,12 +32,12 @@ interface GetProperties { * Gets the properties of a file. The properties include S3 system metadata and * the user metadata that was provided when uploading the file. * - * @param input - The `GetPropertiesInputKey` object. + * @param input - The `GetPropertiesInputWithKey` object. * @returns Requested object properties. * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ - (input: GetPropertiesInputKey): Promise; + (input: GetPropertiesInputWithKey): Promise; } export const getProperties: GetProperties = < diff --git a/packages/storage/src/providers/s3/apis/getUrl.ts b/packages/storage/src/providers/s3/apis/getUrl.ts index a49210063a3..5f426952fd5 100644 --- a/packages/storage/src/providers/s3/apis/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/getUrl.ts @@ -5,8 +5,8 @@ import { Amplify } from '@aws-amplify/core'; import { GetUrlInput, - GetUrlInputKey, - GetUrlInputPath, + GetUrlInputWithKey, + GetUrlInputWithPath, GetUrlOutput, } from '../types'; @@ -22,14 +22,14 @@ interface GetUrl { * to true, this method will verify the given object already exists in S3 before returning a presigned * URL, and will throw `StorageError` if the object does not exist. * - * @param input - The `GetUrlInputPath` object. + * @param input - The `GetUrlInputWithPath` object. * @returns Presigned URL and timestamp when the URL MAY expire. * @throws service: `S3Exception` - thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Validation errors * thrown either username or key are not defined. * */ - (input: GetUrlInputPath): Promise; + (input: GetUrlInputWithPath): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. @@ -42,14 +42,14 @@ interface GetUrl { * to true, this method will verify the given object already exists in S3 before returning a presigned * URL, and will throw `StorageError` if the object does not exist. * - * @param input - The `GetUrlInputKey` object. + * @param input - The `GetUrlInputWithKey` object. * @returns Presigned URL and timestamp when the URL MAY expire. * @throws service: `S3Exception` - thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Validation errors * thrown either username or key are not defined. * */ - (input: GetUrlInputKey): Promise; + (input: GetUrlInputWithKey): Promise; } export const getUrl: GetUrl = (input: GetUrlInput): Promise => diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts index 26576323b86..5b8669dbe1b 100644 --- a/packages/storage/src/providers/s3/apis/internal/copy.ts +++ b/packages/storage/src/providers/s3/apis/internal/copy.ts @@ -6,11 +6,11 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { CopyInput, - CopyInputKey, - CopyInputPath, + CopyInputWithKey, + CopyInputWithPath, CopyOutput, - CopyOutputKey, - CopyOutputPath, + CopyOutputWithKey, + CopyOutputWithPath, } from '../../types'; import { ResolvedS3Config } from '../../types/options'; import { @@ -24,7 +24,7 @@ import { copyObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; -const isCopyInputWithPath = (input: CopyInput): input is CopyInputPath => +const isCopyInputWithPath = (input: CopyInput): input is CopyInputWithPath => isInputWithPath(input.source); export const copy = async ( @@ -38,8 +38,8 @@ export const copy = async ( const copyWithPath = async ( amplify: AmplifyClassV6, - input: CopyInputPath, -): Promise => { + input: CopyInputWithPath, +): Promise => { const { source, destination } = input; const { s3Config, bucket, identityId } = await resolveS3ConfigAndInput(amplify); @@ -76,8 +76,8 @@ const copyWithPath = async ( /** @deprecated Use {@link copyWithPath} instead. */ export const copyWithKey = async ( amplify: AmplifyClassV6, - input: CopyInputKey, -): Promise => { + input: CopyInputWithKey, +): Promise => { const { source: { key: sourceKey }, destination: { key: destinationKey }, diff --git a/packages/storage/src/providers/s3/apis/internal/list.ts b/packages/storage/src/providers/s3/apis/internal/list.ts index c11a8a440f4..47d36d59178 100644 --- a/packages/storage/src/providers/s3/apis/internal/list.ts +++ b/packages/storage/src/providers/s3/apis/internal/list.ts @@ -7,14 +7,14 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { ListAllInput, ListAllOutput, - ListAllOutputPath, - ListAllOutputPrefix, - ListOutputItemKey, - ListOutputItemPath, + ListAllOutputWithPath, + ListAllOutputWithPrefix, + ListOutputItemWithKey, + ListOutputItemWithPath, ListPaginateInput, ListPaginateOutput, - ListPaginateOutputPath, - ListPaginateOutputPrefix, + ListPaginateOutputWithPath, + ListPaginateOutputWithPrefix, } from '../../types'; import { resolveS3ConfigAndInput, @@ -79,40 +79,41 @@ export const list = async ( }; if (options.listAll) { if (isInputWithPrefix) { - return _listAllPrefix({ + return _listAllWithPrefix({ ...listInputArgs, generatedPrefix, }); } else { - return _listAllPath(listInputArgs); + return _listAllWithPath(listInputArgs); } } else { if (inputType === STORAGE_INPUT_PREFIX) { - return _listPrefix({ ...listInputArgs, generatedPrefix }); + return _listWithPrefix({ ...listInputArgs, generatedPrefix }); } else { - return _listPath(listInputArgs); + return _listWithPath(listInputArgs); } } }; -/** @deprecated Use {@link _listAllPath} instead. */ -const _listAllPrefix = async ({ +/** @deprecated Use {@link _listAllWithPath} instead. */ +const _listAllWithPrefix = async ({ s3Config, listParams, generatedPrefix, -}: ListInputArgs): Promise => { - const listResult: ListOutputItemKey[] = []; +}: ListInputArgs): Promise => { + const listResult: ListOutputItemWithKey[] = []; let continuationToken = listParams.ContinuationToken; do { - const { items: pageResults, nextToken: pageNextToken } = await _listPrefix({ - generatedPrefix, - s3Config, - listParams: { - ...listParams, - ContinuationToken: continuationToken, - MaxKeys: MAX_PAGE_SIZE, - }, - }); + const { items: pageResults, nextToken: pageNextToken } = + await _listWithPrefix({ + generatedPrefix, + s3Config, + listParams: { + ...listParams, + ContinuationToken: continuationToken, + MaxKeys: MAX_PAGE_SIZE, + }, + }); listResult.push(...pageResults); continuationToken = pageNextToken; } while (continuationToken); @@ -122,12 +123,12 @@ const _listAllPrefix = async ({ }; }; -/** @deprecated Use {@link _listPath} instead. */ -const _listPrefix = async ({ +/** @deprecated Use {@link _listWithPath} instead. */ +const _listWithPrefix = async ({ s3Config, listParams, generatedPrefix, -}: ListInputArgs): Promise => { +}: ListInputArgs): Promise => { const listParamsClone = { ...listParams }; if (!listParamsClone.MaxKeys || listParamsClone.MaxKeys > MAX_PAGE_SIZE) { logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`); @@ -161,21 +162,22 @@ const _listPrefix = async ({ }; }; -const _listAllPath = async ({ +const _listAllWithPath = async ({ s3Config, listParams, -}: ListInputArgs): Promise => { - const listResult: ListOutputItemPath[] = []; +}: ListInputArgs): Promise => { + const listResult: ListOutputItemWithPath[] = []; let continuationToken = listParams.ContinuationToken; do { - const { items: pageResults, nextToken: pageNextToken } = await _listPath({ - s3Config, - listParams: { - ...listParams, - ContinuationToken: continuationToken, - MaxKeys: MAX_PAGE_SIZE, - }, - }); + const { items: pageResults, nextToken: pageNextToken } = + await _listWithPath({ + s3Config, + listParams: { + ...listParams, + ContinuationToken: continuationToken, + MaxKeys: MAX_PAGE_SIZE, + }, + }); listResult.push(...pageResults); continuationToken = pageNextToken; } while (continuationToken); @@ -185,10 +187,10 @@ const _listAllPath = async ({ }; }; -const _listPath = async ({ +const _listWithPath = async ({ s3Config, listParams, -}: ListInputArgs): Promise => { +}: ListInputArgs): Promise => { const listParamsClone = { ...listParams }; if (!listParamsClone.MaxKeys || listParamsClone.MaxKeys > MAX_PAGE_SIZE) { logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`); diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index 7fce787b9ea..9ee3662430b 100644 --- a/packages/storage/src/providers/s3/apis/list.ts +++ b/packages/storage/src/providers/s3/apis/list.ts @@ -4,17 +4,17 @@ import { Amplify } from '@aws-amplify/core'; import { ListAllInput, - ListAllInputPath, - ListAllInputPrefix, + ListAllInputWithPath, + ListAllInputWithPrefix, ListAllOutput, - ListAllOutputPath, - ListAllOutputPrefix, + ListAllOutputWithPath, + ListAllOutputWithPrefix, ListPaginateInput, - ListPaginateInputPath, - ListPaginateInputPrefix, + ListPaginateInputWithPath, + ListPaginateInputWithPrefix, ListPaginateOutput, - ListPaginateOutputPath, - ListPaginateOutputPrefix, + ListPaginateOutputWithPath, + ListPaginateOutputWithPrefix, } from '../types'; import { list as listInternal } from './internal/list'; @@ -23,41 +23,41 @@ interface ListApi { /** * List files in pages with the given `path`. * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputPath` object. + * @param input - The `ListPaginateInputWithPath` object. * @returns A list of objects with path and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input: ListPaginateInputPath): Promise; + (input: ListPaginateInputWithPath): Promise; /** * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputPath` object. + * @param input - The `ListAllInputWithPath` object. * @returns A list of all objects with path and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input: ListAllInputPath): Promise; + (input: ListAllInputWithPath): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. * List files in pages with the given `prefix`. * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputPrefix` object. + * @param input - The `ListPaginateInputWithPrefix` object. * @returns A list of objects with key and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input?: ListPaginateInputPrefix): Promise; + (input?: ListPaginateInputWithPrefix): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputPrefix` object. + * @param input - The `ListAllInputWithPrefix` object. * @returns A list of all objects with key and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input?: ListAllInputPrefix): Promise; + (input?: ListAllInputWithPrefix): Promise; } export const list: ListApi = < diff --git a/packages/storage/src/providers/s3/apis/remove.ts b/packages/storage/src/providers/s3/apis/remove.ts index 3688dfb4041..f98d63c1735 100644 --- a/packages/storage/src/providers/s3/apis/remove.ts +++ b/packages/storage/src/providers/s3/apis/remove.ts @@ -5,11 +5,11 @@ import { Amplify } from '@aws-amplify/core'; import { RemoveInput, - RemoveInputKey, - RemoveInputPath, + RemoveInputWithKey, + RemoveInputWithPath, RemoveOutput, - RemoveOutputKey, - RemoveOutputPath, + RemoveOutputWithKey, + RemoveOutputWithPath, } from '../types'; import { remove as removeInternal } from './internal/remove'; @@ -17,25 +17,25 @@ import { remove as removeInternal } from './internal/remove'; interface RemoveApi { /** * Remove a file from your S3 bucket. - * @param input - The `RemoveInputPath` object. + * @param input - The `RemoveInputWithPath` object. * @return Output containing the removed object path. * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. * @throws validation: `StorageValidationErrorCode` - Validation errors thrown * when there is no path or path is empty or path has a leading slash. */ - (input: RemoveInputPath): Promise; + (input: RemoveInputWithPath): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. * * Remove a file from your S3 bucket. - * @param input - The `RemoveInputKey` object. + * @param input - The `RemoveInputWithKey` object. * @return Output containing the removed object key * @throws service: `S3Exception` - S3 service errors thrown while while removing the object * @throws validation: `StorageValidationErrorCode` - Validation errors thrown * when there is no key or its empty. */ - (input: RemoveInputKey): Promise; + (input: RemoveInputWithKey): Promise; } export const remove: RemoveApi = ( diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index 308bd05a89a..2b77c86794a 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -7,11 +7,11 @@ import { import { CopyInput, - CopyInputKey, - CopyInputPath, + CopyInputWithKey, + CopyInputWithPath, CopyOutput, - CopyOutputKey, - CopyOutputPath, + CopyOutputWithKey, + CopyOutputWithPath, } from '../../types'; import { copy as copyInternal } from '../internal/copy'; @@ -20,7 +20,7 @@ interface Copy { * Copy an object from a source to a destination object within the same bucket. * * @param contextSpec - The isolated server context. - * @param input - The CopyInputPath object. + * @param input - The CopyInputWithPath object. * @returns Output containing the destination object path. * @throws service: `S3Exception` - Thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Thrown when @@ -28,8 +28,8 @@ interface Copy { */ ( contextSpec: AmplifyServer.ContextSpec, - input: CopyInputPath, - ): Promise; + input: CopyInputWithPath, + ): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. @@ -38,7 +38,7 @@ interface Copy { * different accessLevel or identityId (if source object's accessLevel is 'protected'). * * @param contextSpec - The isolated server context. - * @param input - The CopyInputKey object. + * @param input - The CopyInputWithKey object. * @returns Output containing the destination object key. * @throws service: `S3Exception` - Thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Thrown when @@ -46,8 +46,8 @@ interface Copy { */ ( contextSpec: AmplifyServer.ContextSpec, - input: CopyInputKey, - ): Promise; + input: CopyInputWithKey, + ): Promise; } export const copy: Copy = ( diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts index a3c7e0c254e..a66050f864d 100644 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/server/getProperties.ts @@ -8,11 +8,11 @@ import { import { GetPropertiesInput, - GetPropertiesInputKey, - GetPropertiesInputPath, + GetPropertiesInputWithKey, + GetPropertiesInputWithPath, GetPropertiesOutput, - GetPropertiesOutputKey, - GetPropertiesOutputPath, + GetPropertiesOutputWithKey, + GetPropertiesOutputWithPath, } from '../../types'; import { getProperties as getPropertiesInternal } from '../internal/getProperties'; @@ -22,15 +22,15 @@ interface GetProperties { * the user metadata that was provided when uploading the file. * * @param contextSpec - The isolated server context. - * @param input - The `GetPropertiesInputPath` object. + * @param input - The `GetPropertiesInputWithPath` object. * @returns Requested object properties. * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ ( contextSpec: AmplifyServer.ContextSpec, - input: GetPropertiesInputPath, - ): Promise; + input: GetPropertiesInputWithPath, + ): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. @@ -39,15 +39,15 @@ interface GetProperties { * the user metadata that was provided when uploading the file. * * @param contextSpec - The isolated server context. - * @param input - The `GetPropertiesInputKey` object. + * @param input - The `GetPropertiesInputWithKey` object. * @returns Requested object properties. * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ ( contextSpec: AmplifyServer.ContextSpec, - input: GetPropertiesInputKey, - ): Promise; + input: GetPropertiesInputWithKey, + ): Promise; } export const getProperties: GetProperties = < diff --git a/packages/storage/src/providers/s3/apis/server/getUrl.ts b/packages/storage/src/providers/s3/apis/server/getUrl.ts index 7843b6323cf..c5cf6207e24 100644 --- a/packages/storage/src/providers/s3/apis/server/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/server/getUrl.ts @@ -8,8 +8,8 @@ import { import { GetUrlInput, - GetUrlInputKey, - GetUrlInputPath, + GetUrlInputWithKey, + GetUrlInputWithPath, GetUrlOutput, } from '../../types'; import { getUrl as getUrlInternal } from '../internal/getUrl'; @@ -25,7 +25,7 @@ interface GetUrl { * URL, and will throw `StorageError` if the object does not exist. * * @param contextSpec - The isolated server context. - * @param input - The `GetUrlInputPath` object. + * @param input - The `GetUrlInputWithPath` object. * @returns Presigned URL and timestamp when the URL MAY expire. * @throws service: `S3Exception` - thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Validation errors @@ -34,7 +34,7 @@ interface GetUrl { */ ( contextSpec: AmplifyServer.ContextSpec, - input: GetUrlInputPath, + input: GetUrlInputWithPath, ): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. @@ -49,7 +49,7 @@ interface GetUrl { * URL, and will throw `StorageError` if the object does not exist. * * @param contextSpec - The isolated server context. - * @param input - The `GetUrlInputKey` object. + * @param input - The `GetUrlInputWithKey` object. * @returns Presigned URL and timestamp when the URL MAY expire. * @throws service: `S3Exception` - thrown when checking for existence of the object * @throws validation: `StorageValidationErrorCode` - Validation errors @@ -58,7 +58,7 @@ interface GetUrl { */ ( contextSpec: AmplifyServer.ContextSpec, - input: GetUrlInputKey, + input: GetUrlInputWithKey, ): Promise; } export const getUrl: GetUrl = async ( diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts index bc10e4cf41b..ea1250a7e3f 100644 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ b/packages/storage/src/providers/s3/apis/server/list.ts @@ -7,17 +7,17 @@ import { import { ListAllInput, - ListAllInputPath, - ListAllInputPrefix, + ListAllInputWithPath, + ListAllInputWithPrefix, ListAllOutput, - ListAllOutputPath, - ListAllOutputPrefix, + ListAllOutputWithPath, + ListAllOutputWithPrefix, ListPaginateInput, - ListPaginateInputPath, - ListPaginateInputPrefix, + ListPaginateInputWithPath, + ListPaginateInputWithPrefix, ListPaginateOutput, - ListPaginateOutputPath, - ListPaginateOutputPrefix, + ListPaginateOutputWithPath, + ListPaginateOutputWithPrefix, } from '../../types'; import { list as listInternal } from '../internal/list'; @@ -25,7 +25,7 @@ interface ListApi { /** * List files in pages with the given `path`. * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputPath` object. + * @param input - The `ListPaginateInputWithPath` object. * @param contextSpec - The context spec used to get the Amplify server context. * @returns A list of objects with path and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket @@ -33,11 +33,11 @@ interface ListApi { */ ( contextSpec: AmplifyServer.ContextSpec, - input: ListPaginateInputPath, - ): Promise; + input: ListPaginateInputWithPath, + ): Promise; /** * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputPath` object. + * @param input - The `ListAllInputWithPath` object. * @param contextSpec - The context spec used to get the Amplify server context. * @returns A list of all objects with path and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket @@ -45,35 +45,35 @@ interface ListApi { */ ( contextSpec: AmplifyServer.ContextSpec, - input: ListAllInputPath, - ): Promise; + input: ListAllInputWithPath, + ): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. * List files in pages with the given `prefix`. * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputPrefix` object. + * @param input - The `ListPaginateInputWithPrefix` object. * @returns A list of objects with key and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ ( contextSpec: AmplifyServer.ContextSpec, - input?: ListPaginateInputPrefix, - ): Promise; + input?: ListPaginateInputWithPrefix, + ): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputPrefix` object. + * @param input - The `ListAllInputWithPrefix` object. * @returns A list of all objects with key and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ ( contextSpec: AmplifyServer.ContextSpec, - input?: ListAllInputPrefix, - ): Promise; + input?: ListAllInputWithPrefix, + ): Promise; } export const list: ListApi = < diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts index 5ef81d0f526..c27e2831020 100644 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ b/packages/storage/src/providers/s3/apis/server/remove.ts @@ -8,18 +8,18 @@ import { import { RemoveInput, - RemoveInputKey, - RemoveInputPath, + RemoveInputWithKey, + RemoveInputWithPath, RemoveOutput, - RemoveOutputKey, - RemoveOutputPath, + RemoveOutputWithKey, + RemoveOutputWithPath, } from '../../types'; import { remove as removeInternal } from '../internal/remove'; interface RemoveApi { /** * Remove a file from your S3 bucket. - * @param input - The `RemoveInputPath` object. + * @param input - The `RemoveInputWithPath` object. * @param contextSpec - The context spec used to get the Amplify server context. * @return Output containing the removed object path. * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. @@ -28,14 +28,14 @@ interface RemoveApi { */ ( contextSpec: AmplifyServer.ContextSpec, - input: RemoveInputPath, - ): Promise; + input: RemoveInputWithPath, + ): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. * * Remove a file from your S3 bucket. - * @param input - The `RemoveInputKey` object. + * @param input - The `RemoveInputWithKey` object. * @param contextSpec - The context spec used to get the Amplify server context. * @return Output containing the removed object key * @throws service: `S3Exception` - S3 service errors thrown while while removing the object @@ -44,8 +44,8 @@ interface RemoveApi { */ ( contextSpec: AmplifyServer.ContextSpec, - input: RemoveInputKey, - ): Promise; + input: RemoveInputWithKey, + ): Promise; } export const remove: RemoveApi = ( diff --git a/packages/storage/src/providers/s3/apis/uploadData/index.ts b/packages/storage/src/providers/s3/apis/uploadData/index.ts index a1de4b1f40f..ae4bbeefa65 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/index.ts @@ -3,11 +3,11 @@ import { UploadDataInput, - UploadDataInputKey, - UploadDataInputPath, + UploadDataInputWithKey, + UploadDataInputWithPath, UploadDataOutput, - UploadDataOutputKey, - UploadDataOutputPath, + UploadDataOutputWithKey, + UploadDataOutputWithPath, } from '../../types'; import { createUploadTask } from '../../utils'; import { assertValidationError } from '../../../../errors/utils/assertValidationError'; @@ -30,7 +30,7 @@ interface UploadData { * @throws Service: `S3Exception` thrown when checking for existence of the object. * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. * - * @param input - A `UploadDataInputPath` object. + * @param input - A `UploadDataInputWithPath` object. * * @returns A cancelable and resumable task exposing result promise from `result` * property. @@ -70,7 +70,7 @@ interface UploadData { * await uploadTask.result; * ``` */ - (input: UploadDataInputPath): UploadDataOutputPath; + (input: UploadDataInputWithPath): UploadDataOutputWithPath; /** * Upload data to the specified S3 object key. By default uses single PUT operation to upload if the payload is less than 5MB. @@ -86,7 +86,7 @@ interface UploadData { * @throws Service: `S3Exception` thrown when checking for existence of the object. * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. * - * @param input - A UploadDataInputKey object. + * @param input - A UploadDataInputWithKey object. * * @returns A cancelable and resumable task exposing result promise from the `result` property. * @@ -125,7 +125,7 @@ interface UploadData { * await uploadTask.result; * ``` */ - (input: UploadDataInputKey): UploadDataOutputKey; + (input: UploadDataInputWithKey): UploadDataOutputWithKey; } export const uploadData: UploadData = ( 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 3ccfef1b9dd..6984498f828 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts @@ -9,13 +9,16 @@ import { resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../../utils'; -import { ItemKey, ItemPath } from '../../../types/outputs'; +import { ItemWithKey, ItemWithPath } from '../../../types/outputs'; import { DEFAULT_ACCESS_LEVEL, DEFAULT_QUEUE_SIZE, STORAGE_INPUT_KEY, } from '../../../utils/constants'; -import { ResolvedS3Config, UploadDataOptionsKey } from '../../../types/options'; +import { + ResolvedS3Config, + UploadDataOptionsWithKey, +} from '../../../types/options'; import { StorageError } from '../../../../../errors/StorageError'; import { CanceledError } from '../../../../../errors/CanceledError'; import { @@ -43,7 +46,9 @@ export const getMultipartUploadHandlers = ( uploadDataInput: UploadDataInput, size?: number, ) => { - let resolveCallback: ((value: ItemKey | ItemPath) => void) | undefined; + let resolveCallback: + | ((value: ItemWithKey | ItemWithPath) => void) + | undefined; let rejectCallback: ((reason?: any) => void) | undefined; let inProgressUpload: | { @@ -64,7 +69,7 @@ export const getMultipartUploadHandlers = ( // This should be replaced by a special abort reason. However,the support of this API is lagged behind. let isAbortSignalFromPause = false; - const startUpload = async (): Promise => { + const startUpload = async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const resolvedS3Options = await resolveS3ConfigAndInput( Amplify, @@ -94,7 +99,7 @@ export const getMultipartUploadHandlers = ( // Resolve "key" specific options if (inputType === STORAGE_INPUT_KEY) { - const accessLevel = (uploadDataOptions as UploadDataOptionsKey) + const accessLevel = (uploadDataOptions as UploadDataOptionsWithKey) ?.accessLevel; resolvedKeyPrefix = resolvedS3Options.keyPrefix; @@ -233,7 +238,7 @@ export const getMultipartUploadHandlers = ( }); const multipartUploadJob = () => - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { resolveCallback = resolve; rejectCallback = reject; startUploadWithResumability(); diff --git a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts index 0ded295f83e..f9d7bfefe3e 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts @@ -10,7 +10,7 @@ import { resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../utils'; -import { ItemKey, ItemPath } from '../../types/outputs'; +import { ItemWithKey, ItemWithPath } from '../../types/outputs'; import { putObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { STORAGE_INPUT_KEY } from '../../utils/constants'; @@ -26,7 +26,7 @@ export const putObjectJob = abortSignal: AbortSignal, totalLength?: number, ) => - async (): Promise => { + async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const { bucket, keyPrefix, s3Config, isObjectLockEnabled, identityId } = await resolveS3ConfigAndInput(Amplify, uploadDataOptions); diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 59ce5f8e966..70b1ba46c63 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -2,72 +2,72 @@ // SPDX-License-Identifier: Apache-2.0 export { - GetUrlOptionsKey, - GetUrlOptionsPath, - UploadDataOptionsPath, - UploadDataOptionsKey, - GetPropertiesOptionsKey, - GetPropertiesOptionsPath, - ListAllOptionsPrefix, - ListPaginateOptionsPrefix, - ListAllOptionsPath, - ListPaginateOptionsPath, + GetUrlOptionsWithKey, + GetUrlOptionsWithPath, + UploadDataOptionsWithPath, + UploadDataOptionsWithKey, + GetPropertiesOptionsWithKey, + GetPropertiesOptionsWithPath, + ListAllOptionsWithPrefix, + ListPaginateOptionsWithPrefix, + ListAllOptionsWithPath, + ListPaginateOptionsWithPath, RemoveOptions, - DownloadDataOptionsPath, - DownloadDataOptionsKey, - CopyDestinationOptionsKey, - CopySourceOptionsKey, + DownloadDataOptionsWithPath, + DownloadDataOptionsWithKey, + CopyDestinationOptionsWithKey, + CopySourceOptionsWithKey, } from './options'; export { DownloadDataOutput, - DownloadDataOutputKey, - DownloadDataOutputPath, + DownloadDataOutputWithKey, + DownloadDataOutputWithPath, GetUrlOutput, UploadDataOutput, - UploadDataOutputKey, - UploadDataOutputPath, - ListOutputItemKey, - ListOutputItemPath, + UploadDataOutputWithKey, + UploadDataOutputWithPath, + ListOutputItemWithKey, + ListOutputItemWithPath, ListAllOutput, ListPaginateOutput, - ListAllOutputPrefix, - ListAllOutputPath, - ListPaginateOutputPath, - ListPaginateOutputPrefix, + ListAllOutputWithPrefix, + ListAllOutputWithPath, + ListPaginateOutputWithPath, + ListPaginateOutputWithPrefix, GetPropertiesOutput, - GetPropertiesOutputKey, - GetPropertiesOutputPath, + GetPropertiesOutputWithKey, + GetPropertiesOutputWithPath, CopyOutput, - CopyOutputKey, - CopyOutputPath, + CopyOutputWithKey, + CopyOutputWithPath, RemoveOutput, - RemoveOutputKey, - RemoveOutputPath, + RemoveOutputWithKey, + RemoveOutputWithPath, } from './outputs'; export { CopyInput, - CopyInputKey, - CopyInputPath, + CopyInputWithKey, + CopyInputWithPath, GetPropertiesInput, - GetPropertiesInputKey, - GetPropertiesInputPath, + GetPropertiesInputWithKey, + GetPropertiesInputWithPath, GetUrlInput, - GetUrlInputKey, - GetUrlInputPath, - RemoveInputKey, - RemoveInputPath, + GetUrlInputWithKey, + GetUrlInputWithPath, + RemoveInputWithKey, + RemoveInputWithPath, RemoveInput, DownloadDataInput, - DownloadDataInputKey, - DownloadDataInputPath, + DownloadDataInputWithKey, + DownloadDataInputWithPath, UploadDataInput, - UploadDataInputPath, - UploadDataInputKey, + UploadDataInputWithPath, + UploadDataInputWithKey, ListAllInput, ListPaginateInput, - ListAllInputPath, - ListPaginateInputPath, - ListAllInputPrefix, - ListPaginateInputPrefix, + ListAllInputWithPath, + ListPaginateInputWithPath, + ListAllInputWithPrefix, + ListPaginateInputWithPrefix, } from './inputs'; export { S3Exception } from './errors'; diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index dd956fc9747..fa16e636b49 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -4,149 +4,155 @@ import { StrictUnion } from '@aws-amplify/core/internals/utils'; import { - StorageCopyInputKey, - StorageCopyInputPath, - StorageDownloadDataInputKey, - StorageDownloadDataInputPath, - StorageGetPropertiesInputKey, - StorageGetPropertiesInputPath, - StorageGetUrlInputKey, - StorageGetUrlInputPath, - StorageListInputPath, - StorageListInputPrefix, - StorageRemoveInputKey, - StorageRemoveInputPath, - StorageUploadDataInputKey, - StorageUploadDataInputPath, + StorageCopyInputWithKey, + StorageCopyInputWithPath, + StorageDownloadDataInputWithKey, + StorageDownloadDataInputWithPath, + StorageGetPropertiesInputWithKey, + StorageGetPropertiesInputWithPath, + StorageGetUrlInputWithKey, + StorageGetUrlInputWithPath, + StorageListInputWithPath, + StorageListInputWithPrefix, + StorageRemoveInputWithKey, + StorageRemoveInputWithPath, + StorageUploadDataInputWithKey, + StorageUploadDataInputWithPath, } from '../../../types'; import { - CopyDestinationOptionsKey, - CopySourceOptionsKey, - DownloadDataOptionsKey, - DownloadDataOptionsPath, - GetPropertiesOptionsKey, - GetPropertiesOptionsPath, - GetUrlOptionsKey, - GetUrlOptionsPath, - ListAllOptionsPath, - ListAllOptionsPrefix, - ListPaginateOptionsPath, - ListPaginateOptionsPrefix, + CopyDestinationOptionsWithKey, + CopySourceOptionsWithKey, + DownloadDataOptionsWithKey, + DownloadDataOptionsWithPath, + GetPropertiesOptionsWithKey, + GetPropertiesOptionsWithPath, + GetUrlOptionsWithKey, + GetUrlOptionsWithPath, + ListAllOptionsWithPath, + ListAllOptionsWithPrefix, + ListPaginateOptionsWithPath, + ListPaginateOptionsWithPrefix, RemoveOptions, - UploadDataOptionsKey, - UploadDataOptionsPath, + UploadDataOptionsWithKey, + UploadDataOptionsWithPath, } from '../types'; // TODO: support use accelerate endpoint option /** * Input type for S3 copy API. */ -export type CopyInput = CopyInputKey | CopyInputPath; +export type CopyInput = CopyInputWithKey | CopyInputWithPath; -/** @deprecated Use {@link CopyInputPath} instead. */ -export type CopyInputKey = StorageCopyInputKey< - CopySourceOptionsKey, - CopyDestinationOptionsKey +/** @deprecated Use {@link CopyInputWithPath} instead. */ +export type CopyInputWithKey = StorageCopyInputWithKey< + CopySourceOptionsWithKey, + CopyDestinationOptionsWithKey >; -export type CopyInputPath = StorageCopyInputPath; +export type CopyInputWithPath = StorageCopyInputWithPath; /** * Input type for S3 getProperties API. */ export type GetPropertiesInput = StrictUnion< - GetPropertiesInputKey | GetPropertiesInputPath + GetPropertiesInputWithKey | GetPropertiesInputWithPath >; -/** @deprecated Use {@link GetPropertiesInputPath} instead. */ -export type GetPropertiesInputKey = - StorageGetPropertiesInputKey; -export type GetPropertiesInputPath = - StorageGetPropertiesInputPath; +/** @deprecated Use {@link GetPropertiesInputWithPath} instead. */ +export type GetPropertiesInputWithKey = + StorageGetPropertiesInputWithKey; +export type GetPropertiesInputWithPath = + StorageGetPropertiesInputWithPath; /** * Input type for S3 getUrl API. */ -export type GetUrlInput = StrictUnion; +export type GetUrlInput = StrictUnion; -/** @deprecated Use {@link GetUrlInputPath} instead. */ -export type GetUrlInputKey = StorageGetUrlInputKey; -export type GetUrlInputPath = StorageGetUrlInputPath; +/** @deprecated Use {@link GetUrlInputWithPath} instead. */ +export type GetUrlInputWithKey = + StorageGetUrlInputWithKey; +export type GetUrlInputWithPath = + StorageGetUrlInputWithPath; /** * Input type for S3 list API. Lists all bucket objects. */ -export type ListAllInput = StrictUnion; +export type ListAllInput = StrictUnion< + ListAllInputWithPath | ListAllInputWithPrefix +>; /** * Input type for S3 list API. Lists bucket objects with pagination. */ export type ListPaginateInput = StrictUnion< - ListPaginateInputPath | ListPaginateInputPrefix + ListPaginateInputWithPath | ListPaginateInputWithPrefix >; /** * Input type for S3 list API. Lists all bucket objects. */ -export type ListAllInputPath = StorageListInputPath; +export type ListAllInputWithPath = + StorageListInputWithPath; /** * Input type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateInputPath = - StorageListInputPath; +export type ListPaginateInputWithPath = + StorageListInputWithPath; /** - * @deprecated Use {@link ListAllInputPath} instead. + * @deprecated Use {@link ListAllInputWithPath} instead. * Input type for S3 list API. Lists all bucket objects. */ -export type ListAllInputPrefix = StorageListInputPrefix; +export type ListAllInputWithPrefix = + StorageListInputWithPrefix; /** - * @deprecated Use {@link ListPaginateInputPath} instead. + * @deprecated Use {@link ListPaginateInputWithPath} instead. * Input type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateInputPrefix = - StorageListInputPrefix; +export type ListPaginateInputWithPrefix = + StorageListInputWithPrefix; /** - * @deprecated Use {@link RemoveInputPath} instead. + * @deprecated Use {@link RemoveInputWithPath} instead. * Input type with key for S3 remove API. */ -export type RemoveInputKey = StorageRemoveInputKey; +export type RemoveInputWithKey = StorageRemoveInputWithKey; /** * Input type with path for S3 remove API. */ -export type RemoveInputPath = StorageRemoveInputPath< +export type RemoveInputWithPath = StorageRemoveInputWithPath< Omit >; /** * Input type for S3 remove API. */ -export type RemoveInput = StrictUnion; +export type RemoveInput = StrictUnion; /** * Input type for S3 downloadData API. */ export type DownloadDataInput = StrictUnion< - DownloadDataInputKey | DownloadDataInputPath + DownloadDataInputWithKey | DownloadDataInputWithPath >; -/** @deprecated Use {@link DownloadDataInputPath} instead. */ -export type DownloadDataInputKey = - StorageDownloadDataInputKey; -export type DownloadDataInputPath = - StorageDownloadDataInputPath; +/** @deprecated Use {@link DownloadDataInputWithPath} instead. */ +export type DownloadDataInputWithKey = + StorageDownloadDataInputWithKey; +export type DownloadDataInputWithPath = + StorageDownloadDataInputWithPath; /** * Input type for S3 uploadData API. */ export type UploadDataInput = StrictUnion< - UploadDataInputKey | UploadDataInputPath + UploadDataInputWithKey | UploadDataInputWithPath >; -/** @deprecated Use {@link UploadDataInputPath} instead. */ -export type UploadDataInputKey = - StorageUploadDataInputKey; -export type UploadDataInputPath = - StorageUploadDataInputPath; +/** @deprecated Use {@link UploadDataInputWithPath} instead. */ +export type UploadDataInputWithKey = + StorageUploadDataInputWithKey; +export type UploadDataInputWithPath = + StorageUploadDataInputWithPath; diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 94456a04e86..4d0af341f52 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -57,9 +57,9 @@ interface TransferOptions { /** * Input options type for S3 getProperties API. */ -/** @deprecated Use {@link GetPropertiesOptionsPath} instead. */ -export type GetPropertiesOptionsKey = ReadOptions & CommonOptions; -export type GetPropertiesOptionsPath = CommonOptions; +/** @deprecated Use {@link GetPropertiesOptionsWithPath} instead. */ +export type GetPropertiesOptionsWithKey = ReadOptions & CommonOptions; +export type GetPropertiesOptionsWithPath = CommonOptions; /** * Input options type for S3 getProperties API. @@ -67,31 +67,34 @@ export type GetPropertiesOptionsPath = CommonOptions; export type RemoveOptions = WriteOptions & CommonOptions; /** - * @deprecated Use {@link ListAllOptionsPath} instead. + * @deprecated Use {@link ListAllOptionsWithPath} instead. * Input options type with prefix for S3 list all API. */ -export type ListAllOptionsPrefix = StorageListAllOptions & +export type ListAllOptionsWithPrefix = StorageListAllOptions & ReadOptions & CommonOptions; /** - * @deprecated Use {@link ListPaginateOptionsPath} instead. + * @deprecated Use {@link ListPaginateOptionsWithPath} instead. * Input options type with prefix for S3 list API to paginate items. */ -export type ListPaginateOptionsPrefix = StorageListPaginateOptions & +export type ListPaginateOptionsWithPrefix = StorageListPaginateOptions & ReadOptions & CommonOptions; /** * Input options type with path for S3 list all API. */ -export type ListAllOptionsPath = Omit & +export type ListAllOptionsWithPath = Omit< + StorageListAllOptions, + 'accessLevel' +> & CommonOptions; /** * Input options type with path for S3 list API to paginate items. */ -export type ListPaginateOptionsPath = Omit< +export type ListPaginateOptionsWithPath = Omit< StorageListPaginateOptions, 'accessLevel' > & @@ -113,9 +116,9 @@ export type GetUrlOptions = CommonOptions & { expiresIn?: number; }; -/** @deprecated Use {@link GetUrlOptionsPath} instead. */ -export type GetUrlOptionsKey = ReadOptions & GetUrlOptions; -export type GetUrlOptionsPath = GetUrlOptions; +/** @deprecated Use {@link GetUrlOptionsWithPath} instead. */ +export type GetUrlOptionsWithKey = ReadOptions & GetUrlOptions; +export type GetUrlOptionsWithPath = GetUrlOptions; /** * Input options type for S3 downloadData API. @@ -124,9 +127,9 @@ export type DownloadDataOptions = CommonOptions & TransferOptions & BytesRangeOptions; -/** @deprecated Use {@link DownloadDataOptionsPath} instead. */ -export type DownloadDataOptionsKey = ReadOptions & DownloadDataOptions; -export type DownloadDataOptionsPath = DownloadDataOptions; +/** @deprecated Use {@link DownloadDataOptionsWithPath} instead. */ +export type DownloadDataOptionsWithKey = ReadOptions & DownloadDataOptions; +export type DownloadDataOptionsWithPath = DownloadDataOptions; export type UploadDataOptions = CommonOptions & TransferOptions & { @@ -152,18 +155,18 @@ export type UploadDataOptions = CommonOptions & metadata?: Record; }; -/** @deprecated Use {@link UploadDataOptionsPath} instead. */ -export type UploadDataOptionsKey = WriteOptions & UploadDataOptions; -export type UploadDataOptionsPath = UploadDataOptions; +/** @deprecated Use {@link UploadDataOptionsWithPath} instead. */ +export type UploadDataOptionsWithKey = WriteOptions & UploadDataOptions; +export type UploadDataOptionsWithPath = UploadDataOptions; /** @deprecated This may be removed in the next major version. */ -export type CopySourceOptionsKey = ReadOptions & { +export type CopySourceOptionsWithKey = ReadOptions & { /** @deprecated This may be removed in the next major version. */ key: string; }; /** @deprecated This may be removed in the next major version. */ -export type CopyDestinationOptionsKey = WriteOptions & { +export type CopyDestinationOptionsWithKey = WriteOptions & { /** @deprecated This may be removed in the next major version. */ key: string; }; diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index 5e624077854..5c7238420c9 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -7,8 +7,8 @@ import { DownloadTask, StorageDownloadDataOutput, StorageGetUrlOutput, - StorageItemKey, - StorageItemPath, + StorageItemWithKey, + StorageItemWithPath, StorageListOutput, UploadTask, } from '../../../types'; @@ -28,129 +28,137 @@ export interface ItemBase { } /** - * @deprecated Use {@link ListOutputItemPath} instead. + * @deprecated Use {@link ListOutputItemWithPath} instead. * type for S3 list item with key. */ -export type ListOutputItemKey = Omit; +export type ListOutputItemWithKey = Omit; /** * type for S3 list item with path. */ -export type ListOutputItemPath = Omit; +export type ListOutputItemWithPath = Omit; /** - * @deprecated Use {@link ItemPath} instead. + * @deprecated Use {@link ItemWithPath} instead. */ -export type ItemKey = ItemBase & StorageItemKey; +export type ItemWithKey = ItemBase & StorageItemWithKey; /** * type for S3 list item with path. */ -export type ItemPath = ItemBase & StorageItemPath; +export type ItemWithPath = ItemBase & StorageItemWithPath; /** * type for S3 list item. */ -export type ListOutputItem = Omit; +export type ListOutputItem = Omit; -/** @deprecated Use {@link DownloadDataOutputPath} instead. */ -export type DownloadDataOutputKey = DownloadTask< - StorageDownloadDataOutput +/** @deprecated Use {@link DownloadDataOutputWithPath} instead. */ +export type DownloadDataOutputWithKey = DownloadTask< + StorageDownloadDataOutput >; -export type DownloadDataOutputPath = DownloadTask< - StorageDownloadDataOutput +export type DownloadDataOutputWithPath = DownloadTask< + StorageDownloadDataOutput >; /** * Output type for S3 downloadData API. */ -export type DownloadDataOutput = DownloadDataOutputKey | DownloadDataOutputPath; +export type DownloadDataOutput = + | DownloadDataOutputWithKey + | DownloadDataOutputWithPath; /** * Output type for S3 getUrl API. */ export type GetUrlOutput = StorageGetUrlOutput; -/** @deprecated Use {@link UploadDataOutputPath} instead. */ -export type UploadDataOutputKey = UploadTask; -export type UploadDataOutputPath = UploadTask; +/** @deprecated Use {@link UploadDataOutputWithPath} instead. */ +export type UploadDataOutputWithKey = UploadTask; +export type UploadDataOutputWithPath = UploadTask; /** * Output type for S3 uploadData API. */ -export type UploadDataOutput = UploadDataOutputKey | UploadDataOutputPath; +export type UploadDataOutput = + | UploadDataOutputWithKey + | UploadDataOutputWithPath; -/** @deprecated Use {@link GetPropertiesOutputPath} instead. */ -export type GetPropertiesOutputKey = ItemKey; -export type GetPropertiesOutputPath = ItemPath; +/** @deprecated Use {@link GetPropertiesOutputWithPath} instead. */ +export type GetPropertiesOutputWithKey = ItemWithKey; +export type GetPropertiesOutputWithPath = ItemWithPath; /** * Output type for S3 getProperties API. */ export type GetPropertiesOutput = - | GetPropertiesOutputKey - | GetPropertiesOutputPath; + | GetPropertiesOutputWithKey + | GetPropertiesOutputWithPath; /** * Output type for S3 list API. Lists all bucket objects. */ export type ListAllOutput = StrictUnion< - ListAllOutputPath | ListAllOutputPrefix + ListAllOutputWithPath | ListAllOutputWithPrefix >; /** * Output type for S3 list API. Lists bucket objects with pagination. */ export type ListPaginateOutput = StrictUnion< - ListPaginateOutputPath | ListPaginateOutputPrefix + ListPaginateOutputWithPath | ListPaginateOutputWithPrefix >; /** - * @deprecated Use {@link ListAllOutputPath} instead. + * @deprecated Use {@link ListAllOutputWithPath} instead. * Output type for S3 list API. Lists all bucket objects. */ -export type ListAllOutputPrefix = StorageListOutput; +export type ListAllOutputWithPrefix = StorageListOutput; /** * Output type for S3 list API. Lists all bucket objects. */ -export type ListAllOutputPath = StorageListOutput; +export type ListAllOutputWithPath = StorageListOutput; /** - * @deprecated Use {@link ListPaginateOutputPath} instead. + * @deprecated Use {@link ListPaginateOutputWithPath} instead. * Output type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateOutputPrefix = StorageListOutput & { - nextToken?: string; -}; +export type ListPaginateOutputWithPrefix = + StorageListOutput & { + nextToken?: string; + }; /** * Output type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateOutputPath = StorageListOutput & { - nextToken?: string; -}; +export type ListPaginateOutputWithPath = + StorageListOutput & { + nextToken?: string; + }; /** - * @deprecated Use {@link CopyOutputPath} instead. + * @deprecated Use {@link CopyOutputWithPath} instead. */ -export type CopyOutputKey = Pick; -export type CopyOutputPath = Pick; +export type CopyOutputWithKey = Pick; +export type CopyOutputWithPath = Pick; -export type CopyOutput = StrictUnion; +export type CopyOutput = StrictUnion; /** * Output type for S3 remove API. */ -export type RemoveOutput = StrictUnion; +export type RemoveOutput = StrictUnion< + RemoveOutputWithKey | RemoveOutputWithPath +>; /** - * @deprecated Use {@link RemoveOutputPath} instead. + * @deprecated Use {@link RemoveOutputWithPath} instead. * Output helper type with key for S3 remove API. */ -export type RemoveOutputKey = Pick; +export type RemoveOutputWithKey = Pick; /** * Output helper type with path for S3 remove API. */ -export type RemoveOutputPath = Pick; +export type RemoveOutputWithPath = Pick; diff --git a/packages/storage/src/providers/s3/utils/isInputWithPath.ts b/packages/storage/src/providers/s3/utils/isInputWithPath.ts index 5c31f924b2c..b0943154351 100644 --- a/packages/storage/src/providers/s3/utils/isInputWithPath.ts +++ b/packages/storage/src/providers/s3/utils/isInputWithPath.ts @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { - StorageOperationInputPath, StorageOperationInputType, + StorageOperationInputWithPath, } from '../../../types/inputs'; export const isInputWithPath = ( input: StorageOperationInputType, -): input is StorageOperationInputPath => { +): input is StorageOperationInputWithPath => { return input.path !== undefined; }; diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts index 04a08c3aa97..da1068af010 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInputWithPrefix.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { - StorageOperationInputPath, + StorageOperationInputWithPath, StorageOperationInputWithPrefixPath, } from '../../../types/inputs'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; @@ -13,7 +13,7 @@ import { STORAGE_INPUT_PATH, STORAGE_INPUT_PREFIX } from './constants'; // Local assertion function with StorageOperationInputWithPrefixPath as Input const _isInputWithPath = ( input: StorageOperationInputWithPrefixPath, -): input is StorageOperationInputPath => { +): input is StorageOperationInputWithPath => { return input.path !== undefined; }; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index aa4210cb72c..0f1a084f461 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -9,20 +9,20 @@ export { } from './common'; export { StorageOperationInput, - StorageGetPropertiesInputKey, - StorageGetPropertiesInputPath, - StorageListInputPrefix, - StorageListInputPath, - StorageRemoveInputPath, - StorageRemoveInputKey, - StorageDownloadDataInputKey, - StorageDownloadDataInputPath, - StorageUploadDataInputKey, - StorageUploadDataInputPath, - StorageCopyInputKey, - StorageCopyInputPath, - StorageGetUrlInputKey, - StorageGetUrlInputPath, + StorageGetPropertiesInputWithKey, + StorageGetPropertiesInputWithPath, + StorageListInputWithPrefix, + StorageListInputWithPath, + StorageRemoveInputWithPath, + StorageRemoveInputWithKey, + StorageDownloadDataInputWithKey, + StorageDownloadDataInputWithPath, + StorageUploadDataInputWithKey, + StorageUploadDataInputWithPath, + StorageCopyInputWithKey, + StorageCopyInputWithPath, + StorageGetUrlInputWithKey, + StorageGetUrlInputWithPath, StorageUploadDataPayload, } from './inputs'; export { @@ -33,8 +33,8 @@ export { } from './options'; export { StorageItem, - StorageItemKey, - StorageItemPath, + StorageItemWithKey, + StorageItemWithPath, StorageListOutput, StorageDownloadDataOutput, StorageGetUrlOutput, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index aa3447ec0d9..8a2cc0a0165 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -12,22 +12,22 @@ import { // TODO: rename to StorageOperationInput once the other type with // the same named is removed export type StorageOperationInputType = StrictUnion< - StorageOperationInputKey | StorageOperationInputPath + StorageOperationInputWithKey | StorageOperationInputWithPath >; export type StorageOperationInputWithPrefixPath = StrictUnion< - StorageOperationInputPath | StorageOperationInputPrefix + StorageOperationInputWithPath | StorageOperationInputWithPrefix >; -/** @deprecated Use {@link StorageOperationInputPath} instead. */ -export interface StorageOperationInputKey { +/** @deprecated Use {@link StorageOperationInputWithPath} instead. */ +export interface StorageOperationInputWithKey { /** @deprecated Use `path` instead. */ key: string; } -export interface StorageOperationInputPath { +export interface StorageOperationInputWithPath { path: string | (({ identityId }: { identityId?: string }) => string); } -/** @deprecated Use {@link StorageOperationInputPath} instead. */ -export interface StorageOperationInputPrefix { +/** @deprecated Use {@link StorageOperationInputWithPath} instead. */ +export interface StorageOperationInputWithPrefix { /** @deprecated Use `path` instead. */ prefix?: string; } @@ -36,12 +36,12 @@ export interface StorageOperationOptionsInput { options?: Options; } -/** @deprecated Use {@link StorageDownloadDataInputPath} instead. */ -export type StorageDownloadDataInputKey = - StorageOperationInputKey & StorageOperationOptionsInput; +/** @deprecated Use {@link StorageDownloadDataInputWithPath} instead. */ +export type StorageDownloadDataInputWithKey = + StorageOperationInputWithKey & StorageOperationOptionsInput; -export type StorageDownloadDataInputPath = StorageOperationInputPath & - StorageOperationOptionsInput; +export type StorageDownloadDataInputWithPath = + StorageOperationInputWithPath & StorageOperationOptionsInput; // TODO: This needs to be removed after refactor of all storage APIs export interface StorageOperationInput { @@ -49,47 +49,48 @@ export interface StorageOperationInput { options?: Options; } -/** @deprecated Use {@link StorageGetPropertiesInputPath} instead. */ -export type StorageGetPropertiesInputKey = - StorageOperationInputKey & StorageOperationInput; +/** @deprecated Use {@link StorageGetPropertiesInputWithPath} instead. */ +export type StorageGetPropertiesInputWithKey = + StorageOperationInputWithKey & StorageOperationInput; -export type StorageGetPropertiesInputPath = StorageOperationInputPath & - StorageOperationOptionsInput; +export type StorageGetPropertiesInputWithPath = + StorageOperationInputWithPath & StorageOperationOptionsInput; -export type StorageRemoveInputKey = StorageOperationInputKey & +export type StorageRemoveInputWithKey = StorageOperationInputWithKey & StorageOperationOptionsInput; -export type StorageRemoveInputPath = StorageOperationInputPath & - StorageOperationOptionsInput; +export type StorageRemoveInputWithPath = + StorageOperationInputWithPath & StorageOperationOptionsInput; -/** @deprecated Use {@link StorageListInputPath} instead. */ -export type StorageListInputPrefix< +/** @deprecated Use {@link StorageListInputWithPath} instead. */ +export type StorageListInputWithPrefix< Options extends StorageListAllOptions | StorageListPaginateOptions, -> = StorageOperationInputPrefix & StorageOperationOptionsInput; +> = StorageOperationInputWithPrefix & StorageOperationOptionsInput; -export type StorageListInputPath< +export type StorageListInputWithPath< Options extends StorageListAllOptions | StorageListPaginateOptions, -> = StorageOperationInputPath & StorageOperationOptionsInput; +> = StorageOperationInputWithPath & StorageOperationOptionsInput; -/** @deprecated Use {@link StorageGetUrlInputPath} instead. */ -export type StorageGetUrlInputKey = - StorageOperationInputKey & StorageOperationInput; +/** @deprecated Use {@link StorageGetUrlInputWithPath} instead. */ +export type StorageGetUrlInputWithKey = + StorageOperationInputWithKey & StorageOperationInput; -export type StorageGetUrlInputPath = StorageOperationInputPath & - StorageOperationOptionsInput; +export type StorageGetUrlInputWithPath = + StorageOperationInputWithPath & StorageOperationOptionsInput; -/** @deprecated Use {@link StorageUploadDataInputPath} instead. */ -export type StorageUploadDataInputKey = - StorageOperationInputKey & +/** @deprecated Use {@link StorageUploadDataInputWithPath} instead. */ +export type StorageUploadDataInputWithKey = + StorageOperationInputWithKey & StorageOperationOptionsInput & StorageUploadDataInputPayload; -export type StorageUploadDataInputPath = StorageOperationInputPath & - StorageOperationOptionsInput & - StorageUploadDataInputPayload; +export type StorageUploadDataInputWithPath = + StorageOperationInputWithPath & + StorageOperationOptionsInput & + StorageUploadDataInputPayload; -/** @deprecated Use {@link StorageCopyInputPath} instead. */ -export interface StorageCopyInputKey< +/** @deprecated Use {@link StorageCopyInputWithPath} instead. */ +export interface StorageCopyInputWithKey< SourceOptions extends StorageOptions, DestinationOptions extends StorageOptions, > { @@ -101,12 +102,12 @@ export interface StorageCopyInputKey< }; } -export interface StorageCopyInputPath { - source: StorageOperationInputPath & { +export interface StorageCopyInputWithPath { + source: StorageOperationInputWithPath & { /** @deprecated Use path instead. */ key?: never; }; - destination: StorageOperationInputPath & { + destination: StorageOperationInputWithPath & { /** @deprecated Use path instead. */ key?: never; }; diff --git a/packages/storage/src/types/outputs.ts b/packages/storage/src/types/outputs.ts index 2e83f60a20d..0fbf572988d 100644 --- a/packages/storage/src/types/outputs.ts +++ b/packages/storage/src/types/outputs.ts @@ -27,15 +27,15 @@ export interface StorageItemBase { metadata?: Record; } -/** @deprecated Use {@link StorageItemPath} instead. */ -export type StorageItemKey = StorageItemBase & { +/** @deprecated Use {@link StorageItemWithPath} instead. */ +export type StorageItemWithKey = StorageItemBase & { /** * Key of the object. */ key: string; }; -export type StorageItemPath = StorageItemBase & { +export type StorageItemWithPath = StorageItemBase & { /** * Path of the object. */ @@ -45,7 +45,7 @@ export type StorageItemPath = StorageItemBase & { /** * A storage item can be identified either by a key or a path. */ -export type StorageItem = StorageItemKey | StorageItemPath; +export type StorageItem = StorageItemWithKey | StorageItemWithPath; export type StorageDownloadDataOutput = Item & { body: ResponseBodyMixin; From 58b86bb8930061d406fe0f03a7d335c4fb40060a Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:34:06 -0700 Subject: [PATCH 18/33] [Fix] Update API overload types (#13214) * update overload type to include union * update type name and cleanup * update server api overload types * update bundle size * update type to use strict union input --- packages/aws-amplify/package.json | 2 +- packages/storage/src/providers/s3/apis/copy.ts | 1 + .../storage/src/providers/s3/apis/downloadData.ts | 1 + .../storage/src/providers/s3/apis/getProperties.ts | 1 + packages/storage/src/providers/s3/apis/getUrl.ts | 1 + packages/storage/src/providers/s3/apis/list.ts | 2 ++ packages/storage/src/providers/s3/apis/remove.ts | 1 + .../storage/src/providers/s3/apis/server/copy.ts | 5 +++++ .../src/providers/s3/apis/server/getProperties.ts | 4 ++++ .../storage/src/providers/s3/apis/server/getUrl.ts | 4 ++++ .../storage/src/providers/s3/apis/server/list.ts | 8 ++++++++ .../storage/src/providers/s3/apis/server/remove.ts | 4 ++++ .../src/providers/s3/apis/uploadData/index.ts | 1 + .../src/providers/s3/utils/isInputWithPath.ts | 4 ++-- .../s3/utils/validateStorageOperationInput.ts | 2 +- packages/storage/src/types/index.ts | 1 - packages/storage/src/types/inputs.ts | 14 +++----------- 17 files changed, 40 insertions(+), 16 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index d022535b24d..c1af3d962ad 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -473,7 +473,7 @@ "name": "[Storage] getProperties (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getProperties }", - "limit": "13.55 kB" + "limit": "13.56 kB" }, { "name": "[Storage] getUrl (S3)", diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index f71c11dfb56..17b25d3de1c 100644 --- a/packages/storage/src/providers/s3/apis/copy.ts +++ b/packages/storage/src/providers/s3/apis/copy.ts @@ -39,6 +39,7 @@ interface Copy { * source or destination key is not defined. */ (input: CopyInputWithKey): Promise; + (input: CopyInput): Promise; } export const copy: Copy = ( diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index 6dcd17b7fab..0f82719f211 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -90,6 +90,7 @@ interface DownloadData { *``` */ (input: DownloadDataInputWithKey): DownloadDataOutputWithKey; + (input: DownloadDataInput): DownloadDataOutput; } export const downloadData: DownloadData = ( diff --git a/packages/storage/src/providers/s3/apis/getProperties.ts b/packages/storage/src/providers/s3/apis/getProperties.ts index 35afaa2e014..d08b77d0fa7 100644 --- a/packages/storage/src/providers/s3/apis/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/getProperties.ts @@ -38,6 +38,7 @@ interface GetProperties { * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ (input: GetPropertiesInputWithKey): Promise; + (input: GetPropertiesInput): Promise; } export const getProperties: GetProperties = < diff --git a/packages/storage/src/providers/s3/apis/getUrl.ts b/packages/storage/src/providers/s3/apis/getUrl.ts index 5f426952fd5..24874da81d2 100644 --- a/packages/storage/src/providers/s3/apis/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/getUrl.ts @@ -50,6 +50,7 @@ interface GetUrl { * */ (input: GetUrlInputWithKey): Promise; + (input: GetUrlInput): Promise; } export const getUrl: GetUrl = (input: GetUrlInput): Promise => diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index 9ee3662430b..51c8a17a9cf 100644 --- a/packages/storage/src/providers/s3/apis/list.ts +++ b/packages/storage/src/providers/s3/apis/list.ts @@ -58,6 +58,8 @@ interface ListApi { * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ (input?: ListAllInputWithPrefix): Promise; + (input?: ListAllInput): Promise; + (input?: ListPaginateInput): Promise; } export const list: ListApi = < diff --git a/packages/storage/src/providers/s3/apis/remove.ts b/packages/storage/src/providers/s3/apis/remove.ts index f98d63c1735..78d93084860 100644 --- a/packages/storage/src/providers/s3/apis/remove.ts +++ b/packages/storage/src/providers/s3/apis/remove.ts @@ -36,6 +36,7 @@ interface RemoveApi { * when there is no key or its empty. */ (input: RemoveInputWithKey): Promise; + (input: RemoveInput): Promise; } export const remove: RemoveApi = ( diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index 2b77c86794a..bd6e3d7e463 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -48,6 +48,11 @@ interface Copy { contextSpec: AmplifyServer.ContextSpec, input: CopyInputWithKey, ): Promise; + + ( + contextSpec: AmplifyServer.ContextSpec, + input: CopyInput, + ): Promise; } export const copy: Copy = ( diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts index a66050f864d..9f5af13554f 100644 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/server/getProperties.ts @@ -48,6 +48,10 @@ interface GetProperties { contextSpec: AmplifyServer.ContextSpec, input: GetPropertiesInputWithKey, ): Promise; + ( + contextSpec: AmplifyServer.ContextSpec, + input: GetPropertiesInput, + ): Promise; } export const getProperties: GetProperties = < diff --git a/packages/storage/src/providers/s3/apis/server/getUrl.ts b/packages/storage/src/providers/s3/apis/server/getUrl.ts index c5cf6207e24..d3a75a1299e 100644 --- a/packages/storage/src/providers/s3/apis/server/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/server/getUrl.ts @@ -60,6 +60,10 @@ interface GetUrl { contextSpec: AmplifyServer.ContextSpec, input: GetUrlInputWithKey, ): Promise; + ( + contextSpec: AmplifyServer.ContextSpec, + input: GetUrlInput, + ): Promise; } export const getUrl: GetUrl = async ( contextSpec: AmplifyServer.ContextSpec, diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts index ea1250a7e3f..4ac584dc998 100644 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ b/packages/storage/src/providers/s3/apis/server/list.ts @@ -74,6 +74,14 @@ interface ListApi { contextSpec: AmplifyServer.ContextSpec, input?: ListAllInputWithPrefix, ): Promise; + ( + contextSpec: AmplifyServer.ContextSpec, + input?: ListPaginateInput, + ): Promise; + ( + contextSpec: AmplifyServer.ContextSpec, + input?: ListAllInput, + ): Promise; } export const list: ListApi = < diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts index c27e2831020..a0f8ef9f6db 100644 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ b/packages/storage/src/providers/s3/apis/server/remove.ts @@ -46,6 +46,10 @@ interface RemoveApi { contextSpec: AmplifyServer.ContextSpec, input: RemoveInputWithKey, ): Promise; + ( + contextSpec: AmplifyServer.ContextSpec, + input: RemoveInput, + ): Promise; } export const remove: RemoveApi = ( diff --git a/packages/storage/src/providers/s3/apis/uploadData/index.ts b/packages/storage/src/providers/s3/apis/uploadData/index.ts index ae4bbeefa65..b7ec67e4848 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/index.ts @@ -126,6 +126,7 @@ interface UploadData { * ``` */ (input: UploadDataInputWithKey): UploadDataOutputWithKey; + (input: UploadDataInput): UploadDataOutput; } export const uploadData: UploadData = ( diff --git a/packages/storage/src/providers/s3/utils/isInputWithPath.ts b/packages/storage/src/providers/s3/utils/isInputWithPath.ts index b0943154351..86e5351f914 100644 --- a/packages/storage/src/providers/s3/utils/isInputWithPath.ts +++ b/packages/storage/src/providers/s3/utils/isInputWithPath.ts @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { - StorageOperationInputType, + StorageOperationInput, StorageOperationInputWithPath, } from '../../../types/inputs'; export const isInputWithPath = ( - input: StorageOperationInputType, + input: StorageOperationInput, ): input is StorageOperationInputWithPath => { return input.path !== undefined; }; diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts index 12ff4eb0c2e..585701c81e9 100644 --- a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { StorageOperationInputType as Input } from '../../../types/inputs'; +import { StorageOperationInput as Input } from '../../../types/inputs'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { StorageValidationErrorCode } from '../../../errors/types/validation'; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 0f1a084f461..317fa20104c 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -8,7 +8,6 @@ export { UploadTask, } from './common'; export { - StorageOperationInput, StorageGetPropertiesInputWithKey, StorageGetPropertiesInputWithPath, StorageListInputWithPrefix, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 8a2cc0a0165..f371cbbb234 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -9,9 +9,7 @@ import { StorageOptions, } from './options'; -// TODO: rename to StorageOperationInput once the other type with -// the same named is removed -export type StorageOperationInputType = StrictUnion< +export type StorageOperationInput = StrictUnion< StorageOperationInputWithKey | StorageOperationInputWithPath >; export type StorageOperationInputWithPrefixPath = StrictUnion< @@ -43,15 +41,9 @@ export type StorageDownloadDataInputWithKey = export type StorageDownloadDataInputWithPath = StorageOperationInputWithPath & StorageOperationOptionsInput; -// TODO: This needs to be removed after refactor of all storage APIs -export interface StorageOperationInput { - key: string; - options?: Options; -} - /** @deprecated Use {@link StorageGetPropertiesInputWithPath} instead. */ export type StorageGetPropertiesInputWithKey = - StorageOperationInputWithKey & StorageOperationInput; + StorageOperationInputWithKey & StorageOperationOptionsInput; export type StorageGetPropertiesInputWithPath = StorageOperationInputWithPath & StorageOperationOptionsInput; @@ -73,7 +65,7 @@ export type StorageListInputWithPath< /** @deprecated Use {@link StorageGetUrlInputWithPath} instead. */ export type StorageGetUrlInputWithKey = - StorageOperationInputWithKey & StorageOperationInput; + StorageOperationInputWithKey & StorageOperationOptionsInput; export type StorageGetUrlInputWithPath = StorageOperationInputWithPath & StorageOperationOptionsInput; From 00224afe4a5a1f634bd82a6bab8cac61f759ff6d Mon Sep 17 00:00:00 2001 From: Francisco Rodriguez Date: Wed, 10 Apr 2024 09:24:50 -0700 Subject: [PATCH 19/33] Fix: relax config for AmplifyOutputs (#13234) Config: relax types to fix issues with TypeScript --- packages/core/__tests__/amplify_outputs.json | 72 ++++++++++++ .../__tests__/parseAmplifyOutputs.test.ts | 107 +++++++++++++++++- packages/core/src/parseAmplifyOutputs.ts | 37 ++---- .../src/singleton/AmplifyOutputs/types.ts | 39 ++----- 4 files changed, 197 insertions(+), 58 deletions(-) create mode 100644 packages/core/__tests__/amplify_outputs.json diff --git a/packages/core/__tests__/amplify_outputs.json b/packages/core/__tests__/amplify_outputs.json new file mode 100644 index 00000000000..3ab7633d1a7 --- /dev/null +++ b/packages/core/__tests__/amplify_outputs.json @@ -0,0 +1,72 @@ +{ + "$schema": "adipisicing cillum", + "version": "1", + "auth": { + "aws_region": "non proident exercitation anim fugiat", + "user_pool_id": "sit velit dolor magna est", + "user_pool_client_id": "voluptate", + "identity_pool_id": "Lorem", + "oauth": { + "identity_providers": ["FACEBOOK", "SIGN_IN_WITH_APPLE", "GOOGLE"], + "domain": "proident dolore do mollit ad", + "scopes": ["incididunt proident"], + "redirect_sign_in_uri": ["Duis", "ipsum velit in dolore"], + "redirect_sign_out_uri": [ + "Excepteur pariatur cillum officia", + "incididunt in Ut Excepteur commodo" + ], + "response_type": "token" + }, + "standard_required_attributes": [ + "address", + "locale", + "family_name", + "sub", + "email" + ], + "username_attributes": ["phone_number", "email"], + "user_verification_types": ["email", "email"], + "unauthenticated_identities_enabled": true, + "mfa_configuration": "OPTIONAL", + "mfa_methods": ["TOTP", "TOTP", "SMS", "TOTP", "TOTP"] + }, + "data": { + "aws_region": "regasd", + "url": "dolore dolor do cillum nulla", + "api_key": "non", + "default_authorization_type": "API_KEY", + "authorization_types": [] + }, + "geo": { + "aws_region": "tempor", + "search_indices": { + "items": [ + "commodo Lorem", + "reprehenderit consequat", + "amet", + "aliquip deserunt", + "ea dolor in proident" + ], + "default": "exercitation fugiat ut dolor sed" + }, + "geofence_collections": { + "items": [ + "fugiat ea irure dolor", + "Ut", + "culpa ut enim exercitation", + "labore", + "ex pariatur est ullamco" + ], + "default": "ullamco incididunt aliquip" + } + }, + "custom": { + "occaecat_4_": -51806024, + "dolorc": 87599986 + }, + "notifications": { + "aws_region": "labore nisi ad", + "amazon_pinpoint_app_id": "in dolor veniam reprehenderit", + "channels": ["EMAIL"] + } +} diff --git a/packages/core/__tests__/parseAmplifyOutputs.test.ts b/packages/core/__tests__/parseAmplifyOutputs.test.ts index d226874a493..39d2012565b 100644 --- a/packages/core/__tests__/parseAmplifyOutputs.test.ts +++ b/packages/core/__tests__/parseAmplifyOutputs.test.ts @@ -1,9 +1,106 @@ import { AmplifyOutputs, parseAmplifyOutputs } from '../src/libraryUtils'; - +import amplifyOutputs from './amplify_outputs.json'; describe('parseAmplifyOutputs tests', () => { describe('auth tests', () => { + + it('should parse from amplify-outputs.json', async () => { + const result = parseAmplifyOutputs(amplifyOutputs); + + expect(result).toEqual({ + "API": { + "GraphQL": { + "apiKey": "non", + "defaultAuthMode": "apiKey", + "endpoint": "dolore dolor do cillum nulla", + "modelIntrospection": undefined, + "region": "regasd", + }, + }, + "Auth": { + "Cognito": { + "allowGuestAccess": true, + "identityPoolId": "Lorem", + "loginWith": { + "email": true, + "oauth": { + "domain": "proident dolore do mollit ad", + "providers": [ + "Facebook", + "Apple", + "Google", + ], + "redirectSignIn": [ + "Duis", + "ipsum velit in dolore", + ], + "redirectSignOut": [ + "Excepteur pariatur cillum officia", + "incididunt in Ut Excepteur commodo", + ], + "responseType": "token", + "scopes": [ + "incididunt proident", + ], + }, + "phone": true, + }, + "mfa": { + "smsEnabled": true, + "status": "optional", + "totpEnabled": true, + }, + "userAttributes": { + "address": { + "required": true, + }, + "email": { + "required": true, + }, + "family_name": { + "required": true, + }, + "locale": { + "required": true, + }, + "sub": { + "required": true, + }, + }, + "userPoolClientId": "voluptate", + "userPoolId": "sit velit dolor magna est", + }, + }, + "Geo": { + "LocationService": { + "geofenceCollections": { + "default": "ullamco incididunt aliquip", + "items": [ + "fugiat ea irure dolor", + "Ut", + "culpa ut enim exercitation", + "labore", + "ex pariatur est ullamco", + ], + }, + "maps": undefined, + "region": "tempor", + "searchIndices": { + "default": "exercitation fugiat ut dolor sed", + "items": [ + "commodo Lorem", + "reprehenderit consequat", + "amet", + "aliquip deserunt", + "ea dolor in proident", + ], + }, + }, + }, + }); + }); + it('should parse auth happy path (all enabled)', () => { - const amplifyOutputs: AmplifyOutputs = { + const amplifyOutputs = { 'version': '1', 'auth': { 'user_pool_id': 'us-east-1:', @@ -26,8 +123,8 @@ describe('parseAmplifyOutputs tests', () => { 'require_numbers': true }, 'standard_required_attributes': ['email'], - 'username_attributes': ['EMAIL'], - 'user_verification_mechanisms': ['EMAIL'], + 'username_attributes': ['email'], + 'user_verification_types': ['email'], 'unauthenticated_identities_enabled': true, 'mfa_configuration': 'OPTIONAL', 'mfa_methods': ['SMS'] @@ -224,7 +321,7 @@ describe('parseAmplifyOutputs tests', () => { 'version': '1', 'notifications': { aws_region: 'us-west-2', - pinpoint_app_id: 'appid123', + amazon_pinpoint_app_id: 'appid123', channels: ['APNS', 'EMAIL', 'FCM', 'IN_APP_MESSAGING', 'SMS'], } }; diff --git a/packages/core/src/parseAmplifyOutputs.ts b/packages/core/src/parseAmplifyOutputs.ts index badd70aef02..09c62e5d4c7 100644 --- a/packages/core/src/parseAmplifyOutputs.ts +++ b/packages/core/src/parseAmplifyOutputs.ts @@ -20,14 +20,11 @@ import { NotificationsConfig } from './singleton/Notifications/types'; import { AmplifyOutputs, AmplifyOutputsAnalyticsProperties, - AmplifyOutputsAuthMFAConfiguration, AmplifyOutputsAuthProperties, AmplifyOutputsDataProperties, AmplifyOutputsGeoProperties, AmplifyOutputsNotificationsProperties, - AmplifyOutputsOAuthIdentityProvider, AmplifyOutputsStorageProperties, - AuthType, } from './singleton/AmplifyOutputs/types'; import { AnalyticsConfig, @@ -130,34 +127,27 @@ function parseAuth( domain: oauth.domain, redirectSignIn: oauth.redirect_sign_in_uri, redirectSignOut: oauth.redirect_sign_out_uri, - responseType: oauth.response_type, + responseType: oauth.response_type === 'token' ? 'token' : 'code', scopes: oauth.scopes, providers: getOAuthProviders(oauth.identity_providers), }, }; } - if (username_attributes?.includes('EMAIL')) { + if (username_attributes?.includes('email')) { authConfig.Cognito.loginWith = { ...authConfig.Cognito.loginWith, email: true, }; } - if (username_attributes?.includes('PHONE_NUMBER')) { + if (username_attributes?.includes('phone_number')) { authConfig.Cognito.loginWith = { ...authConfig.Cognito.loginWith, phone: true, }; } - if (username_attributes?.includes('USERNAME')) { - authConfig.Cognito.loginWith = { - ...authConfig.Cognito.loginWith, - username: true, - }; - } - if (standard_required_attributes) { authConfig.Cognito.userAttributes = standard_required_attributes.reduce( (acc, curr) => ({ ...acc, [curr]: { required: true } }), @@ -240,7 +230,7 @@ function parseNotifications( return undefined; } - const { aws_region, channels, pinpoint_app_id } = + const { aws_region, channels, amazon_pinpoint_app_id } = amplifyOutputsNotificationsProperties; const hasInAppMessaging = channels.includes('IN_APP_MESSAGING'); @@ -257,7 +247,7 @@ function parseNotifications( if (hasInAppMessaging) { notificationsConfig.InAppMessaging = { Pinpoint: { - appId: pinpoint_app_id, + appId: amazon_pinpoint_app_id, region: aws_region, }, }; @@ -266,7 +256,7 @@ function parseNotifications( if (hasPushNotification) { notificationsConfig.PushNotification = { Pinpoint: { - appId: pinpoint_app_id, + appId: amazon_pinpoint_app_id, region: aws_region, }, }; @@ -309,7 +299,7 @@ export function parseAmplifyOutputs( return resourcesConfig; } -const authModeNames: Record = { +const authModeNames: Record = { AMAZON_COGNITO_USER_POOLS: 'userPool', API_KEY: 'apiKey', AWS_IAM: 'iam', @@ -317,28 +307,23 @@ const authModeNames: Record = { OPENID_CONNECT: 'oidc', }; -function getGraphQLAuthMode(authType: AuthType): GraphQLAuthMode { +function getGraphQLAuthMode(authType: string): GraphQLAuthMode { return authModeNames[authType]; } -const providerNames: Record< - AmplifyOutputsOAuthIdentityProvider, - OAuthProvider -> = { +const providerNames: Record = { GOOGLE: 'Google', LOGIN_WITH_AMAZON: 'Amazon', FACEBOOK: 'Facebook', SIGN_IN_WITH_APPLE: 'Apple', }; -function getOAuthProviders( - providers: AmplifyOutputsOAuthIdentityProvider[] = [], -): OAuthProvider[] { +function getOAuthProviders(providers: string[] = []): OAuthProvider[] { return providers.map(provider => providerNames[provider]); } function getMfaStatus( - mfaConfiguration: AmplifyOutputsAuthMFAConfiguration, + mfaConfiguration: string, ): CognitoUserPoolConfigMfaStatus { if (mfaConfiguration === 'OPTIONAL') return 'optional'; if (mfaConfiguration === 'REQUIRED') return 'on'; diff --git a/packages/core/src/singleton/AmplifyOutputs/types.ts b/packages/core/src/singleton/AmplifyOutputs/types.ts index c28b2c53513..d3476f87723 100644 --- a/packages/core/src/singleton/AmplifyOutputs/types.ts +++ b/packages/core/src/singleton/AmplifyOutputs/types.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { ModelIntrospectionSchema } from '../API/types'; -import { AuthStandardAttributeKey } from '../Auth/types'; export type AmplifyOutputsOAuthIdentityProvider = | 'GOOGLE' @@ -10,13 +9,6 @@ export type AmplifyOutputsOAuthIdentityProvider = | 'LOGIN_WITH_AMAZON' | 'SIGN_IN_WITH_APPLE'; -type AmplifyOutputsAuthUsernameAttribute = - | 'EMAIL' - | 'PHONE_NUMBER' - | 'USERNAME'; - -type AmplifyOutputsAuthUserVerificationMethod = 'EMAIL' | 'PHONE_NUMBER'; - export type AmplifyOutputsAuthMFAConfiguration = | 'OPTIONAL' | 'REQUIRED' @@ -38,19 +30,19 @@ export interface AmplifyOutputsAuthProperties { require_symbols: boolean; }; oauth?: { - identity_providers: AmplifyOutputsOAuthIdentityProvider[]; + identity_providers: string[]; domain: string; scopes: string[]; redirect_sign_in_uri: string[]; redirect_sign_out_uri: string[]; - response_type: 'code' | 'token'; + response_type: string; }; - standard_required_attributes?: AuthStandardAttributeKey[]; - username_attributes?: AmplifyOutputsAuthUsernameAttribute[]; - user_verification_mechanisms?: AmplifyOutputsAuthUserVerificationMethod[]; + standard_required_attributes?: string[]; + username_attributes?: string[]; + user_verification_types?: string[]; unauthenticated_identities_enabled?: boolean; - mfa_configuration?: AmplifyOutputsAuthMFAConfiguration; - mfa_methods?: AmplifyOutputsAuthMFAMethod[]; + mfa_configuration?: string; + mfa_methods?: string[]; } export interface AmplifyOutputsStorageProperties { @@ -85,24 +77,17 @@ export type AuthType = export interface AmplifyOutputsDataProperties { aws_region: string; url: string; - default_authorization_type: AuthType; - authorization_types: AuthType[]; + default_authorization_type: string; + authorization_types: string[]; model_introspection?: ModelIntrospectionSchema; api_key?: string; - conflict_resolution_mode?: 'AUTO_MERGE' | 'OPTIMISTIC_CONCURRENCY' | 'LAMBDA'; + conflict_resolution_mode?: string; } -type AmplifyOutputsNotificationChannel = - | 'IN_APP_MESSAGING' - | 'FCM' - | 'APNS' - | 'EMAIL' - | 'SMS'; - export interface AmplifyOutputsNotificationsProperties { aws_region: string; - pinpoint_app_id: string; - channels: AmplifyOutputsNotificationChannel[]; + amazon_pinpoint_app_id: string; + channels: string[]; } export interface AmplifyOutputs { From a81fc29e3b339479110410ed75f28a2f95e32686 Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:58:20 -0700 Subject: [PATCH 20/33] Fix Overload return types for storage APIs (#13239) * update getProperties return type * update remove return type * update downloadData return types * update copy return types * update upload return types * update list return types * clean up output types * update test and list api --- packages/aws-amplify/package.json | 2 +- .../__tests__/providers/s3/apis/copy.test.ts | 11 +- .../providers/s3/apis/downloadData.test.ts | 3 + .../providers/s3/apis/getProperties.test.ts | 3 +- .../__tests__/providers/s3/apis/list.test.ts | 51 ++++--- .../providers/s3/apis/remove.test.ts | 19 +-- .../s3/apis/uploadData/putObjectJob.test.ts | 9 +- .../storage/src/providers/s3/apis/copy.ts | 6 +- .../src/providers/s3/apis/downloadData.ts | 21 +-- .../src/providers/s3/apis/getProperties.ts | 6 +- .../src/providers/s3/apis/internal/copy.ts | 9 +- .../s3/apis/internal/getProperties.ts | 4 +- .../src/providers/s3/apis/internal/list.ts | 41 +++--- .../src/providers/s3/apis/internal/remove.ts | 4 +- .../storage/src/providers/s3/apis/list.ts | 12 +- .../storage/src/providers/s3/apis/remove.ts | 6 +- .../src/providers/s3/apis/server/copy.ts | 6 +- .../providers/s3/apis/server/getProperties.ts | 6 +- .../src/providers/s3/apis/server/list.ts | 12 +- .../src/providers/s3/apis/server/remove.ts | 6 +- .../src/providers/s3/apis/uploadData/index.ts | 6 +- .../uploadData/multipart/uploadHandlers.ts | 14 +- .../s3/apis/uploadData/putObjectJob.ts | 8 +- .../storage/src/providers/s3/types/index.ts | 17 +-- .../storage/src/providers/s3/types/outputs.ts | 124 +++--------------- packages/storage/src/types/index.ts | 2 - packages/storage/src/types/outputs.ts | 21 +-- 27 files changed, 160 insertions(+), 269 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index c1af3d962ad..8706c23c601 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -479,7 +479,7 @@ "name": "[Storage] getUrl (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getUrl }", - "limit": "14.63 kB" + "limit": "14.65 kB" }, { "name": "[Storage] list (S3)", diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index 8221377fea9..c7896ca868e 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -34,7 +34,6 @@ const bucket = 'bucket'; const region = 'region'; const targetIdentityId = 'targetIdentityId'; const defaultIdentityId = 'defaultIdentityId'; -const copyResult = { key: destinationKey }; const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -161,6 +160,11 @@ describe('copy API', () => { const targetIdentityIdMsg = source?.targetIdentityId ? `with targetIdentityId` : ''; + const copyResult = { + key: destinationKey, + path: expectedDestinationKey, + }; + it(`should copy ${source.accessLevel} ${targetIdentityIdMsg} -> ${destination.accessLevel}`, async () => { expect( await copy({ @@ -223,7 +227,10 @@ describe('copy API', () => { source: { path: sourcePath }, destination: { path: destinationPath }, }), - ).toEqual({ path: expectedDestinationPath }); + ).toEqual({ + path: expectedDestinationPath, + key: expectedDestinationPath, + }); expect(copyObject).toHaveBeenCalledTimes(1); expect(copyObject).toHaveBeenCalledWith(copyObjectClientConfig, { ...copyObjectClientBaseParams, diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 3904d9436dc..8118d47e56a 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -138,6 +138,7 @@ describe('downloadData with key', () => { const contentType = 'contentType'; const body = 'body'; const key = 'key'; + const expectedKey = `public/${key}`; (getObject as jest.Mock).mockResolvedValueOnce({ Body: body, LastModified: lastModified, @@ -153,6 +154,7 @@ describe('downloadData with key', () => { expect(getObject).toHaveBeenCalledTimes(1); expect(result).toEqual({ key, + path: `public/${key}`, body, lastModified, size: contentLength, @@ -281,6 +283,7 @@ describe('downloadData with path', () => { expect(getObject).toHaveBeenCalledTimes(1); expect(result).toEqual({ path, + key: path, body, lastModified, size: contentLength, diff --git a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts index 151ae7cebd2..a8731fb65c5 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -113,7 +113,7 @@ describe('getProperties with key', () => { key, options: options as GetPropertiesOptionsWithKey, }), - ).toEqual(expected); + ).toEqual({ ...expected, path: expectedKey }); expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); }, @@ -170,6 +170,7 @@ describe('Happy cases: With path', () => { }); describe('getProperties with path', () => { const expected = { + key: path, path, size: '100', contentType: 'text/plain', diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index 1db5fa5a6dc..0b46c6a21db 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -8,8 +8,8 @@ import { list } from '../../../../src/providers/s3'; import { ListAllOptionsWithPrefix, ListPaginateOptionsWithPrefix, + ListPaginateOutput, } from '../../../../src/providers/s3/types'; -import { StorageValidationErrorCode } from '../../../../src/errors/types/validation'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -147,7 +147,9 @@ describe('list API', () => { prefix: key, options: options as ListPaginateOptionsWithPrefix, }); - expect(response.items).toEqual([{ ...listResultItem, key: key ?? '' }]); + expect(response.items).toEqual([ + { ...listResultItem, key: key ?? '', path: expectedPath ?? '' }, + ]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { @@ -174,7 +176,7 @@ describe('list API', () => { }; }); const customPageSize = 5; - const response = await list({ + const response: ListPaginateOutput = await list({ prefix: key, options: { ...(options as ListPaginateOptionsWithPrefix), @@ -182,7 +184,9 @@ describe('list API', () => { nextToken: nextToken, }, }); - expect(response.items).toEqual([{ ...listResultItem, key: key ?? '' }]); + expect(response.items).toEqual([ + { ...listResultItem, key: key ?? '', path: expectedPath ?? '' }, + ]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { @@ -232,7 +236,11 @@ describe('list API', () => { options: { ...options, listAll: true } as ListAllOptionsWithPrefix, }); - const listResult = { ...listResultItem, key: key ?? '' }; + const listResult = { + ...listResultItem, + key: key ?? '', + path: expectedPath ?? '', + }; expect(result.items).toEqual([listResult, listResult, listResult]); expect(result).not.toHaveProperty(nextToken); @@ -284,12 +292,13 @@ describe('list API', () => { it.each(pathAsFunctionAndStringTests)( 'should list objects with pagination, default pageSize, custom path', async ({ path }) => { + const resolvedPath = resolvePath(path); mockListObject.mockImplementationOnce(() => { return { Contents: [ { ...listObjectClientBaseResultItem, - Key: resolvePath(path), + Key: resolvedPath, }, ], NextContinuationToken: nextToken, @@ -299,14 +308,18 @@ describe('list API', () => { path, }); expect(response.items).toEqual([ - { ...listResultItem, path: resolvePath(path) }, + { + ...listResultItem, + path: resolvedPath, + key: resolvedPath, + }, ]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, MaxKeys: 1000, - Prefix: resolvePath(path), + Prefix: resolvedPath, }); }, ); @@ -314,12 +327,13 @@ describe('list API', () => { it.each(pathAsFunctionAndStringTests)( 'should list objects with pagination using custom pageSize, nextToken and custom path: ${path}', async ({ path }) => { + const resolvedPath = resolvePath(path); mockListObject.mockImplementationOnce(() => { return { Contents: [ { ...listObjectClientBaseResultItem, - Key: resolvePath(path), + Key: resolvedPath, }, ], NextContinuationToken: nextToken, @@ -334,13 +348,17 @@ describe('list API', () => { }, }); expect(response.items).toEqual([ - { ...listResultItem, path: resolvePath(path) ?? '' }, + { + ...listResultItem, + path: resolvedPath, + key: resolvedPath, + }, ]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, - Prefix: resolvePath(path), + Prefix: resolvedPath, ContinuationToken: nextToken, MaxKeys: customPageSize, }); @@ -350,6 +368,7 @@ describe('list API', () => { it.each(pathAsFunctionAndStringTests)( 'should list objects with zero results with custom path: ${path}', async ({ path }) => { + const resolvedPath = resolvePath(path); mockListObject.mockImplementationOnce(() => { return {}; }); @@ -362,7 +381,7 @@ describe('list API', () => { expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, MaxKeys: 1000, - Prefix: resolvePath(path), + Prefix: resolvedPath, }); }, ); @@ -370,6 +389,7 @@ describe('list API', () => { it.each(pathAsFunctionAndStringTests)( 'should list all objects having three pages with custom path: ${path}', async ({ path }) => { + const resolvedPath = resolvePath(path); mockListObjectsV2ApiWithPages(3); const result = await list({ path, @@ -378,7 +398,8 @@ describe('list API', () => { const listResult = { ...listResultItem, - path: resolvePath(path), + path: resolvedPath, + key: resolvedPath, }; expect(result.items).toEqual([listResult, listResult, listResult]); expect(result).not.toHaveProperty(nextToken); @@ -392,7 +413,7 @@ describe('list API', () => { listObjectClientConfig, { Bucket: bucket, - Prefix: resolvePath(path), + Prefix: resolvedPath, MaxKeys: 1000, ContinuationToken: undefined, }, @@ -403,7 +424,7 @@ describe('list API', () => { listObjectClientConfig, { Bucket: bucket, - Prefix: resolvePath(path), + Prefix: resolvedPath, MaxKeys: 1000, ContinuationToken: nextToken, }, diff --git a/packages/storage/__tests__/providers/s3/apis/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/remove.test.ts index aa1cdaa2b3c..96abf2f976d 100644 --- a/packages/storage/__tests__/providers/s3/apis/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/remove.test.ts @@ -27,7 +27,6 @@ const key = 'key'; const bucket = 'bucket'; const region = 'region'; const defaultIdentityId = 'defaultIdentityId'; -const removeResultKey = { key }; const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -84,12 +83,13 @@ describe('remove API', () => { }, ].forEach(({ options, expectedKey }) => { const accessLevel = options?.accessLevel ?? 'default'; + const removeResultKey = { key }; it(`should remove object with ${accessLevel} accessLevel`, async () => { expect.assertions(3); expect( await remove({ key, options: options as StorageOptions }), - ).toEqual(removeResultKey); + ).toEqual({ ...removeResultKey, path: expectedKey }); expect(deleteObject).toHaveBeenCalledTimes(1); expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { Bucket: bucket, @@ -99,10 +99,6 @@ describe('remove API', () => { }); }); describe('With Path', () => { - const resolvePath = (path: string | Function) => - typeof path === 'string' - ? path - : path({ identityId: defaultIdentityId }); beforeEach(() => { mockDeleteObject.mockImplementation(() => { return { @@ -121,8 +117,13 @@ describe('remove API', () => { path: ({ identityId }: any) => `protected/${identityId}/${key}`, }, ].forEach(({ path }) => { + const resolvePath = + typeof path === 'string' + ? path + : path({ identityId: defaultIdentityId }); const removeResultPath = { - path: resolvePath(path), + path: resolvePath, + key: resolvePath, }; it(`should remove object for the given path`, async () => { @@ -131,14 +132,14 @@ describe('remove API', () => { expect(deleteObject).toHaveBeenCalledTimes(1); expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { Bucket: bucket, - Key: resolvePath(path), + Key: resolvePath, }); }); }); }); }); - describe('Error Path Cases:', () => { + describe('Error Cases', () => { afterEach(() => { jest.clearAllMocks(); }); 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 e429b4a9fb3..6106ff8fa46 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts @@ -58,6 +58,7 @@ describe('putObjectJob with key', () => { it('should supply the correct parameters to putObject API handler', async () => { const abortController = new AbortController(); const key = 'key'; + const finalKey = `public/${key}`; const data = 'data'; const contentType = 'contentType'; const contentDisposition = 'contentDisposition'; @@ -84,6 +85,7 @@ describe('putObjectJob with key', () => { const result = await job(); expect(result).toEqual({ key, + path: finalKey, eTag: 'eTag', versionId: 'versionId', contentType: 'contentType', @@ -101,7 +103,7 @@ describe('putObjectJob with key', () => { }, { Bucket: 'bucket', - Key: `public/${key}`, + Key: finalKey, Body: data, ContentType: contentType, ContentDisposition: contentDisposition, @@ -143,7 +145,7 @@ describe('putObjectJob with path', () => { expectedKey: testPath, }, ])( - 'should supply the correct parameters to putObject API handler when path is $path', + 'should supply the correct parameters to putObject API handler when path is $path', async ({ path, expectedKey }) => { const abortController = new AbortController(); const data = 'data'; @@ -172,6 +174,7 @@ describe('putObjectJob with path', () => { const result = await job(); expect(result).toEqual({ path: expectedKey, + key: expectedKey, eTag: 'eTag', versionId: 'versionId', contentType: 'contentType', @@ -198,7 +201,7 @@ describe('putObjectJob with path', () => { ContentMD5: undefined, }, ); - } + }, ); it('should set ContentMD5 if object lock is enabled', async () => { diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index 17b25d3de1c..19cfbf034db 100644 --- a/packages/storage/src/providers/s3/apis/copy.ts +++ b/packages/storage/src/providers/s3/apis/copy.ts @@ -8,8 +8,6 @@ import { CopyInputWithKey, CopyInputWithPath, CopyOutput, - CopyOutputWithKey, - CopyOutputWithPath, } from '../types'; import { copy as copyInternal } from './internal/copy'; @@ -24,7 +22,7 @@ interface Copy { * @throws validation: `StorageValidationErrorCode` - Thrown when * source or destination path is not defined. */ - (input: CopyInputWithPath): Promise; + (input: CopyInputWithPath): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. @@ -38,7 +36,7 @@ interface Copy { * @throws validation: `StorageValidationErrorCode` - Thrown when * source or destination key is not defined. */ - (input: CopyInputWithKey): Promise; + (input: CopyInputWithKey): Promise; (input: CopyInput): Promise; } diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index 0f82719f211..ff12d6b55c3 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -9,19 +9,14 @@ import { DownloadDataInputWithKey, DownloadDataInputWithPath, DownloadDataOutput, - DownloadDataOutputWithKey, - DownloadDataOutputWithPath, + ItemWithKeyAndPath, } from '../types'; import { resolveS3ConfigAndInput } from '../utils/resolveS3ConfigAndInput'; import { createDownloadTask, validateStorageOperationInput } from '../utils'; import { getObject } from '../utils/client'; import { getStorageUserAgentValue } from '../utils/userAgent'; import { logger } from '../../../utils'; -import { - StorageDownloadDataOutput, - StorageItemWithKey, - StorageItemWithPath, -} from '../../../types'; +import { StorageDownloadDataOutput } from '../../../types'; import { STORAGE_INPUT_KEY } from '../utils/constants'; interface DownloadData { @@ -55,7 +50,7 @@ interface DownloadData { * } *``` */ - (input: DownloadDataInputWithPath): DownloadDataOutputWithPath; + (input: DownloadDataInputWithPath): DownloadDataOutput; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. @@ -89,7 +84,7 @@ interface DownloadData { * } *``` */ - (input: DownloadDataInputWithKey): DownloadDataOutputWithKey; + (input: DownloadDataInputWithKey): DownloadDataOutput; (input: DownloadDataInput): DownloadDataOutput; } @@ -110,9 +105,7 @@ export const downloadData: DownloadData = ( const downloadDataJob = (downloadDataInput: DownloadDataInput, abortSignal: AbortSignal) => - async (): Promise< - StorageDownloadDataOutput - > => { + async (): Promise> => { const { options: downloadDataOptions } = downloadDataInput; const { bucket, keyPrefix, s3Config, identityId } = await resolveS3ConfigAndInput(Amplify, downloadDataOptions); @@ -160,6 +153,6 @@ const downloadDataJob = }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, ...result } - : { path: finalKey, ...result }; + ? { key: objectKey, path: finalKey, ...result } + : { path: finalKey, key: finalKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/apis/getProperties.ts b/packages/storage/src/providers/s3/apis/getProperties.ts index d08b77d0fa7..c5e866922b4 100644 --- a/packages/storage/src/providers/s3/apis/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/getProperties.ts @@ -8,8 +8,6 @@ import { GetPropertiesInputWithKey, GetPropertiesInputWithPath, GetPropertiesOutput, - GetPropertiesOutputWithKey, - GetPropertiesOutputWithPath, } from '../types'; import { getProperties as getPropertiesInternal } from './internal/getProperties'; @@ -24,7 +22,7 @@ interface GetProperties { * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ - (input: GetPropertiesInputWithPath): Promise; + (input: GetPropertiesInputWithPath): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. @@ -37,7 +35,7 @@ interface GetProperties { * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ - (input: GetPropertiesInputWithKey): Promise; + (input: GetPropertiesInputWithKey): Promise; (input: GetPropertiesInput): Promise; } diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts index 5b8669dbe1b..7882a3429dc 100644 --- a/packages/storage/src/providers/s3/apis/internal/copy.ts +++ b/packages/storage/src/providers/s3/apis/internal/copy.ts @@ -9,8 +9,6 @@ import { CopyInputWithKey, CopyInputWithPath, CopyOutput, - CopyOutputWithKey, - CopyOutputWithPath, } from '../../types'; import { ResolvedS3Config } from '../../types/options'; import { @@ -39,7 +37,7 @@ export const copy = async ( const copyWithPath = async ( amplify: AmplifyClassV6, input: CopyInputWithPath, -): Promise => { +): Promise => { const { source, destination } = input; const { s3Config, bucket, identityId } = await resolveS3ConfigAndInput(amplify); @@ -70,14 +68,14 @@ const copyWithPath = async ( s3Config, }); - return { path: finalCopyDestination }; + return { path: finalCopyDestination, key: finalCopyDestination }; }; /** @deprecated Use {@link copyWithPath} instead. */ export const copyWithKey = async ( amplify: AmplifyClassV6, input: CopyInputWithKey, -): Promise => { +): Promise => { const { source: { key: sourceKey }, destination: { key: destinationKey }, @@ -113,6 +111,7 @@ export const copyWithKey = async ( return { key: destinationKey, + path: finalCopyDestination, }; }; diff --git a/packages/storage/src/providers/s3/apis/internal/getProperties.ts b/packages/storage/src/providers/s3/apis/internal/getProperties.ts index e7f20187a36..e2dd8a19780 100644 --- a/packages/storage/src/providers/s3/apis/internal/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/internal/getProperties.ts @@ -53,6 +53,6 @@ export const getProperties = async ( }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, ...result } - : { path: objectKey, ...result }; + ? { key: objectKey, path: finalKey, ...result } + : { path: finalKey, key: finalKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/apis/internal/list.ts b/packages/storage/src/providers/s3/apis/internal/list.ts index 47d36d59178..bf17316d495 100644 --- a/packages/storage/src/providers/s3/apis/internal/list.ts +++ b/packages/storage/src/providers/s3/apis/internal/list.ts @@ -7,14 +7,8 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { ListAllInput, ListAllOutput, - ListAllOutputWithPath, - ListAllOutputWithPrefix, - ListOutputItemWithKey, - ListOutputItemWithPath, ListPaginateInput, ListPaginateOutput, - ListPaginateOutputWithPath, - ListPaginateOutputWithPrefix, } from '../../types'; import { resolveS3ConfigAndInput, @@ -29,6 +23,7 @@ import { import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; import { STORAGE_INPUT_PREFIX } from '../../utils/constants'; +import { ListOutputItem } from '../../types/outputs'; const MAX_PAGE_SIZE = 1000; @@ -87,7 +82,7 @@ export const list = async ( return _listAllWithPath(listInputArgs); } } else { - if (inputType === STORAGE_INPUT_PREFIX) { + if (isInputWithPrefix) { return _listWithPrefix({ ...listInputArgs, generatedPrefix }); } else { return _listWithPath(listInputArgs); @@ -100,8 +95,8 @@ const _listAllWithPrefix = async ({ s3Config, listParams, generatedPrefix, -}: ListInputArgs): Promise => { - const listResult: ListOutputItemWithKey[] = []; +}: ListInputArgs): Promise => { + const listResult: ListOutputItem[] = []; let continuationToken = listParams.ContinuationToken; do { const { items: pageResults, nextToken: pageNextToken } = @@ -128,7 +123,7 @@ const _listWithPrefix = async ({ s3Config, listParams, generatedPrefix, -}: ListInputArgs): Promise => { +}: ListInputArgs): Promise => { const listParamsClone = { ...listParams }; if (!listParamsClone.MaxKeys || listParamsClone.MaxKeys > MAX_PAGE_SIZE) { logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`); @@ -150,14 +145,19 @@ const _listWithPrefix = async ({ } return { - items: response.Contents.map(item => ({ - key: generatedPrefix + items: response.Contents.map(item => { + const finalKey = generatedPrefix ? item.Key!.substring(generatedPrefix.length) - : item.Key!, - eTag: item.ETag, - lastModified: item.LastModified, - size: item.Size, - })), + : item.Key!; + + return { + key: finalKey, + path: item.Key!, + eTag: item.ETag, + lastModified: item.LastModified, + size: item.Size, + }; + }), nextToken: response.NextContinuationToken, }; }; @@ -165,8 +165,8 @@ const _listWithPrefix = async ({ const _listAllWithPath = async ({ s3Config, listParams, -}: ListInputArgs): Promise => { - const listResult: ListOutputItemWithPath[] = []; +}: ListInputArgs): Promise => { + const listResult: ListOutputItem[] = []; let continuationToken = listParams.ContinuationToken; do { const { items: pageResults, nextToken: pageNextToken } = @@ -190,7 +190,7 @@ const _listAllWithPath = async ({ const _listWithPath = async ({ s3Config, listParams, -}: ListInputArgs): Promise => { +}: ListInputArgs): Promise => { const listParamsClone = { ...listParams }; if (!listParamsClone.MaxKeys || listParamsClone.MaxKeys > MAX_PAGE_SIZE) { logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`); @@ -214,6 +214,7 @@ const _listWithPath = async ({ return { items: response.Contents.map(item => ({ path: item.Key!, + key: item.Key!, eTag: item.ETag, lastModified: item.LastModified, size: item.Size, diff --git a/packages/storage/src/providers/s3/apis/internal/remove.ts b/packages/storage/src/providers/s3/apis/internal/remove.ts index 0b38095b7ce..492b35c2362 100644 --- a/packages/storage/src/providers/s3/apis/internal/remove.ts +++ b/packages/storage/src/providers/s3/apis/internal/remove.ts @@ -50,8 +50,10 @@ export const remove = async ( return inputType === STORAGE_INPUT_KEY ? { key: objectKey, + path: finalKey, } : { - path: objectKey, + path: finalKey, + key: finalKey, }; }; diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index 51c8a17a9cf..2383ebfcca1 100644 --- a/packages/storage/src/providers/s3/apis/list.ts +++ b/packages/storage/src/providers/s3/apis/list.ts @@ -7,14 +7,10 @@ import { ListAllInputWithPath, ListAllInputWithPrefix, ListAllOutput, - ListAllOutputWithPath, - ListAllOutputWithPrefix, ListPaginateInput, ListPaginateInputWithPath, ListPaginateInputWithPrefix, ListPaginateOutput, - ListPaginateOutputWithPath, - ListPaginateOutputWithPrefix, } from '../types'; import { list as listInternal } from './internal/list'; @@ -28,7 +24,7 @@ interface ListApi { * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input: ListPaginateInputWithPath): Promise; + (input: ListPaginateInputWithPath): Promise; /** * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. * @param input - The `ListAllInputWithPath` object. @@ -36,7 +32,7 @@ interface ListApi { * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input: ListAllInputWithPath): Promise; + (input: ListAllInputWithPath): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. @@ -47,7 +43,7 @@ interface ListApi { * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input?: ListPaginateInputWithPrefix): Promise; + (input?: ListPaginateInputWithPrefix): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. @@ -57,7 +53,7 @@ interface ListApi { * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ - (input?: ListAllInputWithPrefix): Promise; + (input?: ListAllInputWithPrefix): Promise; (input?: ListAllInput): Promise; (input?: ListPaginateInput): Promise; } diff --git a/packages/storage/src/providers/s3/apis/remove.ts b/packages/storage/src/providers/s3/apis/remove.ts index 78d93084860..e0e100d2d4a 100644 --- a/packages/storage/src/providers/s3/apis/remove.ts +++ b/packages/storage/src/providers/s3/apis/remove.ts @@ -8,8 +8,6 @@ import { RemoveInputWithKey, RemoveInputWithPath, RemoveOutput, - RemoveOutputWithKey, - RemoveOutputWithPath, } from '../types'; import { remove as removeInternal } from './internal/remove'; @@ -23,7 +21,7 @@ interface RemoveApi { * @throws validation: `StorageValidationErrorCode` - Validation errors thrown * when there is no path or path is empty or path has a leading slash. */ - (input: RemoveInputWithPath): Promise; + (input: RemoveInputWithPath): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. @@ -35,7 +33,7 @@ interface RemoveApi { * @throws validation: `StorageValidationErrorCode` - Validation errors thrown * when there is no key or its empty. */ - (input: RemoveInputWithKey): Promise; + (input: RemoveInputWithKey): Promise; (input: RemoveInput): Promise; } diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index bd6e3d7e463..b241a808a58 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -10,8 +10,6 @@ import { CopyInputWithKey, CopyInputWithPath, CopyOutput, - CopyOutputWithKey, - CopyOutputWithPath, } from '../../types'; import { copy as copyInternal } from '../internal/copy'; @@ -29,7 +27,7 @@ interface Copy { ( contextSpec: AmplifyServer.ContextSpec, input: CopyInputWithPath, - ): Promise; + ): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. @@ -47,7 +45,7 @@ interface Copy { ( contextSpec: AmplifyServer.ContextSpec, input: CopyInputWithKey, - ): Promise; + ): Promise; ( contextSpec: AmplifyServer.ContextSpec, diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts index 9f5af13554f..bca2c7aae98 100644 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/server/getProperties.ts @@ -11,8 +11,6 @@ import { GetPropertiesInputWithKey, GetPropertiesInputWithPath, GetPropertiesOutput, - GetPropertiesOutputWithKey, - GetPropertiesOutputWithPath, } from '../../types'; import { getProperties as getPropertiesInternal } from '../internal/getProperties'; @@ -30,7 +28,7 @@ interface GetProperties { ( contextSpec: AmplifyServer.ContextSpec, input: GetPropertiesInputWithPath, - ): Promise; + ): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. @@ -47,7 +45,7 @@ interface GetProperties { ( contextSpec: AmplifyServer.ContextSpec, input: GetPropertiesInputWithKey, - ): Promise; + ): Promise; ( contextSpec: AmplifyServer.ContextSpec, input: GetPropertiesInput, diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts index 4ac584dc998..fe1a4e5f885 100644 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ b/packages/storage/src/providers/s3/apis/server/list.ts @@ -10,14 +10,10 @@ import { ListAllInputWithPath, ListAllInputWithPrefix, ListAllOutput, - ListAllOutputWithPath, - ListAllOutputWithPrefix, ListPaginateInput, ListPaginateInputWithPath, ListPaginateInputWithPrefix, ListPaginateOutput, - ListPaginateOutputWithPath, - ListPaginateOutputWithPrefix, } from '../../types'; import { list as listInternal } from '../internal/list'; @@ -34,7 +30,7 @@ interface ListApi { ( contextSpec: AmplifyServer.ContextSpec, input: ListPaginateInputWithPath, - ): Promise; + ): Promise; /** * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. * @param input - The `ListAllInputWithPath` object. @@ -46,7 +42,7 @@ interface ListApi { ( contextSpec: AmplifyServer.ContextSpec, input: ListAllInputWithPath, - ): Promise; + ): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. @@ -60,7 +56,7 @@ interface ListApi { ( contextSpec: AmplifyServer.ContextSpec, input?: ListPaginateInputWithPrefix, - ): Promise; + ): Promise; /** * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. @@ -73,7 +69,7 @@ interface ListApi { ( contextSpec: AmplifyServer.ContextSpec, input?: ListAllInputWithPrefix, - ): Promise; + ): Promise; ( contextSpec: AmplifyServer.ContextSpec, input?: ListPaginateInput, diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts index a0f8ef9f6db..0815f1c1c44 100644 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ b/packages/storage/src/providers/s3/apis/server/remove.ts @@ -11,8 +11,6 @@ import { RemoveInputWithKey, RemoveInputWithPath, RemoveOutput, - RemoveOutputWithKey, - RemoveOutputWithPath, } from '../../types'; import { remove as removeInternal } from '../internal/remove'; @@ -29,7 +27,7 @@ interface RemoveApi { ( contextSpec: AmplifyServer.ContextSpec, input: RemoveInputWithPath, - ): Promise; + ): Promise; /** * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. @@ -45,7 +43,7 @@ interface RemoveApi { ( contextSpec: AmplifyServer.ContextSpec, input: RemoveInputWithKey, - ): Promise; + ): Promise; ( contextSpec: AmplifyServer.ContextSpec, input: RemoveInput, diff --git a/packages/storage/src/providers/s3/apis/uploadData/index.ts b/packages/storage/src/providers/s3/apis/uploadData/index.ts index b7ec67e4848..0bf632996a3 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/index.ts @@ -6,8 +6,6 @@ import { UploadDataInputWithKey, UploadDataInputWithPath, UploadDataOutput, - UploadDataOutputWithKey, - UploadDataOutputWithPath, } from '../../types'; import { createUploadTask } from '../../utils'; import { assertValidationError } from '../../../../errors/utils/assertValidationError'; @@ -70,7 +68,7 @@ interface UploadData { * await uploadTask.result; * ``` */ - (input: UploadDataInputWithPath): UploadDataOutputWithPath; + (input: UploadDataInputWithPath): UploadDataOutput; /** * Upload data to the specified S3 object key. By default uses single PUT operation to upload if the payload is less than 5MB. @@ -125,7 +123,7 @@ interface UploadData { * await uploadTask.result; * ``` */ - (input: UploadDataInputWithKey): UploadDataOutputWithKey; + (input: UploadDataInputWithKey): UploadDataOutput; (input: UploadDataInput): UploadDataOutput; } 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 6984498f828..e0100107492 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts @@ -9,7 +9,7 @@ import { resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../../utils'; -import { ItemWithKey, ItemWithPath } from '../../../types/outputs'; +import { ItemWithKeyAndPath } from '../../../types/outputs'; import { DEFAULT_ACCESS_LEVEL, DEFAULT_QUEUE_SIZE, @@ -46,9 +46,7 @@ export const getMultipartUploadHandlers = ( uploadDataInput: UploadDataInput, size?: number, ) => { - let resolveCallback: - | ((value: ItemWithKey | ItemWithPath) => void) - | undefined; + let resolveCallback: ((value: ItemWithKeyAndPath) => void) | undefined; let rejectCallback: ((reason?: any) => void) | undefined; let inProgressUpload: | { @@ -69,7 +67,7 @@ export const getMultipartUploadHandlers = ( // This should be replaced by a special abort reason. However,the support of this API is lagged behind. let isAbortSignalFromPause = false; - const startUpload = async (): Promise => { + const startUpload = async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const resolvedS3Options = await resolveS3ConfigAndInput( Amplify, @@ -220,8 +218,8 @@ export const getMultipartUploadHandlers = ( }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, ...result } - : { path: objectKey, ...result }; + ? { key: objectKey, path: finalKey, ...result } + : { path: finalKey, key: finalKey, ...result }; }; const startUploadWithResumability = () => @@ -238,7 +236,7 @@ export const getMultipartUploadHandlers = ( }); const multipartUploadJob = () => - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { resolveCallback = resolve; rejectCallback = reject; startUploadWithResumability(); diff --git a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts index f9d7bfefe3e..81b2fe6fa94 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts @@ -10,7 +10,7 @@ import { resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../utils'; -import { ItemWithKey, ItemWithPath } from '../../types/outputs'; +import { ItemWithKeyAndPath } from '../../types/outputs'; import { putObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { STORAGE_INPUT_KEY } from '../../utils/constants'; @@ -26,7 +26,7 @@ export const putObjectJob = abortSignal: AbortSignal, totalLength?: number, ) => - async (): Promise => { + async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const { bucket, keyPrefix, s3Config, isObjectLockEnabled, identityId } = await resolveS3ConfigAndInput(Amplify, uploadDataOptions); @@ -75,6 +75,6 @@ export const putObjectJob = }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, ...result } - : { path: objectKey, ...result }; + ? { key: objectKey, path: finalKey, ...result } + : { path: finalKey, key: finalKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 70b1ba46c63..17c0a7dd28d 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -20,29 +20,14 @@ export { } from './options'; export { DownloadDataOutput, - DownloadDataOutputWithKey, - DownloadDataOutputWithPath, GetUrlOutput, UploadDataOutput, - UploadDataOutputWithKey, - UploadDataOutputWithPath, - ListOutputItemWithKey, - ListOutputItemWithPath, ListAllOutput, ListPaginateOutput, - ListAllOutputWithPrefix, - ListAllOutputWithPath, - ListPaginateOutputWithPath, - ListPaginateOutputWithPrefix, GetPropertiesOutput, - GetPropertiesOutputWithKey, - GetPropertiesOutputWithPath, CopyOutput, - CopyOutputWithKey, - CopyOutputWithPath, RemoveOutput, - RemoveOutputWithKey, - RemoveOutputWithPath, + ItemWithKeyAndPath, } from './outputs'; export { CopyInput, diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index 5c7238420c9..5d4b7f484a7 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -1,14 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { StrictUnion } from '@aws-amplify/core/internals/utils'; - import { DownloadTask, StorageDownloadDataOutput, StorageGetUrlOutput, - StorageItemWithKey, - StorageItemWithPath, + StorageItem, StorageListOutput, UploadTask, } from '../../../types'; @@ -28,137 +25,52 @@ export interface ItemBase { } /** - * @deprecated Use {@link ListOutputItemWithPath} instead. - * type for S3 list item with key. - */ -export type ListOutputItemWithKey = Omit; - -/** - * type for S3 list item with path. - */ -export type ListOutputItemWithPath = Omit; - -/** - * @deprecated Use {@link ItemWithPath} instead. + * type for S3 list item. */ -export type ItemWithKey = ItemBase & StorageItemWithKey; +export type ListOutputItem = Omit; -/** - * type for S3 list item with path. - */ -export type ItemWithPath = ItemBase & StorageItemWithPath; +export type ItemWithKeyAndPath = ItemBase & StorageItem; /** - * type for S3 list item. + * Output type for S3 downloadData API. */ -export type ListOutputItem = Omit; - -/** @deprecated Use {@link DownloadDataOutputWithPath} instead. */ -export type DownloadDataOutputWithKey = DownloadTask< - StorageDownloadDataOutput ->; -export type DownloadDataOutputWithPath = DownloadTask< - StorageDownloadDataOutput +export type DownloadDataOutput = DownloadTask< + StorageDownloadDataOutput >; /** - * Output type for S3 downloadData API. + * Output type for S3 uploadData API. */ -export type DownloadDataOutput = - | DownloadDataOutputWithKey - | DownloadDataOutputWithPath; +export type UploadDataOutput = UploadTask; /** * Output type for S3 getUrl API. */ export type GetUrlOutput = StorageGetUrlOutput; -/** @deprecated Use {@link UploadDataOutputWithPath} instead. */ -export type UploadDataOutputWithKey = UploadTask; -export type UploadDataOutputWithPath = UploadTask; - -/** - * Output type for S3 uploadData API. - */ -export type UploadDataOutput = - | UploadDataOutputWithKey - | UploadDataOutputWithPath; - -/** @deprecated Use {@link GetPropertiesOutputWithPath} instead. */ -export type GetPropertiesOutputWithKey = ItemWithKey; -export type GetPropertiesOutputWithPath = ItemWithPath; - /** * Output type for S3 getProperties API. */ -export type GetPropertiesOutput = - | GetPropertiesOutputWithKey - | GetPropertiesOutputWithPath; - -/** - * Output type for S3 list API. Lists all bucket objects. - */ -export type ListAllOutput = StrictUnion< - ListAllOutputWithPath | ListAllOutputWithPrefix ->; +export type GetPropertiesOutput = ItemWithKeyAndPath; /** - * Output type for S3 list API. Lists bucket objects with pagination. + * Output type for S3 Copy API. */ -export type ListPaginateOutput = StrictUnion< - ListPaginateOutputWithPath | ListPaginateOutputWithPrefix ->; +export type CopyOutput = Pick; /** - * @deprecated Use {@link ListAllOutputWithPath} instead. - * Output type for S3 list API. Lists all bucket objects. + * Output type for S3 remove API. */ -export type ListAllOutputWithPrefix = StorageListOutput; +export type RemoveOutput = Pick; /** * Output type for S3 list API. Lists all bucket objects. */ -export type ListAllOutputWithPath = StorageListOutput; +export type ListAllOutput = StorageListOutput; /** - * @deprecated Use {@link ListPaginateOutputWithPath} instead. * Output type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateOutputWithPrefix = - StorageListOutput & { - nextToken?: string; - }; - -/** - * Output type for S3 list API. Lists bucket objects with pagination. - */ -export type ListPaginateOutputWithPath = - StorageListOutput & { - nextToken?: string; - }; - -/** - * @deprecated Use {@link CopyOutputWithPath} instead. - */ -export type CopyOutputWithKey = Pick; -export type CopyOutputWithPath = Pick; - -export type CopyOutput = StrictUnion; - -/** - * Output type for S3 remove API. - */ -export type RemoveOutput = StrictUnion< - RemoveOutputWithKey | RemoveOutputWithPath ->; - -/** - * @deprecated Use {@link RemoveOutputWithPath} instead. - * Output helper type with key for S3 remove API. - */ -export type RemoveOutputWithKey = Pick; - -/** - * Output helper type with path for S3 remove API. - */ -export type RemoveOutputWithPath = Pick; +export type ListPaginateOutput = StorageListOutput & { + nextToken?: string; +}; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 317fa20104c..311922811a2 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -32,8 +32,6 @@ export { } from './options'; export { StorageItem, - StorageItemWithKey, - StorageItemWithPath, StorageListOutput, StorageDownloadDataOutput, StorageGetUrlOutput, diff --git a/packages/storage/src/types/outputs.ts b/packages/storage/src/types/outputs.ts index 0fbf572988d..aa947602ca5 100644 --- a/packages/storage/src/types/outputs.ts +++ b/packages/storage/src/types/outputs.ts @@ -27,25 +27,14 @@ export interface StorageItemBase { metadata?: Record; } -/** @deprecated Use {@link StorageItemWithPath} instead. */ -export type StorageItemWithKey = StorageItemBase & { - /** - * Key of the object. - */ - key: string; -}; - -export type StorageItemWithPath = StorageItemBase & { - /** - * Path of the object. - */ - path: string; -}; - /** * A storage item can be identified either by a key or a path. */ -export type StorageItem = StorageItemWithKey | StorageItemWithPath; +export type StorageItem = StorageItemBase & { + /** @deprecated This may be removed in next major version */ + key: string; + path: string; +}; export type StorageDownloadDataOutput = Item & { body: ResponseBodyMixin; From dd48dddfffc5a5031915a6a320842d0e4f339bb3 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 17 Apr 2024 08:58:38 -0700 Subject: [PATCH 21/33] chore: fix linter errors in parseGen2 config tests --- .../__tests__/initSingleton.test.ts | 126 +++--- .../__tests__/parseAmplifyOutputs.test.ts | 401 ++++++++---------- 2 files changed, 249 insertions(+), 278 deletions(-) diff --git a/packages/aws-amplify/__tests__/initSingleton.test.ts b/packages/aws-amplify/__tests__/initSingleton.test.ts index 2ee74b4d509..dd2973b8f9d 100644 --- a/packages/aws-amplify/__tests__/initSingleton.test.ts +++ b/packages/aws-amplify/__tests__/initSingleton.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 @@ -7,13 +8,13 @@ import { ResourcesConfig, defaultStorage, } from '@aws-amplify/core'; +import { AmplifyOutputs } from '@aws-amplify/core/internals/utils'; import { cognitoCredentialsProvider, cognitoUserPoolsTokenProvider, } from '../src/auth/cognito'; import { Amplify } from '../src'; -import { AmplifyOutputs } from '@aws-amplify/core/internals/utils'; jest.mock('@aws-amplify/core'); jest.mock('../src/auth/cognito', () => ({ @@ -72,94 +73,89 @@ describe('initSingleton (DefaultAmplify)', () => { describe('Amplify configure with AmplifyOutputs format', () => { it('should use AmplifyOutputs config type', () => { const amplifyOutputs: AmplifyOutputs = { - 'version': '1', - 'storage': { - 'aws_region': 'us-east-1', - 'bucket_name': 'my-bucket-name' + version: '1', + storage: { + aws_region: 'us-east-1', + bucket_name: 'my-bucket-name', }, - 'auth': { - 'user_pool_id': 'us-east-1:', - 'user_pool_client_id': 'xxxx', - 'aws_region': 'us-east-1', - 'identity_pool_id': 'test' + auth: { + user_pool_id: 'us-east-1:', + user_pool_client_id: 'xxxx', + aws_region: 'us-east-1', + identity_pool_id: 'test', }, - 'analytics': { + analytics: { amazon_pinpoint: { app_id: 'xxxxx', - aws_region: 'us-east-1' - } + aws_region: 'us-east-1', + }, }, - 'geo': { + geo: { aws_region: 'us-east-1', maps: { - items: { 'map1': { name: 'map1', style: 'color' } }, - default: 'map1' + items: { map1: { name: 'map1', style: 'color' } }, + default: 'map1', }, geofence_collections: { items: ['a', 'b', 'c'], - default: 'a' + default: 'a', }, search_indices: { items: ['a', 'b', 'c'], - default: 'a' - } - } + default: 'a', + }, + }, }; Amplify.configure(amplifyOutputs); - expect(AmplifySingleton.configure).toHaveBeenCalledWith({ - Storage: { - S3: { - bucket: 'my-bucket-name', - region: 'us-east-1' + expect(AmplifySingleton.configure).toHaveBeenCalledWith( + { + Storage: { + S3: { + bucket: 'my-bucket-name', + region: 'us-east-1', + }, }, - }, - Auth: { - Cognito: { - identityPoolId: 'test', - userPoolId: 'us-east-1:', - userPoolClientId: 'xxxx' - } - }, - Analytics: { - Pinpoint: { - appId: 'xxxxx', - region: 'us-east-1', + Auth: { + Cognito: { + identityPoolId: 'test', + userPoolId: 'us-east-1:', + userPoolClientId: 'xxxx', + }, }, - }, - Geo: { - LocationService: { - 'geofenceCollections': { - 'default': 'a', - 'items': [ - 'a', - 'b', - 'c', - ], + Analytics: { + Pinpoint: { + appId: 'xxxxx', + region: 'us-east-1', }, - 'maps': { - 'default': 'map1', - 'items': { - 'map1': { - 'name': 'map1', - 'style': 'color' + }, + Geo: { + LocationService: { + geofenceCollections: { + default: 'a', + items: ['a', 'b', 'c'], + }, + maps: { + default: 'map1', + items: { + map1: { + name: 'map1', + style: 'color', + }, }, }, - }, - 'region': 'us-east-1', - 'searchIndices': { - 'default': 'a', - 'items': [ - 'a', - 'b', - 'c', - ], + region: 'us-east-1', + searchIndices: { + default: 'a', + items: ['a', 'b', 'c'], + }, }, }, - } - }, expect.anything()); - }) + }, + expect.anything(), + ); + }); }); describe('DefaultAmplify.configure()', () => { diff --git a/packages/core/__tests__/parseAmplifyOutputs.test.ts b/packages/core/__tests__/parseAmplifyOutputs.test.ts index 39d2012565b..3efca756732 100644 --- a/packages/core/__tests__/parseAmplifyOutputs.test.ts +++ b/packages/core/__tests__/parseAmplifyOutputs.test.ts @@ -1,97 +1,90 @@ +/* eslint-disable camelcase */ import { AmplifyOutputs, parseAmplifyOutputs } from '../src/libraryUtils'; -import amplifyOutputs from './amplify_outputs.json'; + +import mockAmplifyOutputs from './amplify_outputs.json'; + describe('parseAmplifyOutputs tests', () => { describe('auth tests', () => { - it('should parse from amplify-outputs.json', async () => { - const result = parseAmplifyOutputs(amplifyOutputs); + const result = parseAmplifyOutputs(mockAmplifyOutputs); expect(result).toEqual({ - "API": { - "GraphQL": { - "apiKey": "non", - "defaultAuthMode": "apiKey", - "endpoint": "dolore dolor do cillum nulla", - "modelIntrospection": undefined, - "region": "regasd", + API: { + GraphQL: { + apiKey: 'non', + defaultAuthMode: 'apiKey', + endpoint: 'dolore dolor do cillum nulla', + modelIntrospection: undefined, + region: 'regasd', }, }, - "Auth": { - "Cognito": { - "allowGuestAccess": true, - "identityPoolId": "Lorem", - "loginWith": { - "email": true, - "oauth": { - "domain": "proident dolore do mollit ad", - "providers": [ - "Facebook", - "Apple", - "Google", - ], - "redirectSignIn": [ - "Duis", - "ipsum velit in dolore", - ], - "redirectSignOut": [ - "Excepteur pariatur cillum officia", - "incididunt in Ut Excepteur commodo", - ], - "responseType": "token", - "scopes": [ - "incididunt proident", + Auth: { + Cognito: { + allowGuestAccess: true, + identityPoolId: 'Lorem', + loginWith: { + email: true, + oauth: { + domain: 'proident dolore do mollit ad', + providers: ['Facebook', 'Apple', 'Google'], + redirectSignIn: ['Duis', 'ipsum velit in dolore'], + redirectSignOut: [ + 'Excepteur pariatur cillum officia', + 'incididunt in Ut Excepteur commodo', ], + responseType: 'token', + scopes: ['incididunt proident'], }, - "phone": true, + phone: true, }, - "mfa": { - "smsEnabled": true, - "status": "optional", - "totpEnabled": true, + mfa: { + smsEnabled: true, + status: 'optional', + totpEnabled: true, }, - "userAttributes": { - "address": { - "required": true, + userAttributes: { + address: { + required: true, }, - "email": { - "required": true, + email: { + required: true, }, - "family_name": { - "required": true, + family_name: { + required: true, }, - "locale": { - "required": true, + locale: { + required: true, }, - "sub": { - "required": true, + sub: { + required: true, }, }, - "userPoolClientId": "voluptate", - "userPoolId": "sit velit dolor magna est", + userPoolClientId: 'voluptate', + userPoolId: 'sit velit dolor magna est', }, }, - "Geo": { - "LocationService": { - "geofenceCollections": { - "default": "ullamco incididunt aliquip", - "items": [ - "fugiat ea irure dolor", - "Ut", - "culpa ut enim exercitation", - "labore", - "ex pariatur est ullamco", + Geo: { + LocationService: { + geofenceCollections: { + default: 'ullamco incididunt aliquip', + items: [ + 'fugiat ea irure dolor', + 'Ut', + 'culpa ut enim exercitation', + 'labore', + 'ex pariatur est ullamco', ], }, - "maps": undefined, - "region": "tempor", - "searchIndices": { - "default": "exercitation fugiat ut dolor sed", - "items": [ - "commodo Lorem", - "reprehenderit consequat", - "amet", - "aliquip deserunt", - "ea dolor in proident", + maps: undefined, + region: 'tempor', + searchIndices: { + default: 'exercitation fugiat ut dolor sed', + items: [ + 'commodo Lorem', + 'reprehenderit consequat', + 'amet', + 'aliquip deserunt', + 'ea dolor in proident', ], }, }, @@ -101,13 +94,13 @@ describe('parseAmplifyOutputs tests', () => { it('should parse auth happy path (all enabled)', () => { const amplifyOutputs = { - 'version': '1', - 'auth': { - 'user_pool_id': 'us-east-1:', - 'user_pool_client_id': 'xxxx', - 'aws_region': 'us-east-1', - 'identity_pool_id': 'test', - 'oauth': { + version: '1', + auth: { + user_pool_id: 'us-east-1:', + user_pool_client_id: 'xxxx', + aws_region: 'us-east-1', + identity_pool_id: 'test', + oauth: { domain: 'https://cognito.com...', redirect_sign_in_uri: ['http://localhost:3000/welcome'], redirect_sign_out_uri: ['http://localhost:3000/come-back-soon'], @@ -115,69 +108,60 @@ describe('parseAmplifyOutputs tests', () => { scopes: ['profile', '...'], identity_providers: ['GOOGLE'], }, - 'password_policy': { - 'min_length': 8, - 'require_lowercase': true, - 'require_uppercase': true, - 'require_symbols': true, - 'require_numbers': true + password_policy: { + min_length: 8, + require_lowercase: true, + require_uppercase: true, + require_symbols: true, + require_numbers: true, }, - 'standard_required_attributes': ['email'], - 'username_attributes': ['email'], - 'user_verification_types': ['email'], - 'unauthenticated_identities_enabled': true, - 'mfa_configuration': 'OPTIONAL', - 'mfa_methods': ['SMS'] + standard_required_attributes: ['email'], + username_attributes: ['email'], + user_verification_types: ['email'], + unauthenticated_identities_enabled: true, + mfa_configuration: 'OPTIONAL', + mfa_methods: ['SMS'], }, }; const result = parseAmplifyOutputs(amplifyOutputs); expect(result).toEqual({ - 'Auth': { - 'Cognito': { - 'allowGuestAccess': true, - 'identityPoolId': 'test', - 'mfa': { - 'smsEnabled': true, - 'status': 'optional', - 'totpEnabled': false, + Auth: { + Cognito: { + allowGuestAccess: true, + identityPoolId: 'test', + mfa: { + smsEnabled: true, + status: 'optional', + totpEnabled: false, }, - 'passwordFormat': { - 'minLength': 8, - 'requireLowercase': true, - 'requireNumbers': true, - 'requireSpecialCharacters': true, - 'requireUppercase': true, + passwordFormat: { + minLength: 8, + requireLowercase: true, + requireNumbers: true, + requireSpecialCharacters: true, + requireUppercase: true, }, - 'userAttributes': { - 'email': { - 'required': true, + userAttributes: { + email: { + required: true, }, }, - 'userPoolClientId': 'xxxx', - 'userPoolId': 'us-east-1:', - 'loginWith': { - 'email': true, - 'oauth': { - 'domain': 'https://cognito.com...', - 'providers': [ - 'Google', - ], - 'redirectSignIn': [ - 'http://localhost:3000/welcome', - ], - 'redirectSignOut': [ - 'http://localhost:3000/come-back-soon', - ], - 'responseType': 'code', - 'scopes': [ - 'profile', - '...', - ] - } - } - } - } + userPoolClientId: 'xxxx', + userPoolId: 'us-east-1:', + loginWith: { + email: true, + oauth: { + domain: 'https://cognito.com...', + providers: ['Google'], + redirectSignIn: ['http://localhost:3000/welcome'], + redirectSignOut: ['http://localhost:3000/come-back-soon'], + responseType: 'code', + scopes: ['profile', '...'], + }, + }, + }, + }, }); }); }); @@ -185,11 +169,11 @@ describe('parseAmplifyOutputs tests', () => { describe('storage tests', () => { it('should parse storage happy path', () => { const amplifyOutputs: AmplifyOutputs = { - 'version': '1', - 'storage': { + version: '1', + storage: { aws_region: 'us-west-2', - bucket_name: 'storage-bucket-test' - } + bucket_name: 'storage-bucket-test', + }, }; const result = parseAmplifyOutputs(amplifyOutputs); @@ -197,153 +181,144 @@ describe('parseAmplifyOutputs tests', () => { expect(result).toEqual({ Storage: { S3: { - 'bucket': 'storage-bucket-test', - 'region': 'us-west-2', - } - } - }) - }) + bucket: 'storage-bucket-test', + region: 'us-west-2', + }, + }, + }); + }); }); describe('analytics tests', () => { it('should parse all providers', () => { const amplifyOutputs: AmplifyOutputs = { - 'version': '1', - 'analytics': { + version: '1', + analytics: { amazon_pinpoint: { app_id: 'xxxxx', - aws_region: 'us-east-1' - } - } + aws_region: 'us-east-1', + }, + }, }; const result = parseAmplifyOutputs(amplifyOutputs); expect(result).toEqual({ - 'Analytics': { - 'Pinpoint': { - 'appId': 'xxxxx', - 'region': 'us-east-1', - } - } - }) + Analytics: { + Pinpoint: { + appId: 'xxxxx', + region: 'us-east-1', + }, + }, + }); }); }); describe('geo tests', () => { it('should parse LocationService config', () => { const amplifyOutputs: AmplifyOutputs = { - 'version': '1', - 'geo': { + version: '1', + geo: { aws_region: 'us-east-1', maps: { items: { - 'map1': { name: 'map1', style: 'color' } + map1: { name: 'map1', style: 'color' }, }, - default: 'map1' + default: 'map1', }, geofence_collections: { items: ['a', 'b', 'c'], - default: 'a' + default: 'a', }, search_indices: { items: ['a', 'b', 'c'], - default: 'a' - } - } + default: 'a', + }, + }, }; const result = parseAmplifyOutputs(amplifyOutputs); expect(result).toEqual({ - 'Geo': { - 'LocationService': { - 'geofenceCollections': { - 'default': 'a', - 'items': [ - 'a', - 'b', - 'c', - ], + Geo: { + LocationService: { + geofenceCollections: { + default: 'a', + items: ['a', 'b', 'c'], }, - 'maps': { - 'default': 'map1', - 'items': { - 'map1': { + maps: { + default: 'map1', + items: { + map1: { style: 'color', - name: 'map1' + name: 'map1', }, }, }, - 'region': 'us-east-1', - 'searchIndices': { - 'default': 'a', - 'items': [ - 'a', - 'b', - 'c', - ], + region: 'us-east-1', + searchIndices: { + default: 'a', + items: ['a', 'b', 'c'], }, }, - } - }) - }) - + }, + }); + }); }); describe('data tests', () => { it('should configure data', () => { const amplifyOutputs: AmplifyOutputs = { - 'version': '1', - 'data': { + version: '1', + data: { aws_region: 'us-west-2', url: 'https://api.appsyncaws.com/graphql', authorization_types: ['API_KEY'], default_authorization_type: 'API_KEY', - api_key: 'da-xxxx' - } + api_key: 'da-xxxx', + }, }; const result = parseAmplifyOutputs(amplifyOutputs); expect(result).toEqual({ API: { - 'GraphQL': { + GraphQL: { endpoint: 'https://api.appsyncaws.com/graphql', region: 'us-west-2', apiKey: 'da-xxxx', - defaultAuthMode: 'apiKey' - } - } + defaultAuthMode: 'apiKey', + }, + }, }); }); describe('notifications tests', () => { it('should configure notifications', () => { const amplifyOutputs: AmplifyOutputs = { - 'version': '1', - 'notifications': { + version: '1', + notifications: { aws_region: 'us-west-2', amazon_pinpoint_app_id: 'appid123', channels: ['APNS', 'EMAIL', 'FCM', 'IN_APP_MESSAGING', 'SMS'], - } + }, }; const result = parseAmplifyOutputs(amplifyOutputs); expect(result).toEqual({ - 'Notifications': { - 'InAppMessaging': { - 'Pinpoint': { - 'appId': 'appid123', - 'region': 'us-west-2', + Notifications: { + InAppMessaging: { + Pinpoint: { + appId: 'appid123', + region: 'us-west-2', }, }, - 'PushNotification': { - 'Pinpoint': { - 'appId': 'appid123', - 'region': 'us-west-2', + PushNotification: { + Pinpoint: { + appId: 'appid123', + region: 'us-west-2', }, - } - } + }, + }, }); }); - }) - }) -}) + }); + }); +}); From f797dc539f57a55a325b227d8205813b122d7789 Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Wed, 17 Apr 2024 13:45:32 -0500 Subject: [PATCH 22/33] fix: Fix SSR & AmplifyOutput types when using Gen2 configuration files (#13247) --- .../api/generateServerClient.test.ts | 11 +-- .../__tests__/createServerRunner.test.ts | 23 +++--- .../__tests__/utils/getAmplifyConfig.test.ts | 46 ------------ .../src/api/createServerRunnerForAPI.ts | 5 +- .../src/api/generateServerClient.ts | 8 ++- .../adapter-nextjs/src/createServerRunner.ts | 5 +- .../adapter-nextjs/src/types/NextServer.ts | 4 +- .../src/utils/getAmplifyConfig.ts | 13 ---- packages/adapter-nextjs/src/utils/index.ts | 1 - packages/aws-amplify/jest.config.js | 2 +- packages/aws-amplify/package.json | 56 +++++++-------- packages/aws-amplify/src/adapterCore/index.ts | 5 +- packages/aws-amplify/src/initSingleton.ts | 14 +--- packages/aws-amplify/src/utils/index.ts | 2 +- .../utils/parseAmplifyConfig.test.ts | 72 +++++++++++++++++++ packages/core/src/libraryUtils.ts | 1 + packages/core/src/parseAmplifyOutputs.ts | 3 +- packages/core/src/singleton/Amplify.ts | 13 ++-- .../src/singleton/AmplifyOutputs/types.ts | 4 +- packages/core/src/singleton/types.ts | 2 + packages/core/src/utils/parseAmplifyConfig.ts | 26 +++++++ packages/interactions/package.json | 6 +- 22 files changed, 182 insertions(+), 140 deletions(-) delete mode 100644 packages/adapter-nextjs/__tests__/utils/getAmplifyConfig.test.ts delete mode 100644 packages/adapter-nextjs/src/utils/getAmplifyConfig.ts create mode 100644 packages/core/__tests__/utils/parseAmplifyConfig.test.ts create mode 100644 packages/core/src/utils/parseAmplifyConfig.ts diff --git a/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts b/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts index dfc038de44a..3cd577d8eaa 100644 --- a/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts +++ b/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts @@ -1,10 +1,10 @@ import { ResourcesConfig } from '@aws-amplify/core'; +import { parseAmplifyConfig } from '@aws-amplify/core/internals/utils'; import { generateServerClientUsingCookies, generateServerClientUsingReqRes, } from '../../src/api'; import { - getAmplifyConfig, createRunWithAmplifyServerContext, } from '../../src/utils'; import { NextApiRequestMock, NextApiResponseMock } from '../mocks/headers'; @@ -33,13 +33,16 @@ const mockAmplifyConfig: ResourcesConfig = { jest.mock('../../src/utils', () => ({ createRunWithAmplifyServerContext: jest.fn(() => jest.fn()), - getAmplifyConfig: jest.fn(() => mockAmplifyConfig), createCookieStorageAdapterFromNextServerContext: jest.fn(), })); +jest.mock('@aws-amplify/core/internals/utils', () => ({ + ...jest.requireActual('@aws-amplify/core/internals/utils'), + parseAmplifyConfig: jest.fn(() => mockAmplifyConfig), +})); jest.mock('aws-amplify/adapter-core'); -const mockGetAmplifyConfig = getAmplifyConfig as jest.Mock; +const mockParseAmplifyConfig = parseAmplifyConfig as jest.Mock; const mockCreateRunWithAmplifyServerContext = createRunWithAmplifyServerContext as jest.Mock; @@ -76,7 +79,7 @@ describe('generateServerClient', () => { it('should call getAmlifyConfig', async () => { generateServerClientUsingReqRes({ config: mockAmplifyConfig }); - expect(mockGetAmplifyConfig).toHaveBeenCalled(); + expect(mockParseAmplifyConfig).toHaveBeenCalled(); }); // TODO: figure out proper mocks and unskip diff --git a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts index 17ff383720f..3b67894077b 100644 --- a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts +++ b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts @@ -30,7 +30,7 @@ jest.mock( describe('createServerRunner', () => { let createServerRunner: any; - const mockParseAWSExports = jest.fn(); + const mockParseAmplifyConfig = jest.fn(); const mockCreateAWSCredentialsAndIdentityIdProvider = jest.fn(); const mockCreateKeyValueStorageFromCookieStorageAdapter = jest.fn(); const mockCreateUserPoolsTokenProvider = jest.fn(); @@ -47,23 +47,23 @@ describe('createServerRunner', () => { runWithAmplifyServerContext: mockRunWithAmplifyServerContextCore, })); jest.doMock('@aws-amplify/core/internals/utils', () => ({ - parseAWSExports: mockParseAWSExports, + parseAmplifyConfig: mockParseAmplifyConfig, })); createServerRunner = require('../src').createServerRunner; }); afterEach(() => { - mockParseAWSExports.mockClear(); + mockParseAmplifyConfig.mockClear(); mockCreateAWSCredentialsAndIdentityIdProvider.mockClear(); mockCreateKeyValueStorageFromCookieStorageAdapter.mockClear(); mockCreateUserPoolsTokenProvider.mockClear(); mockRunWithAmplifyServerContextCore.mockClear(); }); - it('calls parseAWSExports when the config object is imported from amplify configuration file', () => { + it('calls parseAmplifyConfig when the config object is imported from amplify configuration file', () => { createServerRunner({ config: { aws_project_region: 'us-west-2' } }); - expect(mockParseAWSExports).toHaveBeenCalled(); + expect(mockParseAmplifyConfig).toHaveBeenCalled(); }); it('returns runWithAmplifyServerContext function', () => { @@ -76,7 +76,7 @@ describe('createServerRunner', () => { describe('runWithAmplifyServerContext', () => { describe('when amplifyConfig.Auth is not defined', () => { it('should call runWithAmplifyServerContextCore without Auth library options', () => { - const mockAmplifyConfig: ResourcesConfig = { + const mockAmplifyConfigAnalytics: ResourcesConfig = { Analytics: { Pinpoint: { appId: 'app-id', @@ -84,13 +84,16 @@ describe('createServerRunner', () => { }, }, }; + + mockParseAmplifyConfig.mockReturnValue(mockAmplifyConfigAnalytics); + const { runWithAmplifyServerContext } = createServerRunner({ - config: mockAmplifyConfig, + config: mockAmplifyConfigAnalytics, }); const operation = jest.fn(); runWithAmplifyServerContext({ operation, nextServerContext: null }); expect(mockRunWithAmplifyServerContextCore).toHaveBeenCalledWith( - mockAmplifyConfig, + mockAmplifyConfigAnalytics, {}, operation, ); @@ -98,6 +101,10 @@ describe('createServerRunner', () => { }); describe('when amplifyConfig.Auth is defined', () => { + beforeEach(() => { + mockParseAmplifyConfig.mockReturnValue(mockAmplifyConfig); + }) + describe('when nextServerContext is null (opt-in unauthenticated role)', () => { it('should create auth providers with sharedInMemoryStorage', () => { const { runWithAmplifyServerContext } = createServerRunner({ diff --git a/packages/adapter-nextjs/__tests__/utils/getAmplifyConfig.test.ts b/packages/adapter-nextjs/__tests__/utils/getAmplifyConfig.test.ts deleted file mode 100644 index 14532cec5b3..00000000000 --- a/packages/adapter-nextjs/__tests__/utils/getAmplifyConfig.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { getAmplifyConfig } from '../../src/utils/getAmplifyConfig'; - -describe('getAmplifyConfig', () => { - const mockLegacyConfig = { - aws_project_region: 'us-west-2', - aws_cognito_identity_pool_id: '123', - aws_cognito_region: 'aws_cognito_region', - aws_user_pools_id: 'abc', - aws_user_pools_web_client_id: 'def', - oauth: {}, - aws_cognito_username_attributes: [], - aws_cognito_social_providers: [], - aws_cognito_signup_attributes: [], - aws_cognito_mfa_configuration: 'OFF', - aws_cognito_mfa_types: ['SMS'], - aws_cognito_password_protection_settings: { - passwordPolicyMinLength: 8, - passwordPolicyCharacters: [], - }, - aws_cognito_verification_mechanisms: ['PHONE_NUMBER'], - aws_user_files_s3_bucket: 'bucket', - aws_user_files_s3_bucket_region: 'us-east-1', - }; - const mockAmplifyConfig = { - Auth: { - Cognito: { - identityPoolId: '123', - userPoolId: 'abc', - userPoolClientId: 'def', - }, - }, - Storage: { - S3: { - bucket: 'bucket', - region: 'us-east-1', - }, - }, - }; - - it('returns config object that conforms to ResourcesConfig', () => { - expect(getAmplifyConfig(mockLegacyConfig)).toMatchObject(mockAmplifyConfig); - }); -}); diff --git a/packages/adapter-nextjs/src/api/createServerRunnerForAPI.ts b/packages/adapter-nextjs/src/api/createServerRunnerForAPI.ts index 94434a8a481..3c5ae6ad97a 100644 --- a/packages/adapter-nextjs/src/api/createServerRunnerForAPI.ts +++ b/packages/adapter-nextjs/src/api/createServerRunnerForAPI.ts @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { ResourcesConfig } from '@aws-amplify/core'; +import { parseAmplifyConfig } from '@aws-amplify/core/internals/utils'; -import { createRunWithAmplifyServerContext, getAmplifyConfig } from '../utils'; +import { createRunWithAmplifyServerContext } from '../utils'; import { NextServer } from '../types'; export const createServerRunnerForAPI = ({ @@ -11,7 +12,7 @@ export const createServerRunnerForAPI = ({ }: NextServer.CreateServerRunnerInput): NextServer.CreateServerRunnerOutput & { resourcesConfig: ResourcesConfig; } => { - const amplifyConfig = getAmplifyConfig(config); + const amplifyConfig = parseAmplifyConfig(config); return { runWithAmplifyServerContext: createRunWithAmplifyServerContext({ diff --git a/packages/adapter-nextjs/src/api/generateServerClient.ts b/packages/adapter-nextjs/src/api/generateServerClient.ts index 7d8723fd716..e1c5ab09816 100644 --- a/packages/adapter-nextjs/src/api/generateServerClient.ts +++ b/packages/adapter-nextjs/src/api/generateServerClient.ts @@ -11,10 +11,12 @@ import { V6ClientSSRCookies, V6ClientSSRRequest, } from '@aws-amplify/api-graphql'; -import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; +import { + GraphQLAuthMode, + parseAmplifyConfig, +} from '@aws-amplify/core/internals/utils'; import { NextServer } from '../types'; -import { getAmplifyConfig } from '../utils'; import { createServerRunnerForAPI } from './createServerRunnerForAPI'; @@ -98,7 +100,7 @@ export function generateServerClientUsingCookies< export function generateServerClientUsingReqRes< T extends Record = never, >({ config, authMode, authToken }: ReqClientParams): V6ClientSSRRequest { - const amplifyConfig = getAmplifyConfig(config); + const amplifyConfig = parseAmplifyConfig(config); return generateClient({ config: amplifyConfig, diff --git a/packages/adapter-nextjs/src/createServerRunner.ts b/packages/adapter-nextjs/src/createServerRunner.ts index 78d419af99b..576356fba3e 100644 --- a/packages/adapter-nextjs/src/createServerRunner.ts +++ b/packages/adapter-nextjs/src/createServerRunner.ts @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { ResourcesConfig } from 'aws-amplify'; +import { parseAmplifyConfig } from '@aws-amplify/core/internals/utils'; -import { createRunWithAmplifyServerContext, getAmplifyConfig } from './utils'; +import { createRunWithAmplifyServerContext } from './utils'; import { NextServer } from './types'; /** @@ -27,7 +28,7 @@ import { NextServer } from './types'; export const createServerRunner: NextServer.CreateServerRunner = ({ config, }) => { - const amplifyConfig = getAmplifyConfig(config); + const amplifyConfig = parseAmplifyConfig(config); return { runWithAmplifyServerContext: createRunWithAmplifyServerContext({ diff --git a/packages/adapter-nextjs/src/types/NextServer.ts b/packages/adapter-nextjs/src/types/NextServer.ts index 107bab823e7..5c3d093b795 100644 --- a/packages/adapter-nextjs/src/types/NextServer.ts +++ b/packages/adapter-nextjs/src/types/NextServer.ts @@ -4,7 +4,7 @@ import { GetServerSidePropsContext as NextGetServerSidePropsContext } from 'next'; import { NextRequest, NextResponse } from 'next/server.js'; import { cookies } from 'next/headers.js'; -import { LegacyConfig } from 'aws-amplify/adapter-core'; +import { AmplifyOutputs, LegacyConfig } from 'aws-amplify/adapter-core'; import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { ResourcesConfig } from '@aws-amplify/core'; @@ -74,7 +74,7 @@ export declare namespace NextServer { ) => Promise; export interface CreateServerRunnerInput { - config: ResourcesConfig | LegacyConfig; + config: ResourcesConfig | LegacyConfig | AmplifyOutputs; } export interface CreateServerRunnerOutput { diff --git a/packages/adapter-nextjs/src/utils/getAmplifyConfig.ts b/packages/adapter-nextjs/src/utils/getAmplifyConfig.ts deleted file mode 100644 index a8ab9f1d22b..00000000000 --- a/packages/adapter-nextjs/src/utils/getAmplifyConfig.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { ResourcesConfig } from 'aws-amplify'; -import { LegacyConfig } from 'aws-amplify/adapter-core'; -import { parseAWSExports } from '@aws-amplify/core/internals/utils'; - -export const getAmplifyConfig = ( - config: ResourcesConfig | LegacyConfig, -): ResourcesConfig => - Object.keys(config).some(key => key.startsWith('aws_')) - ? parseAWSExports(config) - : (config as ResourcesConfig); diff --git a/packages/adapter-nextjs/src/utils/index.ts b/packages/adapter-nextjs/src/utils/index.ts index e5fd9bb5f87..68ab6cdf55c 100644 --- a/packages/adapter-nextjs/src/utils/index.ts +++ b/packages/adapter-nextjs/src/utils/index.ts @@ -1,5 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { getAmplifyConfig } from './getAmplifyConfig'; export { createRunWithAmplifyServerContext } from './createRunWithAmplifyServerContext'; diff --git a/packages/aws-amplify/jest.config.js b/packages/aws-amplify/jest.config.js index 7365a413e7c..5254f524623 100644 --- a/packages/aws-amplify/jest.config.js +++ b/packages/aws-amplify/jest.config.js @@ -3,7 +3,7 @@ module.exports = { coverageThreshold: { global: { branches: 85, - functions: 66, + functions: 65.5, lines: 90, statements: 91, }, diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 8706c23c601..df7111b459f 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -293,31 +293,31 @@ "name": "[Analytics] record (Pinpoint)", "path": "./dist/esm/analytics/index.mjs", "import": "{ record }", - "limit": "16.50 kB" + "limit": "17.02 kB" }, { "name": "[Analytics] record (Kinesis)", "path": "./dist/esm/analytics/kinesis/index.mjs", "import": "{ record }", - "limit": "45.50 kB" + "limit": "48.56 kB" }, { "name": "[Analytics] record (Kinesis Firehose)", "path": "./dist/esm/analytics/kinesis-firehose/index.mjs", "import": "{ record }", - "limit": "42.50 kB" + "limit": "45.68 kB" }, { "name": "[Analytics] record (Personalize)", "path": "./dist/esm/analytics/personalize/index.mjs", "import": "{ record }", - "limit": "46.50 kB" + "limit": "49.50 kB" }, { "name": "[Analytics] identifyUser (Pinpoint)", "path": "./dist/esm/analytics/index.mjs", "import": "{ identifyUser }", - "limit": "15.00 kB" + "limit": "15.52 kB" }, { "name": "[Analytics] enable", @@ -335,7 +335,7 @@ "name": "[API] generateClient (AppSync)", "path": "./dist/esm/api/index.mjs", "import": "{ generateClient }", - "limit": "38.00 kB" + "limit": "38.58 kB" }, { "name": "[API] REST API handlers", @@ -353,13 +353,13 @@ "name": "[Auth] resetPassword (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ resetPassword }", - "limit": "9.02 kB" + "limit": "12.44 kB" }, { "name": "[Auth] confirmResetPassword (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmResetPassword }", - "limit": "9.00 kB" + "limit": "12.39 kB" }, { "name": "[Auth] signIn (Cognito)", @@ -371,7 +371,7 @@ "name": "[Auth] resendSignUpCode (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ resendSignUpCode }", - "limit": "9.00 kB" + "limit": "12.40 kB" }, { "name": "[Auth] confirmSignUp (Cognito)", @@ -383,31 +383,31 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "26.40 kB" + "limit": "27.63 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ updateMFAPreference }", - "limit": "8.6 kB" + "limit": "11.74 kB" }, { "name": "[Auth] fetchMFAPreference (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ fetchMFAPreference }", - "limit": "8.35 kB" + "limit": "11.78 kB" }, { "name": "[Auth] verifyTOTPSetup (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ verifyTOTPSetup }", - "limit": "9.18 kB" + "limit": "12.59 kB" }, { "name": "[Auth] updatePassword (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ updatePassword }", - "limit": "9.19 kB" + "limit": "12.63 kB" }, { "name": "[Auth] setUpTOTP (Cognito)", @@ -419,85 +419,85 @@ "name": "[Auth] updateUserAttributes (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ updateUserAttributes }", - "limit": "8.46 kB" + "limit": "11.87 kB" }, { "name": "[Auth] getCurrentUser (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ getCurrentUser }", - "limit": "4.32 kB" + "limit": "7.75 kB" }, { "name": "[Auth] confirmUserAttribute (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmUserAttribute }", - "limit": "9.19 kB" + "limit": "12.61 kB" }, { "name": "[Auth] signInWithRedirect (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect }", - "limit": "19.40 kB" + "limit": "20.98 kB" }, { "name": "[Auth] fetchUserAttributes (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ fetchUserAttributes }", - "limit": "8.27 kB" + "limit": "11.69 kB" }, { "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "28.50 kB" + "limit": "29.83 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "19.80 kB" + "limit": "21.42 kB" }, { "name": "[Storage] copy (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ copy }", - "limit": "13.69 kB" + "limit": "14.54 kB" }, { "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "14.32 kB" + "limit": "15.17 kB" }, { "name": "[Storage] getProperties (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getProperties }", - "limit": "13.56 kB" + "limit": "14.43 kB" }, { "name": "[Storage] getUrl (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getUrl }", - "limit": "14.65 kB" + "limit": "15.51 kB" }, { "name": "[Storage] list (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ list }", - "limit": "14.2 kB" + "limit": "14.94 kB" }, { "name": "[Storage] remove (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ remove }", - "limit": "13.50 kB" + "limit": "14.29 kB" }, { "name": "[Storage] uploadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ uploadData }", - "limit": "18.79 kB" + "limit": "19.64 kB" } ] } diff --git a/packages/aws-amplify/src/adapterCore/index.ts b/packages/aws-amplify/src/adapterCore/index.ts index 81f099b9353..755f8c12b42 100644 --- a/packages/aws-amplify/src/adapterCore/index.ts +++ b/packages/aws-amplify/src/adapterCore/index.ts @@ -7,7 +7,10 @@ export { createAWSCredentialsAndIdentityIdProvider, createUserPoolsTokenProvider, } from './authProvidersFactories/cognito'; -export { LegacyConfig } from '@aws-amplify/core/internals/utils'; +export { + LegacyConfig, + AmplifyOutputs, +} from '@aws-amplify/core/internals/utils'; export { AmplifyServer, CookieStorage, diff --git a/packages/aws-amplify/src/initSingleton.ts b/packages/aws-amplify/src/initSingleton.ts index fd6d29e2635..b5de7deb56a 100644 --- a/packages/aws-amplify/src/initSingleton.ts +++ b/packages/aws-amplify/src/initSingleton.ts @@ -10,9 +10,7 @@ import { import { AmplifyOutputs, LegacyConfig, - isAmplifyOutputs, - parseAWSExports, - parseAmplifyOutputs, + parseAmplifyConfig, } from '@aws-amplify/core/internals/utils'; import { @@ -37,15 +35,7 @@ export const DefaultAmplify = { resourceConfig: ResourcesConfig | LegacyConfig | AmplifyOutputs, libraryOptions?: LibraryOptions, ): void { - let resolvedResourceConfig: ResourcesConfig; - - if (Object.keys(resourceConfig).some(key => key.startsWith('aws_'))) { - resolvedResourceConfig = parseAWSExports(resourceConfig); - } else if (isAmplifyOutputs(resourceConfig)) { - resolvedResourceConfig = parseAmplifyOutputs(resourceConfig); - } else { - resolvedResourceConfig = resourceConfig as ResourcesConfig; - } + const resolvedResourceConfig = parseAmplifyConfig(resourceConfig); // If no Auth config is provided, no special handling will be required, configure as is. // Otherwise, we can assume an Auth config is provided from here on. diff --git a/packages/aws-amplify/src/utils/index.ts b/packages/aws-amplify/src/utils/index.ts index 52367f4911a..35093e8e864 100644 --- a/packages/aws-amplify/src/utils/index.ts +++ b/packages/aws-amplify/src/utils/index.ts @@ -18,4 +18,4 @@ export { KeyValueStorageInterface, } from '@aws-amplify/core'; -export { parseAWSExports as parseAmplifyConfig } from '@aws-amplify/core/internals/utils'; +export { parseAmplifyConfig } from '@aws-amplify/core/internals/utils'; diff --git a/packages/core/__tests__/utils/parseAmplifyConfig.test.ts b/packages/core/__tests__/utils/parseAmplifyConfig.test.ts new file mode 100644 index 00000000000..2394e0b1918 --- /dev/null +++ b/packages/core/__tests__/utils/parseAmplifyConfig.test.ts @@ -0,0 +1,72 @@ +import { ResourcesConfig } from '../../src'; +import { parseAmplifyConfig } from '../../src/libraryUtils'; +import { parseAWSExports } from '../../src/parseAWSExports'; +import { isAmplifyOutputs, parseAmplifyOutputs } from '../../src/parseAmplifyOutputs'; + +jest.mock('../../src/parseAWSExports'); +jest.mock('../../src/parseAmplifyOutputs'); + +const testAmplifyOutputs = { + 'version': '1', + 'auth': { + 'user_pool_id': 'us-east-1:', + 'user_pool_client_id': 'xxxx', + 'aws_region': 'us-east-1', + }, +} + +const testLegacyConfig = { + aws_project_region: 'us-west-2', + aws_user_pools_id: 'user-pool-id', + aws_user_pools_web_client_id: 'user-pool-client-id' +} + +const testResourcesConfig: ResourcesConfig = { + Auth: { + Cognito: { + userPoolId: 'us-east-1:xxx', + userPoolClientId: 'xxxx', + identityPoolId: 'test' + } + } +}; + +describe('parseAmplifyConfig', () => { + const mockParseAWSExports = parseAWSExports as jest.Mock; + const mockParseAmplifyOutputs = parseAmplifyOutputs as jest.Mock; + const mockIsAmplifyOutputs = isAmplifyOutputs as unknown as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockParseAWSExports.mockReturnValue(testResourcesConfig); + mockParseAmplifyOutputs.mockReturnValue(testResourcesConfig); + mockIsAmplifyOutputs.mockReturnValue(false); + }); + + it('returns a ResourceConfig when one is provided', () => { + const parsedConfig = parseAmplifyConfig(testResourcesConfig); + + // Verify that a provided ResourceConfig is returned back unmodified + expect(parsedConfig).toEqual(testResourcesConfig) + }); + + it('parses legacy config objects into ResourcesConfig', () => { + const parsedConfig = parseAmplifyConfig(testLegacyConfig); + + // Verify that a provided legacy config is parsed into a ResourcesConfig + expect(parsedConfig).toEqual(testResourcesConfig); + expect(mockParseAWSExports).toHaveBeenCalledTimes(1); + expect(mockParseAWSExports).toHaveBeenCalledWith(testLegacyConfig) + }); + + it('parses Gen2 config objects into ResourcesConfig', () => { + mockIsAmplifyOutputs.mockReturnValueOnce(true); + const parsedConfig = parseAmplifyConfig(testAmplifyOutputs); + + // Verify that a provided Gen2 config is parsed into a ResourcesConfig + expect(parsedConfig).toEqual(testResourcesConfig); + expect(mockParseAmplifyOutputs).toHaveBeenCalledTimes(1); + expect(mockIsAmplifyOutputs).toHaveBeenCalledTimes(1); + expect(mockParseAmplifyOutputs).toHaveBeenCalledWith(testAmplifyOutputs); + }); +}) \ No newline at end of file diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 4e1f576834e..df38e8b3476 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -27,6 +27,7 @@ export { AmplifyOutputs } from './singleton/AmplifyOutputs/types'; export { ADD_OAUTH_LISTENER } from './singleton/constants'; export { amplifyUuid } from './utils/amplifyUuid'; export { AmplifyUrl, AmplifyUrlSearchParams } from './utils/amplifyUrl'; +export { parseAmplifyConfig } from './utils/parseAmplifyConfig'; // Auth utilities export { diff --git a/packages/core/src/parseAmplifyOutputs.ts b/packages/core/src/parseAmplifyOutputs.ts index 09c62e5d4c7..930f36df2de 100644 --- a/packages/core/src/parseAmplifyOutputs.ts +++ b/packages/core/src/parseAmplifyOutputs.ts @@ -11,6 +11,7 @@ import { APIConfig, APIGraphQLConfig, GraphQLAuthMode, + ModelIntrospectionSchema, } from './singleton/API/types'; import { CognitoUserPoolConfigMfaStatus, @@ -215,7 +216,7 @@ function parseData( defaultAuthMode: getGraphQLAuthMode(default_authorization_type), region: aws_region, apiKey: api_key, - modelIntrospection: model_introspection, + modelIntrospection: model_introspection as ModelIntrospectionSchema, }; return { diff --git a/packages/core/src/singleton/Amplify.ts b/packages/core/src/singleton/Amplify.ts index 87611d6c4cd..d3bcd33a2a4 100644 --- a/packages/core/src/singleton/Amplify.ts +++ b/packages/core/src/singleton/Amplify.ts @@ -1,10 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { AMPLIFY_SYMBOL, Hub } from '../Hub'; -import { parseAWSExports } from '../parseAWSExports'; import { deepFreeze } from '../utils'; +import { parseAmplifyConfig } from '../libraryUtils'; import { + AmplifyOutputs, AuthConfig, LegacyConfig, LibraryOptions, @@ -48,16 +49,10 @@ export class AmplifyClass { * @param libraryOptions - Additional options for customizing the behavior of the library. */ configure( - resourcesConfig: ResourcesConfig | LegacyConfig, + resourcesConfig: ResourcesConfig | LegacyConfig | AmplifyOutputs, libraryOptions?: LibraryOptions, ): void { - let resolvedResourceConfig: ResourcesConfig; - - if (Object.keys(resourcesConfig).some(key => key.startsWith('aws_'))) { - resolvedResourceConfig = parseAWSExports(resourcesConfig); - } else { - resolvedResourceConfig = resourcesConfig as ResourcesConfig; - } + const resolvedResourceConfig = parseAmplifyConfig(resourcesConfig); this.resourcesConfig = resolvedResourceConfig; diff --git a/packages/core/src/singleton/AmplifyOutputs/types.ts b/packages/core/src/singleton/AmplifyOutputs/types.ts index d3476f87723..a09bf792267 100644 --- a/packages/core/src/singleton/AmplifyOutputs/types.ts +++ b/packages/core/src/singleton/AmplifyOutputs/types.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ModelIntrospectionSchema } from '../API/types'; - export type AmplifyOutputsOAuthIdentityProvider = | 'GOOGLE' | 'FACEBOOK' @@ -79,7 +77,7 @@ export interface AmplifyOutputsDataProperties { url: string; default_authorization_type: string; authorization_types: string[]; - model_introspection?: ModelIntrospectionSchema; + model_introspection?: object; api_key?: string; conflict_resolution_mode?: string; } diff --git a/packages/core/src/singleton/types.ts b/packages/core/src/singleton/types.ts index 423ac96d23f..e2acbeb6611 100644 --- a/packages/core/src/singleton/types.ts +++ b/packages/core/src/singleton/types.ts @@ -26,6 +26,8 @@ import { import { NotificationsConfig } from './Notifications/types'; import { InteractionsConfig } from './Interactions/types'; +export { AmplifyOutputs } from './AmplifyOutputs/types'; + /** * Compatibility type representing the Amplify Gen 1 configuration file schema. This type should not be used directly. */ diff --git a/packages/core/src/utils/parseAmplifyConfig.ts b/packages/core/src/utils/parseAmplifyConfig.ts new file mode 100644 index 00000000000..424f71a7102 --- /dev/null +++ b/packages/core/src/utils/parseAmplifyConfig.ts @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ResourcesConfig } from '../index'; +import { AmplifyOutputs } from '../singleton/AmplifyOutputs/types'; +import { LegacyConfig } from '../singleton/types'; +import { parseAWSExports } from '../parseAWSExports'; +import { isAmplifyOutputs, parseAmplifyOutputs } from '../parseAmplifyOutputs'; + +/** + * Parses the variety of configuration shapes that Amplify can accept into a ResourcesConfig. + * + * @param amplifyConfig An Amplify configuration object conforming to one of the supported schemas. + * @return A ResourcesConfig for the provided configuration object. + */ +export const parseAmplifyConfig = ( + amplifyConfig: ResourcesConfig | LegacyConfig | AmplifyOutputs, +): ResourcesConfig => { + if (Object.keys(amplifyConfig).some(key => key.startsWith('aws_'))) { + return parseAWSExports(amplifyConfig); + } else if (isAmplifyOutputs(amplifyConfig)) { + return parseAmplifyOutputs(amplifyConfig); + } else { + return amplifyConfig as ResourcesConfig; + } +}; diff --git a/packages/interactions/package.json b/packages/interactions/package.json index c2603233939..c756c5b9695 100644 --- a/packages/interactions/package.json +++ b/packages/interactions/package.json @@ -88,19 +88,19 @@ "name": "Interactions (default to Lex v2)", "path": "./dist/esm/index.mjs", "import": "{ Interactions }", - "limit": "52.00 kB" + "limit": "52.52 kB" }, { "name": "Interactions (Lex v2)", "path": "./dist/esm/lex-v2/index.mjs", "import": "{ Interactions }", - "limit": "52.00 kB" + "limit": "52.52 kB" }, { "name": "Interactions (Lex v1)", "path": "./dist/esm/lex-v1/index.mjs", "import": "{ Interactions }", - "limit": "47.00 kB" + "limit": "47.33 kB" } ] } From 944d23564fee7cf0ee51c919e51d06d641addb82 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 17 Apr 2024 13:08:38 -0700 Subject: [PATCH 23/33] chore: fix linter erros and bump bundle --- packages/aws-amplify/package.json | 6 ++-- .../utils/parseAmplifyConfig.test.ts | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 9c5db39479c..1d20ffb3e37 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -335,7 +335,7 @@ "name": "[API] generateClient (AppSync)", "path": "./dist/esm/api/index.mjs", "import": "{ generateClient }", - "limit": "39.0 kB" + "limit": "39.50 kB" }, { "name": "[API] REST API handlers", @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "27.63 kB" + "limit": "28.09 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -449,7 +449,7 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "29.83 kB" + "limit": "29.90 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", diff --git a/packages/core/__tests__/utils/parseAmplifyConfig.test.ts b/packages/core/__tests__/utils/parseAmplifyConfig.test.ts index 2394e0b1918..1b3022c8753 100644 --- a/packages/core/__tests__/utils/parseAmplifyConfig.test.ts +++ b/packages/core/__tests__/utils/parseAmplifyConfig.test.ts @@ -1,34 +1,38 @@ +/* eslint-disable camelcase */ import { ResourcesConfig } from '../../src'; import { parseAmplifyConfig } from '../../src/libraryUtils'; import { parseAWSExports } from '../../src/parseAWSExports'; -import { isAmplifyOutputs, parseAmplifyOutputs } from '../../src/parseAmplifyOutputs'; +import { + isAmplifyOutputs, + parseAmplifyOutputs, +} from '../../src/parseAmplifyOutputs'; jest.mock('../../src/parseAWSExports'); jest.mock('../../src/parseAmplifyOutputs'); const testAmplifyOutputs = { - 'version': '1', - 'auth': { - 'user_pool_id': 'us-east-1:', - 'user_pool_client_id': 'xxxx', - 'aws_region': 'us-east-1', + version: '1', + auth: { + user_pool_id: 'us-east-1:', + user_pool_client_id: 'xxxx', + aws_region: 'us-east-1', }, -} +}; const testLegacyConfig = { aws_project_region: 'us-west-2', aws_user_pools_id: 'user-pool-id', - aws_user_pools_web_client_id: 'user-pool-client-id' -} + aws_user_pools_web_client_id: 'user-pool-client-id', +}; const testResourcesConfig: ResourcesConfig = { Auth: { Cognito: { userPoolId: 'us-east-1:xxx', userPoolClientId: 'xxxx', - identityPoolId: 'test' - } - } + identityPoolId: 'test', + }, + }, }; describe('parseAmplifyConfig', () => { @@ -47,7 +51,7 @@ describe('parseAmplifyConfig', () => { const parsedConfig = parseAmplifyConfig(testResourcesConfig); // Verify that a provided ResourceConfig is returned back unmodified - expect(parsedConfig).toEqual(testResourcesConfig) + expect(parsedConfig).toEqual(testResourcesConfig); }); it('parses legacy config objects into ResourcesConfig', () => { @@ -56,7 +60,7 @@ describe('parseAmplifyConfig', () => { // Verify that a provided legacy config is parsed into a ResourcesConfig expect(parsedConfig).toEqual(testResourcesConfig); expect(mockParseAWSExports).toHaveBeenCalledTimes(1); - expect(mockParseAWSExports).toHaveBeenCalledWith(testLegacyConfig) + expect(mockParseAWSExports).toHaveBeenCalledWith(testLegacyConfig); }); it('parses Gen2 config objects into ResourcesConfig', () => { @@ -69,4 +73,4 @@ describe('parseAmplifyConfig', () => { expect(mockIsAmplifyOutputs).toHaveBeenCalledTimes(1); expect(mockParseAmplifyOutputs).toHaveBeenCalledWith(testAmplifyOutputs); }); -}) \ No newline at end of file +}); From 9bc6ce8365648dec7a80fc241b1150c067b4030b Mon Sep 17 00:00:00 2001 From: ashika112 Date: Tue, 23 Apr 2024 09:21:11 -0700 Subject: [PATCH 24/33] bundle size update --- packages/aws-amplify/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index cf0157cfebd..39389ade291 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -335,7 +335,7 @@ "name": "[API] generateClient (AppSync)", "path": "./dist/esm/api/index.mjs", "import": "{ generateClient }", - "limit": "39.50 kB" + "limit": "39.80 kB" }, { "name": "[API] REST API handlers", From e54bfa991dbe75b7c7691bf0504a0b1616362837 Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:52:54 -0700 Subject: [PATCH 25/33] [Fix]: Storage Input/Output types (#13270) * Revert "Fix Overload return types for storage APIs (#13239)" This reverts commit a81fc29e3b339479110410ed75f28a2f95e32686. * make a first pass at type update * download & remove base update * getProps & getUrl base update * update list base tpes * update copy type * update to fn declaration * update server def * adding tests * updating tests - copy,remove * update tests -list/upload * address Pr comments --- .../__tests__/providers/s3/apis/copy.test.ts | 66 ++--- .../providers/s3/apis/downloadData.test.ts | 200 +++++++++------ .../providers/s3/apis/getProperties.test.ts | 167 ++++++++----- .../providers/s3/apis/getUrl.test.ts | 97 +++++--- .../__tests__/providers/s3/apis/list.test.ts | 226 +++++++++-------- .../providers/s3/apis/remove.test.ts | 71 +++--- .../s3/apis/uploadData/index.test.ts | 44 ++-- .../apis/uploadData/multipartHandlers.test.ts | 11 +- .../s3/apis/uploadData/putObjectJob.test.ts | 39 ++- packages/storage/src/index.ts | 16 ++ .../storage/src/providers/s3/apis/copy.ts | 61 +++-- .../src/providers/s3/apis/downloadData.ts | 169 +++++++------ .../src/providers/s3/apis/getProperties.ts | 67 ++--- .../storage/src/providers/s3/apis/getUrl.ts | 88 +++---- .../src/providers/s3/apis/internal/copy.ts | 22 +- .../s3/apis/internal/getProperties.ts | 15 +- .../src/providers/s3/apis/internal/getUrl.ts | 22 +- .../src/providers/s3/apis/internal/list.ts | 46 ++-- .../src/providers/s3/apis/internal/remove.ts | 15 +- .../storage/src/providers/s3/apis/list.ts | 106 ++++---- .../storage/src/providers/s3/apis/remove.ts | 57 +++-- .../src/providers/s3/apis/server/copy.ts | 87 +++---- .../providers/s3/apis/server/getProperties.ts | 85 +++---- .../src/providers/s3/apis/server/getUrl.ts | 108 ++++---- .../src/providers/s3/apis/server/list.ts | 139 +++++------ .../src/providers/s3/apis/server/remove.ts | 80 +++--- .../src/providers/s3/apis/uploadData/index.ts | 231 +++++++++--------- .../uploadData/multipart/uploadHandlers.ts | 18 +- .../s3/apis/uploadData/putObjectJob.ts | 12 +- packages/storage/src/providers/s3/index.ts | 16 ++ .../storage/src/providers/s3/types/index.ts | 41 ++-- .../storage/src/providers/s3/types/inputs.ts | 103 ++++---- .../storage/src/providers/s3/types/outputs.ts | 97 ++++++-- packages/storage/src/types/index.ts | 2 + packages/storage/src/types/inputs.ts | 18 +- packages/storage/src/types/outputs.ts | 22 +- 36 files changed, 1439 insertions(+), 1225 deletions(-) diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index c7896ca868e..52eaf7c902f 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -2,14 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify } from '@aws-amplify/core'; +import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { StorageError } from '../../../../src/errors/StorageError'; import { StorageValidationErrorCode } from '../../../../src/errors/types/validation'; import { copyObject } from '../../../../src/providers/s3/utils/client'; import { copy } from '../../../../src/providers/s3/apis'; import { - CopySourceOptionsWithKey, - CopyDestinationOptionsWithKey, + CopyInput, + CopyWithPathInput, + CopyOutput, + CopyWithPathOutput, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -67,6 +69,8 @@ describe('copy API', () => { describe('Happy Cases', () => { describe('With key', () => { + const copyWrapper = async (input: CopyInput): Promise => + copy(input); beforeEach(() => { mockCopyObject.mockImplementation(() => { return { @@ -77,7 +81,14 @@ describe('copy API', () => { afterEach(() => { jest.clearAllMocks(); }); - [ + const testCases: Array<{ + source: { accessLevel?: StorageAccessLevel; targetIdentityId?: string }; + destination: { + accessLevel?: StorageAccessLevel; + }; + expectedSourceKey: string; + expectedDestinationKey: string; + }> = [ { source: { accessLevel: 'guest' }, destination: { accessLevel: 'guest' }, @@ -150,7 +161,8 @@ describe('copy API', () => { expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, }, - ].forEach( + ]; + testCases.forEach( ({ source, destination, @@ -160,24 +172,18 @@ describe('copy API', () => { const targetIdentityIdMsg = source?.targetIdentityId ? `with targetIdentityId` : ''; - const copyResult = { - key: destinationKey, - path: expectedDestinationKey, - }; - it(`should copy ${source.accessLevel} ${targetIdentityIdMsg} -> ${destination.accessLevel}`, async () => { - expect( - await copy({ - source: { - ...(source as CopySourceOptionsWithKey), - key: sourceKey, - }, - destination: { - ...(destination as CopyDestinationOptionsWithKey), - key: destinationKey, - }, - }), - ).toEqual(copyResult); + const { key } = await copyWrapper({ + source: { + ...source, + key: sourceKey, + }, + destination: { + ...destination, + key: destinationKey, + }, + }); + expect(key).toEqual(destinationKey); expect(copyObject).toHaveBeenCalledTimes(1); expect(copyObject).toHaveBeenCalledWith(copyObjectClientConfig, { ...copyObjectClientBaseParams, @@ -190,6 +196,10 @@ describe('copy API', () => { }); describe('With path', () => { + const copyWrapper = async ( + input: CopyWithPathInput, + ): Promise => copy(input); + beforeEach(() => { mockCopyObject.mockImplementation(() => { return { @@ -222,15 +232,11 @@ describe('copy API', () => { destinationPath, expectedDestinationPath, }) => { - expect( - await copy({ - source: { path: sourcePath }, - destination: { path: destinationPath }, - }), - ).toEqual({ - path: expectedDestinationPath, - key: expectedDestinationPath, + const { path } = await copyWrapper({ + source: { path: sourcePath }, + destination: { path: destinationPath }, }); + expect(path).toEqual(expectedDestinationPath); expect(copyObject).toHaveBeenCalledTimes(1); expect(copyObject).toHaveBeenCalledWith(copyObjectClientConfig, { ...copyObjectClientBaseParams, diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 8118d47e56a..0c9c4a3d007 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify } from '@aws-amplify/core'; +import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { getObject } from '../../../../src/providers/s3/utils/client'; import { downloadData } from '../../../../src/providers/s3'; import { @@ -10,13 +10,18 @@ import { validateStorageOperationInput, } from '../../../../src/providers/s3/utils'; import { - DownloadDataOptionsWithKey, - DownloadDataOptionsWithPath, + DownloadDataInput, + DownloadDataWithPathInput, } from '../../../../src/providers/s3/types'; import { STORAGE_INPUT_KEY, STORAGE_INPUT_PATH, } from '../../../../src/providers/s3/utils/constants'; +import { StorageDownloadDataOutput } from '../../../../src/types'; +import { + ItemWithKey, + ItemWithPath, +} from '../../../../src/providers/s3/types/outputs'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('../../../../src/providers/s3/utils'); @@ -36,11 +41,21 @@ const credentials: AWSCredentials = { sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', }; -const key = 'key'; +const inputKey = 'key'; +const inputPath = 'path'; const bucket = 'bucket'; const region = 'region'; const targetIdentityId = 'targetIdentityId'; const defaultIdentityId = 'defaultIdentityId'; +const mockDownloadResultBase = { + body: 'body', + lastModified: 'lastModified', + size: 'contentLength', + eTag: 'eTag', + metadata: 'metadata', + versionId: 'versionId', + contentType: 'contentType', +}; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockCreateDownloadTask = createDownloadTask as jest.Mock; @@ -62,55 +77,69 @@ describe('downloadData with key', () => { }, }); }); - mockCreateDownloadTask.mockReturnValue('downloadTask'); - mockValidateStorageInput.mockReturnValue({ - inputType: STORAGE_INPUT_KEY, - objectKey: key, - }); beforeEach(() => { jest.clearAllMocks(); + + mockCreateDownloadTask.mockReturnValue('downloadTask'); + mockValidateStorageInput.mockReturnValue({ + inputType: STORAGE_INPUT_KEY, + objectKey: inputKey, + }); }); it('should return a download task with key', async () => { - expect(downloadData({ key: 'key' })).toBe('downloadTask'); + const mockDownloadInput: DownloadDataInput = { + key: inputKey, + options: { accessLevel: 'protected', targetIdentityId: targetIdentityId }, + }; + expect(downloadData(mockDownloadInput)).toBe('downloadTask'); }); - test.each([ + const testCases: Array<{ + expectedKey: string; + options?: { accessLevel?: StorageAccessLevel; targetIdentityId?: string }; + }> = [ { - expectedKey: `public/${key}`, + expectedKey: `public/${inputKey}`, }, { options: { accessLevel: 'guest' }, - expectedKey: `public/${key}`, + expectedKey: `public/${inputKey}`, }, { options: { accessLevel: 'private' }, - expectedKey: `private/${defaultIdentityId}/${key}`, + expectedKey: `private/${defaultIdentityId}/${inputKey}`, }, { options: { accessLevel: 'protected' }, - expectedKey: `protected/${defaultIdentityId}/${key}`, + expectedKey: `protected/${defaultIdentityId}/${inputKey}`, }, { options: { accessLevel: 'protected', targetIdentityId }, - expectedKey: `protected/${targetIdentityId}/${key}`, + expectedKey: `protected/${targetIdentityId}/${inputKey}`, }, - ])( + ]; + + test.each(testCases)( 'should supply the correct parameters to getObject API handler with $expectedKey accessLevel', async ({ options, expectedKey }) => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const onProgress = jest.fn(); downloadData({ - key, + key: inputKey, options: { ...options, useAccelerateEndpoint: true, onProgress, - } as DownloadDataOptionsWithKey, + }, }); const job = mockCreateDownloadTask.mock.calls[0][0].job; - await job(); + const { key, body }: StorageDownloadDataOutput = await job(); + expect({ key, body }).toEqual({ + key: inputKey, + body: 'body', + }); expect(getObject).toHaveBeenCalledTimes(1); expect(getObject).toHaveBeenCalledWith( { @@ -130,38 +159,40 @@ describe('downloadData with key', () => { ); it('should assign the getObject API handler response to the result with key', async () => { - const lastModified = 'lastModified'; - const contentLength = 'contentLength'; - const eTag = 'eTag'; - const metadata = 'metadata'; - const versionId = 'versionId'; - const contentType = 'contentType'; - const body = 'body'; - const key = 'key'; - const expectedKey = `public/${key}`; (getObject as jest.Mock).mockResolvedValueOnce({ - Body: body, - LastModified: lastModified, - ContentLength: contentLength, - ETag: eTag, - Metadata: metadata, - VersionId: versionId, - ContentType: contentType, + Body: 'body', + LastModified: 'lastModified', + ContentLength: 'contentLength', + ETag: 'eTag', + Metadata: 'metadata', + VersionId: 'versionId', + ContentType: 'contentType', }); - downloadData({ key }); + downloadData({ key: inputKey }); const job = mockCreateDownloadTask.mock.calls[0][0].job; - const result = await job(); - expect(getObject).toHaveBeenCalledTimes(1); - expect(result).toEqual({ + const { key, - path: `public/${key}`, body, - lastModified, - size: contentLength, + contentType, eTag, + lastModified, metadata, + size, versionId, + }: StorageDownloadDataOutput = await job(); + expect(getObject).toHaveBeenCalledTimes(1); + expect({ + key, + body, contentType, + eTag, + lastModified, + metadata, + size, + versionId, + }).toEqual({ + key: inputKey, + ...mockDownloadResultBase, }); }); @@ -171,7 +202,7 @@ describe('downloadData with key', () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); downloadData({ - key: 'mockKey', + key: inputKey, options: { bytesRange: { start, end }, }, @@ -206,7 +237,7 @@ describe('downloadData with path', () => { mockCreateDownloadTask.mockReturnValue('downloadTask'); mockValidateStorageInput.mockReturnValue({ inputType: STORAGE_INPUT_PATH, - objectKey: 'path', + objectKey: inputPath, }); }); @@ -215,17 +246,21 @@ describe('downloadData with path', () => { }); it('should return a download task with path', async () => { - expect(downloadData({ path: 'path' })).toBe('downloadTask'); + const mockDownloadInput: DownloadDataWithPathInput = { + path: inputPath, + options: { useAccelerateEndpoint: true }, + }; + expect(downloadData(mockDownloadInput)).toBe('downloadTask'); }); test.each([ { - path: 'path', - expectedKey: 'path', + path: inputPath, + expectedKey: inputPath, }, { - path: () => 'path', - expectedKey: 'path', + path: () => inputPath, + expectedKey: inputPath, }, ])( 'should call getObject API with $expectedKey when path provided is $path', @@ -233,14 +268,24 @@ describe('downloadData with path', () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const onProgress = jest.fn(); downloadData({ - path: path, + path, options: { useAccelerateEndpoint: true, onProgress, - } as DownloadDataOptionsWithPath, + }, }); const job = mockCreateDownloadTask.mock.calls[0][0].job; - await job(); + const { + path: resultPath, + body, + }: StorageDownloadDataOutput = await job(); + expect({ + path: resultPath, + body, + }).toEqual({ + path: expectedKey, + body: 'body', + }); expect(getObject).toHaveBeenCalledTimes(1); expect(getObject).toHaveBeenCalledWith( { @@ -260,37 +305,40 @@ describe('downloadData with path', () => { ); it('should assign the getObject API handler response to the result with path', async () => { - const lastModified = 'lastModified'; - const contentLength = 'contentLength'; - const eTag = 'eTag'; - const metadata = 'metadata'; - const versionId = 'versionId'; - const contentType = 'contentType'; - const body = 'body'; - const path = 'path'; (getObject as jest.Mock).mockResolvedValueOnce({ - Body: body, - LastModified: lastModified, - ContentLength: contentLength, - ETag: eTag, - Metadata: metadata, - VersionId: versionId, - ContentType: contentType, + Body: 'body', + LastModified: 'lastModified', + ContentLength: 'contentLength', + ETag: 'eTag', + Metadata: 'metadata', + VersionId: 'versionId', + ContentType: 'contentType', }); - downloadData({ path }); + downloadData({ path: inputPath }); const job = mockCreateDownloadTask.mock.calls[0][0].job; - const result = await job(); - expect(getObject).toHaveBeenCalledTimes(1); - expect(result).toEqual({ + const { path, - key: path, body, - lastModified, - size: contentLength, + contentType, eTag, + lastModified, metadata, + size, versionId, + }: StorageDownloadDataOutput = await job(); + expect(getObject).toHaveBeenCalledTimes(1); + expect({ + path, + body, contentType, + eTag, + lastModified, + metadata, + size, + versionId, + }).toEqual({ + path: inputPath, + ...mockDownloadResultBase, }); }); @@ -300,7 +348,7 @@ describe('downloadData with path', () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); downloadData({ - path: 'mockPath', + path: inputPath, options: { bytesRange: { start, end }, }, diff --git a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts index a8731fb65c5..191802f04f9 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -4,10 +4,12 @@ import { headObject } from '../../../../src/providers/s3/utils/client'; import { getProperties } from '../../../../src/providers/s3'; import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify } from '@aws-amplify/core'; +import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { - GetPropertiesOptionsWithKey, - GetPropertiesOptionsWithPath, + GetPropertiesInput, + GetPropertiesWithPathInput, + GetPropertiesOutput, + GetPropertiesWithPathOutput, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -22,7 +24,7 @@ jest.mock('@aws-amplify/core', () => ({ }, }, })); -const mockHeadObject = headObject as jest.Mock; +const mockHeadObject = headObject as jest.MockedFunction; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; @@ -33,12 +35,24 @@ const credentials: AWSCredentials = { sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', }; -const key = 'key'; -const path = 'path'; +const inputKey = 'key'; +const inputPath = 'path'; const targetIdentityId = 'targetIdentityId'; const defaultIdentityId = 'defaultIdentityId'; +const expectedResult = { + size: 100, + contentType: 'text/plain', + eTag: 'etag', + metadata: { key: 'value' }, + lastModified: new Date('01-01-1980'), + versionId: 'version-id', +}; + describe('getProperties with key', () => { + const getPropertiesWrapper = ( + input: GetPropertiesInput, + ): Promise => getProperties(input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -54,66 +68,81 @@ describe('getProperties with key', () => { }); }); describe('Happy cases: With key', () => { - const expected = { - key, - size: '100', - contentType: 'text/plain', - eTag: 'etag', - metadata: { key: 'value' }, - lastModified: 'last-modified', - versionId: 'version-id', - }; const config = { credentials, region: 'region', userAgentValue: expect.any(String), }; beforeEach(() => { - mockHeadObject.mockReturnValueOnce({ - ContentLength: '100', + mockHeadObject.mockResolvedValue({ + ContentLength: 100, ContentType: 'text/plain', ETag: 'etag', - LastModified: 'last-modified', + LastModified: new Date('01-01-1980'), Metadata: { key: 'value' }, VersionId: 'version-id', + $metadata: {} as any, }); }); afterEach(() => { jest.clearAllMocks(); }); - test.each([ + + const testCases: Array<{ + expectedKey: string; + options?: { accessLevel?: StorageAccessLevel; targetIdentityId?: string }; + }> = [ { - expectedKey: `public/${key}`, + expectedKey: `public/${inputKey}`, }, { options: { accessLevel: 'guest' }, - expectedKey: `public/${key}`, + expectedKey: `public/${inputKey}`, }, { options: { accessLevel: 'private' }, - expectedKey: `private/${defaultIdentityId}/${key}`, + expectedKey: `private/${defaultIdentityId}/${inputKey}`, }, { options: { accessLevel: 'protected' }, - expectedKey: `protected/${defaultIdentityId}/${key}`, + expectedKey: `protected/${defaultIdentityId}/${inputKey}`, }, { options: { accessLevel: 'protected', targetIdentityId }, - expectedKey: `protected/${targetIdentityId}/${key}`, + expectedKey: `protected/${targetIdentityId}/${inputKey}`, }, - ])( + ]; + test.each(testCases)( 'should getProperties with key $expectedKey', async ({ options, expectedKey }) => { const headObjectOptions = { Bucket: 'bucket', Key: expectedKey, }; - expect( - await getProperties({ - key, - options: options as GetPropertiesOptionsWithKey, - }), - ).toEqual({ ...expected, path: expectedKey }); + const { + key, + contentType, + eTag, + lastModified, + metadata, + size, + versionId, + } = await getPropertiesWrapper({ + key: inputKey, + options, + }); + expect({ + key, + contentType, + eTag, + lastModified, + metadata, + size, + versionId, + }).toEqual({ + key: inputKey, + ...expectedResult, + }); expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); }, @@ -133,7 +162,7 @@ describe('getProperties with key', () => { ); expect.assertions(3); try { - await getProperties({ key }); + await getPropertiesWrapper({ key: inputKey }); } catch (error: any) { expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith( @@ -144,7 +173,7 @@ describe('getProperties with key', () => { }, { Bucket: 'bucket', - Key: `public/${key}`, + Key: `public/${inputKey}`, }, ); expect(error.$metadata.httpStatusCode).toBe(404); @@ -154,6 +183,9 @@ describe('getProperties with key', () => { }); describe('Happy cases: With path', () => { + const getPropertiesWrapper = ( + input: GetPropertiesWithPathInput, + ): Promise => getProperties(input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -169,16 +201,6 @@ describe('Happy cases: With path', () => { }); }); describe('getProperties with path', () => { - const expected = { - key: path, - path, - size: '100', - contentType: 'text/plain', - eTag: 'etag', - metadata: { key: 'value' }, - lastModified: 'last-modified', - versionId: 'version-id', - }; const config = { credentials, region: 'region', @@ -186,13 +208,14 @@ describe('Happy cases: With path', () => { userAgentValue: expect.any(String), }; beforeEach(() => { - mockHeadObject.mockReturnValueOnce({ - ContentLength: '100', + mockHeadObject.mockResolvedValue({ + ContentLength: 100, ContentType: 'text/plain', ETag: 'etag', - LastModified: 'last-modified', + LastModified: new Date('01-01-1980'), Metadata: { key: 'value' }, VersionId: 'version-id', + $metadata: {} as any, }); }); afterEach(() => { @@ -200,28 +223,46 @@ describe('Happy cases: With path', () => { }); test.each([ { - testPath: path, - expectedKey: path, + testPath: inputPath, + expectedPath: inputPath, }, { - testPath: () => path, - expectedKey: path, + testPath: () => inputPath, + expectedPath: inputPath, }, ])( - 'should getProperties with path $path and expectedKey $expectedKey', - async ({ testPath, expectedKey }) => { + 'should getProperties with path $path and expectedPath $expectedPath', + async ({ testPath, expectedPath }) => { const headObjectOptions = { Bucket: 'bucket', - Key: expectedKey, + Key: expectedPath, }; - expect( - await getProperties({ - path: testPath, - options: { - useAccelerateEndpoint: true, - } as GetPropertiesOptionsWithPath, - }), - ).toEqual(expected); + const { + path, + contentType, + eTag, + lastModified, + metadata, + size, + versionId, + } = await getPropertiesWrapper({ + path: testPath, + options: { + useAccelerateEndpoint: true, + }, + }); + expect({ + path, + contentType, + eTag, + lastModified, + metadata, + size, + versionId, + }).toEqual({ + path: expectedPath, + ...expectedResult, + }); expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); }, @@ -241,7 +282,7 @@ describe('Happy cases: With path', () => { ); expect.assertions(3); try { - await getProperties({ path }); + await getPropertiesWrapper({ path: inputPath }); } catch (error: any) { expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith( @@ -252,7 +293,7 @@ describe('Happy cases: With path', () => { }, { Bucket: 'bucket', - Key: path, + Key: inputPath, }, ); expect(error.$metadata.httpStatusCode).toBe(404); diff --git a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts index a68c8c3516b..8f56299d943 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -3,14 +3,16 @@ import { getUrl } from '../../../../src/providers/s3/apis'; import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify } from '@aws-amplify/core'; +import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { getPresignedGetObjectUrl, headObject, } from '../../../../src/providers/s3/utils/client'; import { - GetUrlOptionsWithKey, - GetUrlOptionsWithPath, + GetUrlInput, + GetUrlWithPathInput, + GetUrlOutput, + GetUrlWithPathOutput, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -37,8 +39,11 @@ const credentials: AWSCredentials = { }; const targetIdentityId = 'targetIdentityId'; const defaultIdentityId = 'defaultIdentityId'; +const mockURL = new URL('https://google.com'); describe('getUrl test with key', () => { + const getUrlWrapper = (input: GetUrlInput): Promise => + getUrl(input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -62,24 +67,28 @@ describe('getUrl test with key', () => { }; const key = 'key'; beforeEach(() => { - (headObject as jest.Mock).mockImplementation(() => { - return { - Key: 'key', - ContentLength: '100', - ContentType: 'text/plain', - ETag: 'etag', - LastModified: 'last-modified', - Metadata: { key: 'value' }, - }; - }); - (getPresignedGetObjectUrl as jest.Mock).mockReturnValueOnce({ - url: new URL('https://google.com'), + (headObject as jest.MockedFunction).mockResolvedValue({ + ContentLength: 100, + ContentType: 'text/plain', + ETag: 'etag', + LastModified: new Date('01-01-1980'), + Metadata: { meta: 'value' }, + $metadata: {} as any, }); + ( + getPresignedGetObjectUrl as jest.MockedFunction< + typeof getPresignedGetObjectUrl + > + ).mockResolvedValue(mockURL); }); afterEach(() => { jest.clearAllMocks(); }); - test.each([ + + const testCases: Array<{ + options?: { accessLevel?: StorageAccessLevel; targetIdentityId?: string }; + expectedKey: string; + }> = [ { expectedKey: `public/${key}`, }, @@ -99,26 +108,30 @@ describe('getUrl test with key', () => { options: { accessLevel: 'protected', targetIdentityId }, expectedKey: `protected/${targetIdentityId}/${key}`, }, - ])( + ]; + + test.each(testCases)( 'should getUrl with key $expectedKey', async ({ options, expectedKey }) => { const headObjectOptions = { Bucket: bucket, Key: expectedKey, }; - const result = await getUrl({ + const { url, expiresAt } = await getUrlWrapper({ key, options: { ...options, validateObjectExistence: true, - } as GetUrlOptionsWithKey, + }, }); + const expectedResult = { + url: mockURL, + expiresAt: expect.any(Date), + }; expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); - expect(result.url).toEqual({ - url: new URL('https://google.com'), - }); + expect({ url, expiresAt }).toEqual(expectedResult); }, ); }); @@ -135,7 +148,7 @@ describe('getUrl test with key', () => { }); expect.assertions(2); try { - await getUrl({ + await getUrlWrapper({ key: 'invalid_key', options: { validateObjectExistence: true }, }); @@ -148,6 +161,9 @@ describe('getUrl test with key', () => { }); describe('getUrl test with path', () => { + const getUrlWrapper = ( + input: GetUrlWithPathInput, + ): Promise => getUrl(input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -170,19 +186,19 @@ describe('getUrl test with path', () => { userAgentValue: expect.any(String), }; beforeEach(() => { - (headObject as jest.Mock).mockImplementation(() => { - return { - Key: 'path', - ContentLength: '100', - ContentType: 'text/plain', - ETag: 'etag', - LastModified: 'last-modified', - Metadata: { key: 'value' }, - }; - }); - (getPresignedGetObjectUrl as jest.Mock).mockReturnValueOnce({ - url: new URL('https://google.com'), + (headObject as jest.MockedFunction).mockResolvedValue({ + ContentLength: 100, + ContentType: 'text/plain', + ETag: 'etag', + LastModified: new Date('01-01-1980'), + Metadata: { meta: 'value' }, + $metadata: {} as any, }); + ( + getPresignedGetObjectUrl as jest.MockedFunction< + typeof getPresignedGetObjectUrl + > + ).mockResolvedValue(mockURL); }); afterEach(() => { jest.clearAllMocks(); @@ -204,17 +220,18 @@ describe('getUrl test with path', () => { Bucket: bucket, Key: expectedKey, }; - const result = await getUrl({ + const { url, expiresAt } = await getUrlWrapper({ path, options: { validateObjectExistence: true, - } as GetUrlOptionsWithPath, + }, }); expect(getPresignedGetObjectUrl).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledTimes(1); expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); - expect(result.url).toEqual({ - url: new URL('https://google.com'), + expect({ url, expiresAt }).toEqual({ + url: mockURL, + expiresAt: expect.any(Date), }); }, ); @@ -232,7 +249,7 @@ describe('getUrl test with path', () => { }); expect.assertions(2); try { - await getUrl({ + await getUrlWrapper({ path: 'invalid_key', options: { validateObjectExistence: true }, }); diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index 0b46c6a21db..21ad76cdc33 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -2,13 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify } from '@aws-amplify/core'; +import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { listObjectsV2 } from '../../../../src/providers/s3/utils/client'; import { list } from '../../../../src/providers/s3'; import { - ListAllOptionsWithPrefix, - ListPaginateOptionsWithPrefix, + ListAllInput, + ListAllWithPathInput, + ListAllOutput, + ListAllWithPathOutput, + ListPaginateInput, + ListPaginateWithPathInput, ListPaginateOutput, + ListPaginateWithPathOutput, } from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); @@ -90,46 +95,58 @@ describe('list API', () => { }); }); describe('Prefix: Happy Cases:', () => { + const listAllWrapper = (input: ListAllInput): Promise => + list(input); + const listPaginatedWrapper = ( + input: ListPaginateInput, + ): Promise => list(input); afterEach(() => { jest.clearAllMocks(); }); - const accessLevelTests = [ + const accessLevelTests: Array<{ + prefix?: string; + expectedKey: string; + options?: { + accessLevel?: StorageAccessLevel; + targetIdentityId?: string; + }; + }> = [ { - expectedPath: `public/`, + expectedKey: `public/`, }, { options: { accessLevel: 'guest' }, - expectedPath: `public/`, + expectedKey: `public/`, }, { - key, - expectedPath: `public/${key}`, + prefix: key, + expectedKey: `public/${key}`, }, { - key, + prefix: key, options: { accessLevel: 'guest' }, - expectedPath: `public/${key}`, + expectedKey: `public/${key}`, }, { - key, + prefix: key, options: { accessLevel: 'private' }, - expectedPath: `private/${defaultIdentityId}/${key}`, + expectedKey: `private/${defaultIdentityId}/${key}`, }, { - key, + prefix: key, options: { accessLevel: 'protected' }, - expectedPath: `protected/${defaultIdentityId}/${key}`, + expectedKey: `protected/${defaultIdentityId}/${key}`, }, { - key, + prefix: key, options: { accessLevel: 'protected', targetIdentityId }, - expectedPath: `protected/${targetIdentityId}/${key}`, + expectedKey: `protected/${targetIdentityId}/${key}`, }, ]; - accessLevelTests.forEach(({ key, options, expectedPath }) => { - const pathMsg = key ? 'custom' : 'default'; + accessLevelTests.forEach(({ prefix, options, expectedKey }) => { + const pathMsg = prefix ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` @@ -137,31 +154,32 @@ describe('list API', () => { it(`should list objects with pagination, default pageSize, ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { mockListObject.mockImplementationOnce(() => { return { - Contents: [ - { ...listObjectClientBaseResultItem, Key: expectedPath }, - ], + Contents: [{ ...listObjectClientBaseResultItem, Key: expectedKey }], NextContinuationToken: nextToken, }; }); - let response = await list({ - prefix: key, - options: options as ListPaginateOptionsWithPrefix, + const response = await listPaginatedWrapper({ + prefix, + options: options, + }); + const { key, eTag, size, lastModified } = response.items[0]; + expect(response.items).toHaveLength(1); + expect({ key, eTag, size, lastModified }).toEqual({ + key: prefix ?? '', + ...listResultItem, }); - expect(response.items).toEqual([ - { ...listResultItem, key: key ?? '', path: expectedPath ?? '' }, - ]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, MaxKeys: 1000, - Prefix: expectedPath, + Prefix: expectedKey, }); }); }); - accessLevelTests.forEach(({ key, options, expectedPath }) => { - const pathMsg = key ? 'custom' : 'default'; + accessLevelTests.forEach(({ prefix, options, expectedKey }) => { + const pathMsg = prefix ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` @@ -169,37 +187,38 @@ describe('list API', () => { it(`should list objects with pagination using pageSize, nextToken, ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { mockListObject.mockImplementationOnce(() => { return { - Contents: [ - { ...listObjectClientBaseResultItem, Key: expectedPath }, - ], + Contents: [{ ...listObjectClientBaseResultItem, Key: expectedKey }], NextContinuationToken: nextToken, }; }); const customPageSize = 5; - const response: ListPaginateOutput = await list({ - prefix: key, + const response = await listPaginatedWrapper({ + prefix, options: { - ...(options as ListPaginateOptionsWithPrefix), + ...options, pageSize: customPageSize, nextToken: nextToken, }, }); - expect(response.items).toEqual([ - { ...listResultItem, key: key ?? '', path: expectedPath ?? '' }, - ]); + const { key, eTag, size, lastModified } = response.items[0]; + expect(response.items).toHaveLength(1); + expect({ key, eTag, size, lastModified }).toEqual({ + key: prefix ?? '', + ...listResultItem, + }); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, - Prefix: expectedPath, + Prefix: expectedKey, ContinuationToken: nextToken, MaxKeys: customPageSize, }); }); }); - accessLevelTests.forEach(({ key, options, expectedPath }) => { - const pathMsg = key ? 'custom' : 'default'; + accessLevelTests.forEach(({ prefix, options, expectedKey }) => { + const pathMsg = prefix ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` @@ -208,9 +227,9 @@ describe('list API', () => { mockListObject.mockImplementationOnce(() => { return {}; }); - let response = await list({ - prefix: key, - options: options as ListPaginateOptionsWithPrefix, + let response = await listPaginatedWrapper({ + prefix, + options, }); expect(response.items).toEqual([]); @@ -218,30 +237,29 @@ describe('list API', () => { expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, MaxKeys: 1000, - Prefix: expectedPath, + Prefix: expectedKey, }); }); }); - accessLevelTests.forEach(({ key, options, expectedPath }) => { - const pathMsg = key ? 'custom' : 'default'; + accessLevelTests.forEach(({ prefix: inputKey, options, expectedKey }) => { + const pathMsg = inputKey ? 'custom' : 'default'; const accessLevelMsg = options?.accessLevel ?? 'default'; const targetIdentityIdMsg = options?.targetIdentityId ? `with targetIdentityId` : ''; it(`should list all objects having three pages with ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { mockListObjectsV2ApiWithPages(3); - const result = await list({ - prefix: key, - options: { ...options, listAll: true } as ListAllOptionsWithPrefix, + const result = await listAllWrapper({ + prefix: inputKey, + options: { ...options, listAll: true }, }); - - const listResult = { + const { key, eTag, lastModified, size } = result.items[0]; + expect(result.items).toHaveLength(3); + expect({ key, eTag, lastModified, size }).toEqual({ ...listResultItem, - key: key ?? '', - path: expectedPath ?? '', - }; - expect(result.items).toEqual([listResult, listResult, listResult]); + key: inputKey ?? '', + }); expect(result).not.toHaveProperty(nextToken); // listing three times for three pages @@ -253,7 +271,7 @@ describe('list API', () => { listObjectClientConfig, { Bucket: bucket, - Prefix: expectedPath, + Prefix: expectedKey, MaxKeys: 1000, ContinuationToken: undefined, }, @@ -264,7 +282,7 @@ describe('list API', () => { listObjectClientConfig, { Bucket: bucket, - Prefix: expectedPath, + Prefix: expectedKey, MaxKeys: 1000, ContinuationToken: nextToken, }, @@ -274,106 +292,110 @@ describe('list API', () => { }); describe('Path: Happy Cases:', () => { + const listAllWrapper = ( + input: ListAllWithPathInput, + ): Promise => list(input); + const listPaginatedWrapper = ( + input: ListPaginateWithPathInput, + ): Promise => list(input); const resolvePath = (path: string | Function) => typeof path === 'string' ? path : path({ identityId: defaultIdentityId }); afterEach(() => { jest.clearAllMocks(); mockListObject.mockClear(); }); - const pathAsFunctionAndStringTests = [ + const pathTestCases = [ { path: `public/${key}`, }, { - path: ({ identityId }: any) => `protected/${identityId}/${key}`, + path: ({ identityId }: { identityId: string }) => + `protected/${identityId}/${key}`, }, ]; - it.each(pathAsFunctionAndStringTests)( + it.each(pathTestCases)( 'should list objects with pagination, default pageSize, custom path', - async ({ path }) => { - const resolvedPath = resolvePath(path); + async ({ path: inputPath }) => { + const resolvedPath = resolvePath(inputPath); mockListObject.mockImplementationOnce(() => { return { Contents: [ { ...listObjectClientBaseResultItem, - Key: resolvedPath, + Key: resolvePath(inputPath), }, ], NextContinuationToken: nextToken, }; }); - let response = await list({ - path, + const response = await listPaginatedWrapper({ + path: resolvedPath, + }); + const { path, eTag, lastModified, size } = response.items[0]; + expect(response.items).toHaveLength(1); + expect({ path, eTag, lastModified, size }).toEqual({ + ...listResultItem, + path: resolvedPath, }); - expect(response.items).toEqual([ - { - ...listResultItem, - path: resolvedPath, - key: resolvedPath, - }, - ]); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, MaxKeys: 1000, - Prefix: resolvedPath, + Prefix: resolvePath(inputPath), }); }, ); - it.each(pathAsFunctionAndStringTests)( + it.each(pathTestCases)( 'should list objects with pagination using custom pageSize, nextToken and custom path: ${path}', - async ({ path }) => { - const resolvedPath = resolvePath(path); + async ({ path: inputPath }) => { + const resolvedPath = resolvePath(inputPath); mockListObject.mockImplementationOnce(() => { return { Contents: [ { ...listObjectClientBaseResultItem, - Key: resolvedPath, + Key: resolvePath(inputPath), }, ], NextContinuationToken: nextToken, }; }); const customPageSize = 5; - const response = await list({ - path, + const response = await listPaginatedWrapper({ + path: resolvedPath, options: { pageSize: customPageSize, nextToken: nextToken, }, }); - expect(response.items).toEqual([ - { - ...listResultItem, - path: resolvedPath, - key: resolvedPath, - }, - ]); + const { path, eTag, lastModified, size } = response.items[0]; + expect(response.items).toHaveLength(1); + expect({ path, eTag, lastModified, size }).toEqual({ + ...listResultItem, + path: resolvedPath, + }); expect(response.nextToken).toEqual(nextToken); expect(listObjectsV2).toHaveBeenCalledTimes(1); expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, - Prefix: resolvedPath, + Prefix: resolvePath(inputPath), ContinuationToken: nextToken, MaxKeys: customPageSize, }); }, ); - it.each(pathAsFunctionAndStringTests)( + it.each(pathTestCases)( 'should list objects with zero results with custom path: ${path}', async ({ path }) => { - const resolvedPath = resolvePath(path); mockListObject.mockImplementationOnce(() => { return {}; }); - let response = await list({ - path, + let response = await listPaginatedWrapper({ + path: resolvePath(path), }); expect(response.items).toEqual([]); @@ -381,26 +403,28 @@ describe('list API', () => { expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { Bucket: bucket, MaxKeys: 1000, - Prefix: resolvedPath, + Prefix: resolvePath(path), }); }, ); - it.each(pathAsFunctionAndStringTests)( + it.each(pathTestCases)( 'should list all objects having three pages with custom path: ${path}', - async ({ path }) => { - const resolvedPath = resolvePath(path); + async ({ path: inputPath }) => { + const resolvedPath = resolvePath(inputPath); mockListObjectsV2ApiWithPages(3); - const result = await list({ - path, + const result = await listAllWrapper({ + path: resolvedPath, options: { listAll: true }, }); const listResult = { - ...listResultItem, path: resolvedPath, - key: resolvedPath, + ...listResultItem, }; + const { path, lastModified, eTag, size } = result.items[0]; + expect(result.items).toHaveLength(3); + expect({ path, lastModified, eTag, size }).toEqual(listResult); expect(result.items).toEqual([listResult, listResult, listResult]); expect(result).not.toHaveProperty(nextToken); diff --git a/packages/storage/__tests__/providers/s3/apis/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/remove.test.ts index 96abf2f976d..0c8662492ac 100644 --- a/packages/storage/__tests__/providers/s3/apis/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/remove.test.ts @@ -2,11 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify } from '@aws-amplify/core'; +import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { deleteObject } from '../../../../src/providers/s3/utils/client'; import { remove } from '../../../../src/providers/s3/apis'; -import { StorageOptions } from '../../../../src/types'; import { StorageValidationErrorCode } from '../../../../src/errors/types/validation'; +import { + RemoveInput, + RemoveWithPathInput, + RemoveOutput, + RemoveWithPathOutput, +} from '../../../../src/providers/s3/types'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -23,7 +28,7 @@ jest.mock('@aws-amplify/core', () => ({ const mockDeleteObject = deleteObject as jest.Mock; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; -const key = 'key'; +const inputKey = 'key'; const bucket = 'bucket'; const region = 'region'; const defaultIdentityId = 'defaultIdentityId'; @@ -55,6 +60,9 @@ describe('remove API', () => { }); describe('Happy Cases', () => { describe('With Key', () => { + const removeWrapper = (input: RemoveInput): Promise => + remove(input); + beforeEach(() => { mockDeleteObject.mockImplementation(() => { return { @@ -65,31 +73,36 @@ describe('remove API', () => { afterEach(() => { jest.clearAllMocks(); }); - [ + const testCases: Array<{ + expectedKey: string; + options?: { accessLevel?: StorageAccessLevel }; + }> = [ { - expectedKey: `public/${key}`, + expectedKey: `public/${inputKey}`, }, { options: { accessLevel: 'guest' }, - expectedKey: `public/${key}`, + expectedKey: `public/${inputKey}`, }, { options: { accessLevel: 'private' }, - expectedKey: `private/${defaultIdentityId}/${key}`, + expectedKey: `private/${defaultIdentityId}/${inputKey}`, }, { options: { accessLevel: 'protected' }, - expectedKey: `protected/${defaultIdentityId}/${key}`, + expectedKey: `protected/${defaultIdentityId}/${inputKey}`, }, - ].forEach(({ options, expectedKey }) => { + ]; + + testCases.forEach(({ options, expectedKey }) => { const accessLevel = options?.accessLevel ?? 'default'; - const removeResultKey = { key }; it(`should remove object with ${accessLevel} accessLevel`, async () => { - expect.assertions(3); - expect( - await remove({ key, options: options as StorageOptions }), - ).toEqual({ ...removeResultKey, path: expectedKey }); + const { key } = await removeWrapper({ + key: inputKey, + options: options, + }); + expect(key).toEqual(inputKey); expect(deleteObject).toHaveBeenCalledTimes(1); expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { Bucket: bucket, @@ -99,6 +112,9 @@ describe('remove API', () => { }); }); describe('With Path', () => { + const removeWrapper = ( + input: RemoveWithPathInput, + ): Promise => remove(input); beforeEach(() => { mockDeleteObject.mockImplementation(() => { return { @@ -111,35 +127,32 @@ describe('remove API', () => { }); [ { - path: `public/${key}`, + path: `public/${inputKey}`, }, { - path: ({ identityId }: any) => `protected/${identityId}/${key}`, + path: ({ identityId }: { identityId?: string }) => + `protected/${identityId}/${inputKey}`, }, - ].forEach(({ path }) => { - const resolvePath = - typeof path === 'string' - ? path - : path({ identityId: defaultIdentityId }); - const removeResultPath = { - path: resolvePath, - key: resolvePath, - }; + ].forEach(({ path: inputPath }) => { + const resolvedPath = + typeof inputPath === 'string' + ? inputPath + : inputPath({ identityId: defaultIdentityId }); it(`should remove object for the given path`, async () => { - expect.assertions(3); - expect(await remove({ path })).toEqual(removeResultPath); + const { path } = await removeWrapper({ path: inputPath }); + expect(path).toEqual(resolvedPath); expect(deleteObject).toHaveBeenCalledTimes(1); expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { Bucket: bucket, - Key: resolvePath, + Key: resolvedPath, }); }); }); }); }); - describe('Error Cases', () => { + describe('Error Cases:', () => { afterEach(() => { jest.clearAllMocks(); }); diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts index df818c254c8..211d3238a35 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts @@ -10,6 +10,7 @@ import { } from '../../../../../src/errors/types/validation'; import { putObjectJob } from '../../../../../src/providers/s3/apis/uploadData/putObjectJob'; import { getMultipartUploadHandlers } from '../../../../../src/providers/s3/apis/uploadData/multipart'; +import { UploadDataInput, UploadDataWithPathInput } from '../../../../../src'; jest.mock('../../../../../src/providers/s3/utils/'); jest.mock('../../../../../src/providers/s3/apis/uploadData/putObjectJob'); @@ -35,12 +36,11 @@ describe('uploadData with key', () => { describe('validation', () => { it('should throw if data size is too big', async () => { - expect(() => - uploadData({ - key: 'key', - data: { size: MAX_OBJECT_SIZE + 1 } as any, - }), - ).toThrow( + const mockUploadInput: UploadDataInput = { + key: 'key', + data: { size: MAX_OBJECT_SIZE + 1 } as any, + }; + expect(() => uploadData(mockUploadInput)).toThrow( expect.objectContaining( validationErrorMap[StorageValidationErrorCode.ObjectIsTooLarge], ), @@ -131,12 +131,11 @@ describe('uploadData with path', () => { describe('validation', () => { it('should throw if data size is too big', async () => { - expect(() => - uploadData({ - path: testPath, - data: { size: MAX_OBJECT_SIZE + 1 } as any, - }), - ).toThrow( + const mockUploadInput: UploadDataWithPathInput = { + path: testPath, + data: { size: MAX_OBJECT_SIZE + 1 } as any, + }; + expect(() => uploadData(mockUploadInput)).toThrow( expect.objectContaining( validationErrorMap[StorageValidationErrorCode.ObjectIsTooLarge], ), @@ -154,7 +153,7 @@ describe('uploadData with path', () => { describe('use putObject for small uploads', () => { const smallData = { size: 5 * 1024 * 1024 } as any; - + test.each([ { path: testPath, @@ -163,22 +162,22 @@ describe('uploadData with path', () => { path: () => testPath, }, ])( - 'should use putObject if data size is <= 5MB when path is $path', + 'should use putObject if data size is <= 5MB when path is $path', async ({ path }) => { const testInput = { path, data: smallData, - } + }; uploadData(testInput); expect(mockPutObjectJob).toHaveBeenCalledWith( - testInput, - expect.any(AbortSignal), - expect.any(Number) + testInput, + expect.any(AbortSignal), + expect.any(Number), ); expect(mockGetMultipartUploadHandlers).not.toHaveBeenCalled(); - } + }, ); it('should use uploadTask', async () => { @@ -207,12 +206,15 @@ describe('uploadData with path', () => { const testInput = { path: testPath, data: biggerData, - } + }; uploadData(testInput); expect(mockPutObjectJob).not.toHaveBeenCalled(); - expect(mockGetMultipartUploadHandlers).toHaveBeenCalledWith(testInput, expect.any(Number)); + expect(mockGetMultipartUploadHandlers).toHaveBeenCalledWith( + testInput, + expect.any(Number), + ); }); it('should use uploadTask', async () => { 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 96e680088bb..302d76beaa8 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts @@ -678,10 +678,11 @@ describe('getMultipartUploadHandlers with path', () => { expectedKey: testPath, }, { - path: ({identityId}: any) => `testPath/${identityId}/object`, + path: ({ identityId }: { identityId?: string }) => + `testPath/${identityId}/object`, expectedKey: `testPath/${defaultIdentityId}/object`, }, - ].forEach(({ path, expectedKey }) => { + ].forEach(({ path: inputPath, expectedKey }) => { it.each([ ['file', new File([getBlob(8 * MB)], 'someName')], ['blob', getBlob(8 * MB)], @@ -693,7 +694,7 @@ describe('getMultipartUploadHandlers with path', () => { async (_, twoPartsPayload) => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers({ - path: path, + path: inputPath, data: twoPartsPayload, }); const result = await multipartUploadJob(); @@ -910,7 +911,9 @@ describe('getMultipartUploadHandlers with path', () => { const lastModifiedRegex = /someName_\d{13}_/; expect(Object.keys(cacheValue)).toEqual([ - expect.stringMatching(new RegExp(lastModifiedRegex.source + testPathCacheKey)), + expect.stringMatching( + new RegExp(lastModifiedRegex.source + testPathCacheKey), + ), ]); }); 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 6106ff8fa46..b03822946da 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts @@ -57,25 +57,24 @@ mockPutObject.mockResolvedValue({ describe('putObjectJob with key', () => { it('should supply the correct parameters to putObject API handler', async () => { const abortController = new AbortController(); - const key = 'key'; - const finalKey = `public/${key}`; + const inputKey = 'key'; const data = 'data'; - const contentType = 'contentType'; + const mockContentType = 'contentType'; const contentDisposition = 'contentDisposition'; const contentEncoding = 'contentEncoding'; - const metadata = { key: 'value' }; + const mockMetadata = { key: 'value' }; const onProgress = jest.fn(); const useAccelerateEndpoint = true; const job = putObjectJob( { - key, + key: inputKey, data, options: { contentDisposition, contentEncoding, - contentType, - metadata, + contentType: mockContentType, + metadata: mockMetadata, onProgress, useAccelerateEndpoint, }, @@ -84,8 +83,7 @@ describe('putObjectJob with key', () => { ); const result = await job(); expect(result).toEqual({ - key, - path: finalKey, + key: inputKey, eTag: 'eTag', versionId: 'versionId', contentType: 'contentType', @@ -103,12 +101,12 @@ describe('putObjectJob with key', () => { }, { Bucket: 'bucket', - Key: finalKey, + Key: `public/${inputKey}`, Body: data, - ContentType: contentType, + ContentType: mockContentType, ContentDisposition: contentDisposition, ContentEncoding: contentEncoding, - Metadata: metadata, + Metadata: mockMetadata, ContentMD5: undefined, }, ); @@ -146,25 +144,25 @@ describe('putObjectJob with path', () => { }, ])( 'should supply the correct parameters to putObject API handler when path is $path', - async ({ path, expectedKey }) => { + async ({ path: inputPath, expectedKey }) => { const abortController = new AbortController(); const data = 'data'; - const contentType = 'contentType'; + const mockContentType = 'contentType'; const contentDisposition = 'contentDisposition'; const contentEncoding = 'contentEncoding'; - const metadata = { key: 'value' }; + const mockMetadata = { key: 'value' }; const onProgress = jest.fn(); const useAccelerateEndpoint = true; const job = putObjectJob( { - path, + path: inputPath, data, options: { contentDisposition, contentEncoding, - contentType, - metadata, + contentType: mockContentType, + metadata: mockMetadata, onProgress, useAccelerateEndpoint, }, @@ -174,7 +172,6 @@ describe('putObjectJob with path', () => { const result = await job(); expect(result).toEqual({ path: expectedKey, - key: expectedKey, eTag: 'eTag', versionId: 'versionId', contentType: 'contentType', @@ -194,10 +191,10 @@ describe('putObjectJob with path', () => { Bucket: 'bucket', Key: expectedKey, Body: data, - ContentType: contentType, + ContentType: mockContentType, ContentDisposition: contentDisposition, ContentEncoding: contentEncoding, - Metadata: metadata, + Metadata: mockMetadata, ContentMD5: undefined, }, ); diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 6183ed3a2d5..45bf9734a66 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -13,24 +13,40 @@ export { export { UploadDataInput, + UploadDataWithPathInput, DownloadDataInput, + DownloadDataWithPathInput, RemoveInput, + RemoveWithPathInput, ListAllInput, + ListAllWithPathInput, ListPaginateInput, + ListPaginateWithPathInput, GetPropertiesInput, + GetPropertiesWithPathInput, CopyInput, + CopyWithPathInput, GetUrlInput, + GetUrlWithPathInput, } from './providers/s3/types/inputs'; export { UploadDataOutput, + UploadDataWithPathOutput, DownloadDataOutput, + DownloadDataWithPathOutput, RemoveOutput, + RemoveWithPathOutput, ListAllOutput, + ListAllWithPathOutput, ListPaginateOutput, + ListPaginateWithPathOutput, GetPropertiesOutput, + GetPropertiesWithPathOutput, CopyOutput, + CopyWithPathOutput, GetUrlOutput, + GetUrlWithPathOutput, } from './providers/s3/types/outputs'; export { TransferProgressEvent } from './types'; diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index 19cfbf034db..763ff45829b 100644 --- a/packages/storage/src/providers/s3/apis/copy.ts +++ b/packages/storage/src/providers/s3/apis/copy.ts @@ -5,41 +5,38 @@ import { Amplify } from '@aws-amplify/core'; import { CopyInput, - CopyInputWithKey, - CopyInputWithPath, CopyOutput, + CopyWithPathInput, + CopyWithPathOutput, } from '../types'; import { copy as copyInternal } from './internal/copy'; -interface Copy { - /** - * Copy an object from a source to a destination object within the same bucket. - * - * @param input - The CopyInputWithPath object. - * @returns Output containing the destination object path. - * @throws service: `S3Exception` - Thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination path is not defined. - */ - (input: CopyInputWithPath): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. - * - * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across - * different accessLevel or identityId (if source object's accessLevel is 'protected'). - * - * @param input - The CopyInputWithKey object. - * @returns Output containing the destination object key. - * @throws service: `S3Exception` - Thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination key is not defined. - */ - (input: CopyInputWithKey): Promise; - (input: CopyInput): Promise; -} +/** + * Copy an object from a source to a destination object within the same bucket. + * + * @param input - The `CopyWithPathInput` object. + * @returns Output containing the destination object path. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination path is not defined. + */ +export function copy(input: CopyWithPathInput): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. + * + * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across + * different accessLevel or identityId (if source object's accessLevel is 'protected'). + * + * @param input - The `CopyInput` object. + * @returns Output containing the destination object key. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination key is not defined. + */ +export function copy(input: CopyInput): Promise; -export const copy: Copy = ( - input: CopyInput, -): Promise => copyInternal(Amplify, input) as Promise; +export function copy(input: CopyInput | CopyWithPathInput) { + return copyInternal(Amplify, input); +} diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index ff12d6b55c3..7c98ee2b857 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -6,91 +6,93 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { DownloadDataInput, - DownloadDataInputWithKey, - DownloadDataInputWithPath, DownloadDataOutput, - ItemWithKeyAndPath, + DownloadDataWithPathInput, + DownloadDataWithPathOutput, } from '../types'; import { resolveS3ConfigAndInput } from '../utils/resolveS3ConfigAndInput'; import { createDownloadTask, validateStorageOperationInput } from '../utils'; import { getObject } from '../utils/client'; import { getStorageUserAgentValue } from '../utils/userAgent'; import { logger } from '../../../utils'; -import { StorageDownloadDataOutput } from '../../../types'; +import { + StorageDownloadDataOutput, + StorageItemWithKey, + StorageItemWithPath, +} from '../../../types'; import { STORAGE_INPUT_KEY } from '../utils/constants'; -interface DownloadData { - /** - * Download S3 object data to memory - * - * @param input - The DownloadDataInputWithPath object. - * @returns A cancelable task exposing result promise from `result` property. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * - * @example - * ```ts - * // Download a file from s3 bucket - * const { body, eTag } = await downloadData({ path, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * @example - * ```ts - * // Cancel a task - * const downloadTask = downloadData({ path }); - * //... - * downloadTask.cancel(); - * try { - * await downloadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - */ - (input: DownloadDataInputWithPath): DownloadDataOutput; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. - * - * Download S3 object data to memory - * - * @param input - The DownloadDataInputWithKey object. - * @returns A cancelable task exposing result promise from `result` property. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * - * @example - * ```ts - * // Download a file from s3 bucket - * const { body, eTag } = await downloadData({ key, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * @example - * ```ts - * // Cancel a task - * const downloadTask = downloadData({ key }); - * //... - * downloadTask.cancel(); - * try { - * await downloadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - */ - (input: DownloadDataInputWithKey): DownloadDataOutput; - (input: DownloadDataInput): DownloadDataOutput; -} +/** + * Download S3 object data to memory + * + * @param input - The `DownloadDataWithPathInput` object. + * @returns A cancelable task exposing result promise from `result` property. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * + * @example + * ```ts + * // Download a file from s3 bucket + * const { body, eTag } = await downloadData({ path, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * @example + * ```ts + * // Cancel a task + * const downloadTask = downloadData({ path }); + * //... + * downloadTask.cancel(); + * try { + * await downloadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + */ +export function downloadData( + input: DownloadDataWithPathInput, +): DownloadDataWithPathOutput; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. + * + * Download S3 object data to memory + * + * @param input - The `DownloadDataInput` object. + * @returns A cancelable task exposing result promise from `result` property. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * + * @example + * ```ts + * // Download a file from s3 bucket + * const { body, eTag } = await downloadData({ key, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * @example + * ```ts + * // Cancel a task + * const downloadTask = downloadData({ key }); + * //... + * downloadTask.cancel(); + * try { + * await downloadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + */ +export function downloadData(input: DownloadDataInput): DownloadDataOutput; -export const downloadData: DownloadData = ( - input: DownloadDataInput, -): Output => { +export function downloadData( + input: DownloadDataInput | DownloadDataWithPathInput, +) { const abortController = new AbortController(); const downloadTask = createDownloadTask({ @@ -100,12 +102,17 @@ export const downloadData: DownloadData = ( }, }); - return downloadTask as Output; -}; + return downloadTask; +} const downloadDataJob = - (downloadDataInput: DownloadDataInput, abortSignal: AbortSignal) => - async (): Promise> => { + ( + downloadDataInput: DownloadDataInput | DownloadDataWithPathInput, + abortSignal: AbortSignal, + ) => + async (): Promise< + StorageDownloadDataOutput + > => { const { options: downloadDataOptions } = downloadDataInput; const { bucket, keyPrefix, s3Config, identityId } = await resolveS3ConfigAndInput(Amplify, downloadDataOptions); @@ -153,6 +160,6 @@ const downloadDataJob = }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, path: finalKey, ...result } - : { path: finalKey, key: finalKey, ...result }; + ? { key: objectKey, ...result } + : { path: objectKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/apis/getProperties.ts b/packages/storage/src/providers/s3/apis/getProperties.ts index c5e866922b4..630d0b1c467 100644 --- a/packages/storage/src/providers/s3/apis/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/getProperties.ts @@ -5,42 +5,43 @@ import { Amplify } from '@aws-amplify/core'; import { GetPropertiesInput, - GetPropertiesInputWithKey, - GetPropertiesInputWithPath, GetPropertiesOutput, + GetPropertiesWithPathInput, + GetPropertiesWithPathOutput, } from '../types'; import { getProperties as getPropertiesInternal } from './internal/getProperties'; -interface GetProperties { - /** - * Gets the properties of a file. The properties include S3 system metadata and - * the user metadata that was provided when uploading the file. - * - * @param input - The `GetPropertiesInputWithPath` object. - * @returns Requested object properties. - * @throws An `S3Exception` when the underlying S3 service returned error. - * @throws A `StorageValidationErrorCode` when API call parameters are invalid. - */ - (input: GetPropertiesInputWithPath): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. - * - * Gets the properties of a file. The properties include S3 system metadata and - * the user metadata that was provided when uploading the file. - * - * @param input - The `GetPropertiesInputWithKey` object. - * @returns Requested object properties. - * @throws An `S3Exception` when the underlying S3 service returned error. - * @throws A `StorageValidationErrorCode` when API call parameters are invalid. - */ - (input: GetPropertiesInputWithKey): Promise; - (input: GetPropertiesInput): Promise; -} - -export const getProperties: GetProperties = < - Output extends GetPropertiesOutput, ->( +/** + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param input - The `GetPropertiesWithPathInput` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ +export function getProperties( + input: GetPropertiesWithPathInput, +): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. + * + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param input - The `GetPropertiesInput` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ +export function getProperties( input: GetPropertiesInput, -): Promise => getPropertiesInternal(Amplify, input) as Promise; +): Promise; + +export function getProperties( + input: GetPropertiesInput | GetPropertiesWithPathInput, +) { + return getPropertiesInternal(Amplify, input); +} diff --git a/packages/storage/src/providers/s3/apis/getUrl.ts b/packages/storage/src/providers/s3/apis/getUrl.ts index 24874da81d2..aafe1f282b3 100644 --- a/packages/storage/src/providers/s3/apis/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/getUrl.ts @@ -5,53 +5,53 @@ import { Amplify } from '@aws-amplify/core'; import { GetUrlInput, - GetUrlInputWithKey, - GetUrlInputWithPath, GetUrlOutput, + GetUrlWithPathInput, + GetUrlWithPathOutput, } from '../types'; import { getUrl as getUrlInternal } from './internal/getUrl'; -interface GetUrl { - /** - * Get a temporary presigned URL to download the specified S3 object. - * The presigned URL expires when the associated role used to sign the request expires or - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * to true, this method will verify the given object already exists in S3 before returning a presigned - * URL, and will throw `StorageError` if the object does not exist. - * - * @param input - The `GetUrlInputWithPath` object. - * @returns Presigned URL and timestamp when the URL MAY expire. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ - (input: GetUrlInputWithPath): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. - * - * Get a temporary presigned URL to download the specified S3 object. - * The presigned URL expires when the associated role used to sign the request expires or - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * to true, this method will verify the given object already exists in S3 before returning a presigned - * URL, and will throw `StorageError` if the object does not exist. - * - * @param input - The `GetUrlInputWithKey` object. - * @returns Presigned URL and timestamp when the URL MAY expire. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ - (input: GetUrlInputWithKey): Promise; - (input: GetUrlInput): Promise; -} +/** + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param input - The `GetUrlWithPathInput` object. + * @returns Presigned URL and timestamp when the URL may expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ +export function getUrl( + input: GetUrlWithPathInput, +): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. + * + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param input - The `GetUrlInput` object. + * @returns Presigned URL and timestamp when the URL may expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ +export function getUrl(input: GetUrlInput): Promise; -export const getUrl: GetUrl = (input: GetUrlInput): Promise => - getUrlInternal(Amplify, input); +export function getUrl(input: GetUrlInput | GetUrlWithPathInput) { + return getUrlInternal(Amplify, input); +} diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts index 7882a3429dc..e0c96a1fba4 100644 --- a/packages/storage/src/providers/s3/apis/internal/copy.ts +++ b/packages/storage/src/providers/s3/apis/internal/copy.ts @@ -6,9 +6,9 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { CopyInput, - CopyInputWithKey, - CopyInputWithPath, CopyOutput, + CopyWithPathInput, + CopyWithPathOutput, } from '../../types'; import { ResolvedS3Config } from '../../types/options'; import { @@ -22,13 +22,14 @@ import { copyObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; -const isCopyInputWithPath = (input: CopyInput): input is CopyInputWithPath => - isInputWithPath(input.source); +const isCopyInputWithPath = ( + input: CopyInput | CopyWithPathInput, +): input is CopyWithPathInput => isInputWithPath(input.source); export const copy = async ( amplify: AmplifyClassV6, - input: CopyInput, -): Promise => { + input: CopyInput | CopyWithPathInput, +): Promise => { return isCopyInputWithPath(input) ? copyWithPath(amplify, input) : copyWithKey(amplify, input); @@ -36,8 +37,8 @@ export const copy = async ( const copyWithPath = async ( amplify: AmplifyClassV6, - input: CopyInputWithPath, -): Promise => { + input: CopyWithPathInput, +): Promise => { const { source, destination } = input; const { s3Config, bucket, identityId } = await resolveS3ConfigAndInput(amplify); @@ -68,13 +69,13 @@ const copyWithPath = async ( s3Config, }); - return { path: finalCopyDestination, key: finalCopyDestination }; + return { path: finalCopyDestination }; }; /** @deprecated Use {@link copyWithPath} instead. */ export const copyWithKey = async ( amplify: AmplifyClassV6, - input: CopyInputWithKey, + input: CopyInput, ): Promise => { const { source: { key: sourceKey }, @@ -111,7 +112,6 @@ export const copyWithKey = async ( return { key: destinationKey, - path: finalCopyDestination, }; }; diff --git a/packages/storage/src/providers/s3/apis/internal/getProperties.ts b/packages/storage/src/providers/s3/apis/internal/getProperties.ts index e2dd8a19780..3b61460d89b 100644 --- a/packages/storage/src/providers/s3/apis/internal/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/internal/getProperties.ts @@ -4,7 +4,12 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { GetPropertiesInput, GetPropertiesOutput } from '../../types'; +import { + GetPropertiesInput, + GetPropertiesOutput, + GetPropertiesWithPathInput, + GetPropertiesWithPathOutput, +} from '../../types'; import { resolveS3ConfigAndInput, validateStorageOperationInput, @@ -16,9 +21,9 @@ import { STORAGE_INPUT_KEY } from '../../utils/constants'; export const getProperties = async ( amplify: AmplifyClassV6, - input: GetPropertiesInput, + input: GetPropertiesInput | GetPropertiesWithPathInput, action?: StorageAction, -): Promise => { +): Promise => { const { options: getPropertiesOptions } = input; const { s3Config, bucket, keyPrefix, identityId } = await resolveS3ConfigAndInput(amplify, getPropertiesOptions); @@ -53,6 +58,6 @@ export const getProperties = async ( }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, path: finalKey, ...result } - : { path: finalKey, key: finalKey, ...result }; + ? { key: objectKey, ...result } + : { path: objectKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/apis/internal/getUrl.ts b/packages/storage/src/providers/s3/apis/internal/getUrl.ts index ea2fd60d741..a2de5d3f770 100644 --- a/packages/storage/src/providers/s3/apis/internal/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/internal/getUrl.ts @@ -4,7 +4,12 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { GetUrlInput, GetUrlOutput } from '../../types'; +import { + GetUrlInput, + GetUrlOutput, + GetUrlWithPathInput, + GetUrlWithPathOutput, +} from '../../types'; import { StorageValidationErrorCode } from '../../../../errors/types/validation'; import { getPresignedGetObjectUrl } from '../../utils/client'; import { @@ -22,8 +27,8 @@ import { getProperties } from './getProperties'; export const getUrl = async ( amplify: AmplifyClassV6, - input: GetUrlInput, -): Promise => { + input: GetUrlInput | GetUrlWithPathInput, +): Promise => { const { options: getUrlOptions } = input; const { s3Config, keyPrefix, bucket, identityId } = await resolveS3ConfigAndInput(amplify, getUrlOptions); @@ -36,16 +41,7 @@ export const getUrl = async ( inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey; if (getUrlOptions?.validateObjectExistence) { - await getProperties( - amplify, - { - options: getUrlOptions, - ...((inputType === STORAGE_INPUT_KEY - ? { key: input.key } - : { path: input.path }) as GetUrlInput), - }, - StorageAction.GetUrl, - ); + await getProperties(amplify, input, StorageAction.GetUrl); } let urlExpirationInSec = diff --git a/packages/storage/src/providers/s3/apis/internal/list.ts b/packages/storage/src/providers/s3/apis/internal/list.ts index bf17316d495..f180dfe5247 100644 --- a/packages/storage/src/providers/s3/apis/internal/list.ts +++ b/packages/storage/src/providers/s3/apis/internal/list.ts @@ -7,8 +7,14 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { ListAllInput, ListAllOutput, + ListAllWithPathInput, + ListAllWithPathOutput, + ListOutputItem, + ListOutputItemWithPath, ListPaginateInput, ListPaginateOutput, + ListPaginateWithPathInput, + ListPaginateWithPathOutput, } from '../../types'; import { resolveS3ConfigAndInput, @@ -23,7 +29,6 @@ import { import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; import { STORAGE_INPUT_PREFIX } from '../../utils/constants'; -import { ListOutputItem } from '../../types/outputs'; const MAX_PAGE_SIZE = 1000; @@ -35,8 +40,17 @@ interface ListInputArgs { export const list = async ( amplify: AmplifyClassV6, - input: ListAllInput | ListPaginateInput, -): Promise => { + input: + | ListAllInput + | ListPaginateInput + | ListAllWithPathInput + | ListPaginateWithPathInput, +): Promise< + | ListAllOutput + | ListPaginateOutput + | ListAllWithPathOutput + | ListPaginateWithPathOutput +> => { const { options = {} } = input; const { s3Config, @@ -145,19 +159,14 @@ const _listWithPrefix = async ({ } return { - items: response.Contents.map(item => { - const finalKey = generatedPrefix + items: response.Contents.map(item => ({ + key: generatedPrefix ? item.Key!.substring(generatedPrefix.length) - : item.Key!; - - return { - key: finalKey, - path: item.Key!, - eTag: item.ETag, - lastModified: item.LastModified, - size: item.Size, - }; - }), + : item.Key!, + eTag: item.ETag, + lastModified: item.LastModified, + size: item.Size, + })), nextToken: response.NextContinuationToken, }; }; @@ -165,8 +174,8 @@ const _listWithPrefix = async ({ const _listAllWithPath = async ({ s3Config, listParams, -}: ListInputArgs): Promise => { - const listResult: ListOutputItem[] = []; +}: ListInputArgs): Promise => { + const listResult: ListOutputItemWithPath[] = []; let continuationToken = listParams.ContinuationToken; do { const { items: pageResults, nextToken: pageNextToken } = @@ -190,7 +199,7 @@ const _listAllWithPath = async ({ const _listWithPath = async ({ s3Config, listParams, -}: ListInputArgs): Promise => { +}: ListInputArgs): Promise => { const listParamsClone = { ...listParams }; if (!listParamsClone.MaxKeys || listParamsClone.MaxKeys > MAX_PAGE_SIZE) { logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`); @@ -214,7 +223,6 @@ const _listWithPath = async ({ return { items: response.Contents.map(item => ({ path: item.Key!, - key: item.Key!, eTag: item.ETag, lastModified: item.LastModified, size: item.Size, diff --git a/packages/storage/src/providers/s3/apis/internal/remove.ts b/packages/storage/src/providers/s3/apis/internal/remove.ts index 492b35c2362..bc0fa4a2ade 100644 --- a/packages/storage/src/providers/s3/apis/internal/remove.ts +++ b/packages/storage/src/providers/s3/apis/internal/remove.ts @@ -4,7 +4,12 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { RemoveInput, RemoveOutput } from '../../types'; +import { + RemoveInput, + RemoveOutput, + RemoveWithPathInput, + RemoveWithPathOutput, +} from '../../types'; import { resolveS3ConfigAndInput, validateStorageOperationInput, @@ -16,8 +21,8 @@ import { STORAGE_INPUT_KEY } from '../../utils/constants'; export const remove = async ( amplify: AmplifyClassV6, - input: RemoveInput, -): Promise => { + input: RemoveInput | RemoveWithPathInput, +): Promise => { const { options = {} } = input ?? {}; const { s3Config, keyPrefix, bucket, identityId } = await resolveS3ConfigAndInput(amplify, options); @@ -50,10 +55,8 @@ export const remove = async ( return inputType === STORAGE_INPUT_KEY ? { key: objectKey, - path: finalKey, } : { - path: finalKey, - key: finalKey, + path: objectKey, }; }; diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index 2383ebfcca1..cd58dbdaacd 100644 --- a/packages/storage/src/providers/s3/apis/list.ts +++ b/packages/storage/src/providers/s3/apis/list.ts @@ -4,62 +4,66 @@ import { Amplify } from '@aws-amplify/core'; import { ListAllInput, - ListAllInputWithPath, - ListAllInputWithPrefix, ListAllOutput, + ListAllWithPathInput, + ListAllWithPathOutput, ListPaginateInput, - ListPaginateInputWithPath, - ListPaginateInputWithPrefix, ListPaginateOutput, + ListPaginateWithPathInput, + ListPaginateWithPathOutput, } from '../types'; import { list as listInternal } from './internal/list'; -interface ListApi { - /** - * List files in pages with the given `path`. - * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputWithPath` object. - * @returns A list of objects with path and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - (input: ListPaginateInputWithPath): Promise; - /** - * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputWithPath` object. - * @returns A list of all objects with path and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - (input: ListAllInputWithPath): Promise; - /** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List files in pages with the given `prefix`. - * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputWithPrefix` object. - * @returns A list of objects with key and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - (input?: ListPaginateInputWithPrefix): Promise; - /** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputWithPrefix` object. - * @returns A list of all objects with key and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - (input?: ListAllInputWithPrefix): Promise; - (input?: ListAllInput): Promise; - (input?: ListPaginateInput): Promise; -} +/** + * List files in pages with the given `path`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateWithPathInput` object. + * @returns A list of objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list( + input: ListPaginateWithPathInput, +): Promise; +/** + * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllWithPathInput` object. + * @returns A list of all objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list( + input: ListAllWithPathInput, +): Promise; +/** + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List files in pages with the given `prefix`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateInput` object. + * @returns A list of objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list(input?: ListPaginateInput): Promise; +/** + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllInput` object. + * @returns A list of all objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list(input?: ListAllInput): Promise; -export const list: ListApi = < - Output extends ListAllOutput | ListPaginateOutput, ->( - input?: ListAllInput | ListPaginateInput, -): Promise => listInternal(Amplify, input ?? {}) as Promise; +export function list( + input?: + | ListAllInput + | ListPaginateInput + | ListAllWithPathInput + | ListPaginateWithPathInput, +) { + return listInternal(Amplify, input ?? {}); +} diff --git a/packages/storage/src/providers/s3/apis/remove.ts b/packages/storage/src/providers/s3/apis/remove.ts index e0e100d2d4a..c0526df854c 100644 --- a/packages/storage/src/providers/s3/apis/remove.ts +++ b/packages/storage/src/providers/s3/apis/remove.ts @@ -5,38 +5,37 @@ import { Amplify } from '@aws-amplify/core'; import { RemoveInput, - RemoveInputWithKey, - RemoveInputWithPath, RemoveOutput, + RemoveWithPathInput, + RemoveWithPathOutput, } from '../types'; import { remove as removeInternal } from './internal/remove'; -interface RemoveApi { - /** - * Remove a file from your S3 bucket. - * @param input - The `RemoveInputWithPath` object. - * @return Output containing the removed object path. - * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. - * @throws validation: `StorageValidationErrorCode` - Validation errors thrown - * when there is no path or path is empty or path has a leading slash. - */ - (input: RemoveInputWithPath): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. - * - * Remove a file from your S3 bucket. - * @param input - The `RemoveInputWithKey` object. - * @return Output containing the removed object key - * @throws service: `S3Exception` - S3 service errors thrown while while removing the object - * @throws validation: `StorageValidationErrorCode` - Validation errors thrown - * when there is no key or its empty. - */ - (input: RemoveInputWithKey): Promise; - (input: RemoveInput): Promise; -} +/** + * Remove a file from your S3 bucket. + * @param input - The `RemoveWithPathInput` object. + * @return Output containing the removed object path. + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no path or path is empty or path has a leading slash. + */ +export function remove( + input: RemoveWithPathInput, +): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. + * + * Remove a file from your S3 bucket. + * @param input - The `RemoveInput` object. + * @return Output containing the removed object key + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no key or its empty. + */ +export function remove(input: RemoveInput): Promise; -export const remove: RemoveApi = ( - input: RemoveInput, -): Promise => removeInternal(Amplify, input) as Promise; +export function remove(input: RemoveInput | RemoveWithPathInput) { + return removeInternal(Amplify, input); +} diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index b241a808a58..e9486e10431 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -7,57 +7,48 @@ import { import { CopyInput, - CopyInputWithKey, - CopyInputWithPath, CopyOutput, + CopyWithPathInput, + CopyWithPathOutput, } from '../../types'; import { copy as copyInternal } from '../internal/copy'; -interface Copy { - /** - * Copy an object from a source to a destination object within the same bucket. - * - * @param contextSpec - The isolated server context. - * @param input - The CopyInputWithPath object. - * @returns Output containing the destination object path. - * @throws service: `S3Exception` - Thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination path is not defined. - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: CopyInputWithPath, - ): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. - * - * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across - * different accessLevel or identityId (if source object's accessLevel is 'protected'). - * - * @param contextSpec - The isolated server context. - * @param input - The CopyInputWithKey object. - * @returns Output containing the destination object key. - * @throws service: `S3Exception` - Thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination key is not defined. - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: CopyInputWithKey, - ): Promise; - - ( - contextSpec: AmplifyServer.ContextSpec, - input: CopyInput, - ): Promise; -} - -export const copy: Copy = ( +/** + * Copy an object from a source to a destination object within the same bucket. + * + * @param contextSpec - The isolated server context. + * @param input - The `CopyWithPathInput` object. + * @returns Output containing the destination object path. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination path is not defined. + */ +export function copy( + contextSpec: AmplifyServer.ContextSpec, + input: CopyWithPathInput, +): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. + * + * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across + * different accessLevel or identityId (if source object's accessLevel is 'protected'). + * + * @param contextSpec - The isolated server context. + * @param input - The `CopyInput` object. + * @returns Output containing the destination object key. + * @throws service: `S3Exception` - Thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Thrown when + * source or destination key is not defined. + */ +export function copy( contextSpec: AmplifyServer.ContextSpec, input: CopyInput, -): Promise => - copyInternal( - getAmplifyServerContext(contextSpec).amplify, - input, - ) as Promise; +): Promise; + +export function copy( + contextSpec: AmplifyServer.ContextSpec, + input: CopyInput | CopyWithPathInput, +) { + return copyInternal(getAmplifyServerContext(contextSpec).amplify, input); +} diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts index bca2c7aae98..87a77a297a4 100644 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/server/getProperties.ts @@ -8,57 +8,50 @@ import { import { GetPropertiesInput, - GetPropertiesInputWithKey, - GetPropertiesInputWithPath, GetPropertiesOutput, + GetPropertiesWithPathInput, + GetPropertiesWithPathOutput, } from '../../types'; import { getProperties as getPropertiesInternal } from '../internal/getProperties'; -interface GetProperties { - /** - * Gets the properties of a file. The properties include S3 system metadata and - * the user metadata that was provided when uploading the file. - * - * @param contextSpec - The isolated server context. - * @param input - The `GetPropertiesInputWithPath` object. - * @returns Requested object properties. - * @throws An `S3Exception` when the underlying S3 service returned error. - * @throws A `StorageValidationErrorCode` when API call parameters are invalid. - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: GetPropertiesInputWithPath, - ): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. - * - * Gets the properties of a file. The properties include S3 system metadata and - * the user metadata that was provided when uploading the file. - * - * @param contextSpec - The isolated server context. - * @param input - The `GetPropertiesInputWithKey` object. - * @returns Requested object properties. - * @throws An `S3Exception` when the underlying S3 service returned error. - * @throws A `StorageValidationErrorCode` when API call parameters are invalid. - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: GetPropertiesInputWithKey, - ): Promise; - ( - contextSpec: AmplifyServer.ContextSpec, - input: GetPropertiesInput, - ): Promise; -} - -export const getProperties: GetProperties = < - Output extends GetPropertiesOutput, ->( +/** + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetPropertiesWithPathInput` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ +export function getProperties( + contextSpec: AmplifyServer.ContextSpec, + input: GetPropertiesWithPathInput, +): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. + * + * Gets the properties of a file. The properties include S3 system metadata and + * the user metadata that was provided when uploading the file. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetPropertiesInput` object. + * @returns Requested object properties. + * @throws An `S3Exception` when the underlying S3 service returned error. + * @throws A `StorageValidationErrorCode` when API call parameters are invalid. + */ +export function getProperties( contextSpec: AmplifyServer.ContextSpec, input: GetPropertiesInput, -): Promise => - getPropertiesInternal( +): Promise; + +export function getProperties( + contextSpec: AmplifyServer.ContextSpec, + input: GetPropertiesInput | GetPropertiesWithPathInput, +) { + return getPropertiesInternal( getAmplifyServerContext(contextSpec).amplify, input, - ) as Promise; + ); +} diff --git a/packages/storage/src/providers/s3/apis/server/getUrl.ts b/packages/storage/src/providers/s3/apis/server/getUrl.ts index d3a75a1299e..f9f4e80d07c 100644 --- a/packages/storage/src/providers/s3/apis/server/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/server/getUrl.ts @@ -8,65 +8,61 @@ import { import { GetUrlInput, - GetUrlInputWithKey, - GetUrlInputWithPath, GetUrlOutput, + GetUrlWithPathInput, + GetUrlWithPathOutput, } from '../../types'; import { getUrl as getUrlInternal } from '../internal/getUrl'; -interface GetUrl { - /** - * Get a temporary presigned URL to download the specified S3 object. - * The presigned URL expires when the associated role used to sign the request expires or - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * to true, this method will verify the given object already exists in S3 before returning a presigned - * URL, and will throw `StorageError` if the object does not exist. - * - * @param contextSpec - The isolated server context. - * @param input - The `GetUrlInputWithPath` object. - * @returns Presigned URL and timestamp when the URL MAY expire. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: GetUrlInputWithPath, - ): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. - * - * Get a temporary presigned URL to download the specified S3 object. - * The presigned URL expires when the associated role used to sign the request expires or - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * to true, this method will verify the given object already exists in S3 before returning a presigned - * URL, and will throw `StorageError` if the object does not exist. - * - * @param contextSpec - The isolated server context. - * @param input - The `GetUrlInputWithKey` object. - * @returns Presigned URL and timestamp when the URL MAY expire. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: GetUrlInputWithKey, - ): Promise; - ( - contextSpec: AmplifyServer.ContextSpec, - input: GetUrlInput, - ): Promise; -} -export const getUrl: GetUrl = async ( +/** + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetUrlWithPathInput` object. + * @returns Presigned URL and timestamp when the URL may expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ +export function getUrl( + contextSpec: AmplifyServer.ContextSpec, + input: GetUrlWithPathInput, +): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. + * + * Get a temporary presigned URL to download the specified S3 object. + * The presigned URL expires when the associated role used to sign the request expires or + * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. + * + * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` + * to true, this method will verify the given object already exists in S3 before returning a presigned + * URL, and will throw `StorageError` if the object does not exist. + * + * @param contextSpec - The isolated server context. + * @param input - The `GetUrlInput` object. + * @returns Presigned URL and timestamp when the URL may expire. + * @throws service: `S3Exception` - thrown when checking for existence of the object + * @throws validation: `StorageValidationErrorCode` - Validation errors + * thrown either username or key are not defined. + * + */ +export function getUrl( contextSpec: AmplifyServer.ContextSpec, input: GetUrlInput, -): Promise => - getUrlInternal(getAmplifyServerContext(contextSpec).amplify, input); +): Promise; + +export function getUrl( + contextSpec: AmplifyServer.ContextSpec, + input: GetUrlInput | GetUrlWithPathInput, +) { + return getUrlInternal(getAmplifyServerContext(contextSpec).amplify, input); +} diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts index fe1a4e5f885..66d0ad4cd22 100644 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ b/packages/storage/src/providers/s3/apis/server/list.ts @@ -7,86 +7,79 @@ import { import { ListAllInput, - ListAllInputWithPath, - ListAllInputWithPrefix, ListAllOutput, + ListAllWithPathInput, + ListAllWithPathOutput, ListPaginateInput, - ListPaginateInputWithPath, - ListPaginateInputWithPrefix, ListPaginateOutput, + ListPaginateWithPathInput, + ListPaginateWithPathOutput, } from '../../types'; import { list as listInternal } from '../internal/list'; -interface ListApi { - /** - * List files in pages with the given `path`. - * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputWithPath` object. - * @param contextSpec - The context spec used to get the Amplify server context. - * @returns A list of objects with path and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: ListPaginateInputWithPath, - ): Promise; - /** - * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputWithPath` object. - * @param contextSpec - The context spec used to get the Amplify server context. - * @returns A list of all objects with path and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: ListAllInputWithPath, - ): Promise; - /** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List files in pages with the given `prefix`. - * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInputWithPrefix` object. - * @returns A list of objects with key and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input?: ListPaginateInputWithPrefix, - ): Promise; - /** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInputWithPrefix` object. - * @returns A list of all objects with key and metadata - * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket - * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input?: ListAllInputWithPrefix, - ): Promise; - ( - contextSpec: AmplifyServer.ContextSpec, - input?: ListPaginateInput, - ): Promise; - ( - contextSpec: AmplifyServer.ContextSpec, - input?: ListAllInput, - ): Promise; -} +/** + * List files in pages with the given `path`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateWithPathInput` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @returns A list of objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list( + contextSpec: AmplifyServer.ContextSpec, + input: ListPaginateWithPathInput, +): Promise; +/** + * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllWithPathInput` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @returns A list of all objects with path and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list( + contextSpec: AmplifyServer.ContextSpec, + input: ListAllWithPathInput, +): Promise; +/** + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List files in pages with the given `prefix`. + * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. + * @param input - The `ListPaginateInput` object. + * @returns A list of objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list( + contextSpec: AmplifyServer.ContextSpec, + input?: ListPaginateInput, +): Promise; +/** + * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. + * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. + * @param input - The `ListAllInput` object. + * @returns A list of all objects with key and metadata + * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket + * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials + */ +export function list( + contextSpec: AmplifyServer.ContextSpec, + input?: ListAllInput, +): Promise; -export const list: ListApi = < - Output extends ListAllOutput | ListPaginateOutput, ->( +export function list( contextSpec: AmplifyServer.ContextSpec, - input?: ListAllInput | ListPaginateInput, -): Promise => - listInternal( + input?: + | ListAllInput + | ListPaginateInput + | ListAllWithPathInput + | ListPaginateWithPathInput, +) { + return listInternal( getAmplifyServerContext(contextSpec).amplify, input ?? {}, - ) as Promise; + ); +} diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts index 0815f1c1c44..5b788447f64 100644 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ b/packages/storage/src/providers/s3/apis/server/remove.ts @@ -8,53 +8,45 @@ import { import { RemoveInput, - RemoveInputWithKey, - RemoveInputWithPath, RemoveOutput, + RemoveWithPathInput, + RemoveWithPathOutput, } from '../../types'; import { remove as removeInternal } from '../internal/remove'; -interface RemoveApi { - /** - * Remove a file from your S3 bucket. - * @param input - The `RemoveInputWithPath` object. - * @param contextSpec - The context spec used to get the Amplify server context. - * @return Output containing the removed object path. - * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. - * @throws validation: `StorageValidationErrorCode` - Validation errors thrown - * when there is no path or path is empty or path has a leading slash. - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: RemoveInputWithPath, - ): Promise; - /** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. - * - * Remove a file from your S3 bucket. - * @param input - The `RemoveInputWithKey` object. - * @param contextSpec - The context spec used to get the Amplify server context. - * @return Output containing the removed object key - * @throws service: `S3Exception` - S3 service errors thrown while while removing the object - * @throws validation: `StorageValidationErrorCode` - Validation errors thrown - * when there is no key or its empty. - */ - ( - contextSpec: AmplifyServer.ContextSpec, - input: RemoveInputWithKey, - ): Promise; - ( - contextSpec: AmplifyServer.ContextSpec, - input: RemoveInput, - ): Promise; -} - -export const remove: RemoveApi = ( +/** + * Remove a file from your S3 bucket. + * @param input - The `RemoveWithPathInput` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @return Output containing the removed object path. + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no path or path is empty or path has a leading slash. + */ +export function remove( + contextSpec: AmplifyServer.ContextSpec, + input: RemoveWithPathInput, +): Promise; +/** + * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. + * + * Remove a file from your S3 bucket. + * @param input - The `RemoveInput` object. + * @param contextSpec - The context spec used to get the Amplify server context. + * @return Output containing the removed object key + * @throws service: `S3Exception` - S3 service errors thrown while while removing the object + * @throws validation: `StorageValidationErrorCode` - Validation errors thrown + * when there is no key or its empty. + */ +export function remove( contextSpec: AmplifyServer.ContextSpec, input: RemoveInput, -): Promise => - removeInternal( - getAmplifyServerContext(contextSpec).amplify, - input, - ) as Promise; +): Promise; + +export function remove( + contextSpec: AmplifyServer.ContextSpec, + input: RemoveInput | RemoveWithPathInput, +) { + return removeInternal(getAmplifyServerContext(contextSpec).amplify, input); +} diff --git a/packages/storage/src/providers/s3/apis/uploadData/index.ts b/packages/storage/src/providers/s3/apis/uploadData/index.ts index 0bf632996a3..8669309ec53 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/index.ts @@ -3,9 +3,9 @@ import { UploadDataInput, - UploadDataInputWithKey, - UploadDataInputWithPath, UploadDataOutput, + UploadDataWithPathInput, + UploadDataWithPathOutput, } from '../../types'; import { createUploadTask } from '../../utils'; import { assertValidationError } from '../../../../errors/utils/assertValidationError'; @@ -16,120 +16,117 @@ import { byteLength } from './byteLength'; import { putObjectJob } from './putObjectJob'; import { getMultipartUploadHandlers } from './multipart'; -interface UploadData { - /** - * Upload data to the specified S3 object path. By default uses single PUT operation to upload if the payload is less than 5MB. - * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. - * - * Limitations: - * * Maximum object size is 5TB. - * * Maximum object size if the size cannot be determined before upload is 50GB. - * - * @throws Service: `S3Exception` thrown when checking for existence of the object. - * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. - * - * @param input - A `UploadDataInputWithPath` object. - * - * @returns A cancelable and resumable task exposing result promise from `result` - * property. - * - * @example - * ```ts - * // Upload a file to s3 bucket - * await uploadData({ path, data: file, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * - * @example - * ```ts - * // Cancel a task - * const uploadTask = uploadData({ path, data: file }); - * //... - * uploadTask.cancel(); - * try { - * await uploadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - * - * @example - * ```ts - * // Pause and resume a task - * const uploadTask = uploadData({ path, data: file }); - * //... - * uploadTask.pause(); - * //... - * uploadTask.resume(); - * //... - * await uploadTask.result; - * ``` - */ - (input: UploadDataInputWithPath): UploadDataOutput; +/** + * Upload data to the specified S3 object path. By default uses single PUT operation to upload if the payload is less than 5MB. + * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. + * + * Limitations: + * * Maximum object size is 5TB. + * * Maximum object size if the size cannot be determined before upload is 50GB. + * + * @throws Service: `S3Exception` thrown when checking for existence of the object. + * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. + * + * @param input - A `UploadDataWithPathInput` object. + * + * @returns A cancelable and resumable task exposing result promise from `result` + * property. + * + * @example + * ```ts + * // Upload a file to s3 bucket + * await uploadData({ path, data: file, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * + * @example + * ```ts + * // Cancel a task + * const uploadTask = uploadData({ path, data: file }); + * //... + * uploadTask.cancel(); + * try { + * await uploadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + * + * @example + * ```ts + * // Pause and resume a task + * const uploadTask = uploadData({ path, data: file }); + * //... + * uploadTask.pause(); + * //... + * uploadTask.resume(); + * //... + * await uploadTask.result; + * ``` + */ +export function uploadData( + input: UploadDataWithPathInput, +): UploadDataWithPathOutput; - /** - * Upload data to the specified S3 object key. By default uses single PUT operation to upload if the payload is less than 5MB. - * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. - * - * Limitations: - * * Maximum object size is 5TB. - * * Maximum object size if the size cannot be determined before upload is 50GB. - * - * @deprecated The `key` and `accessLevel` parameters are deprecated and will be removed in next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/upload/#uploaddata | path} instead. - * - * @throws Service: `S3Exception` thrown when checking for existence of the object. - * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. - * - * @param input - A UploadDataInputWithKey object. - * - * @returns A cancelable and resumable task exposing result promise from the `result` property. - * - * @example - * ```ts - * // Upload a file to s3 bucket - * await uploadData({ key, data: file, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * - * @example - * ```ts - * // Cancel a task - * const uploadTask = uploadData({ key, data: file }); - * //... - * uploadTask.cancel(); - * try { - * await uploadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - * - * @example - * ```ts - * // Pause and resume a task - * const uploadTask = uploadData({ key, data: file }); - * //... - * uploadTask.pause(); - * //... - * uploadTask.resume(); - * //... - * await uploadTask.result; - * ``` - */ - (input: UploadDataInputWithKey): UploadDataOutput; - (input: UploadDataInput): UploadDataOutput; -} +/** + * Upload data to the specified S3 object key. By default uses single PUT operation to upload if the payload is less than 5MB. + * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. + * + * Limitations: + * * Maximum object size is 5TB. + * * Maximum object size if the size cannot be determined before upload is 50GB. + * + * @deprecated The `key` and `accessLevel` parameters are deprecated and will be removed in next major version. + * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/upload/#uploaddata | path} instead. + * + * @throws Service: `S3Exception` thrown when checking for existence of the object. + * @throws Validation: `StorageValidationErrorCode` thrown when a validation error occurs. + * + * @param input - A `UploadDataInput` object. + * + * @returns A cancelable and resumable task exposing result promise from the `result` property. + * + * @example + * ```ts + * // Upload a file to s3 bucket + * await uploadData({ key, data: file, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * + * @example + * ```ts + * // Cancel a task + * const uploadTask = uploadData({ key, data: file }); + * //... + * uploadTask.cancel(); + * try { + * await uploadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + * + * @example + * ```ts + * // Pause and resume a task + * const uploadTask = uploadData({ key, data: file }); + * //... + * uploadTask.pause(); + * //... + * uploadTask.resume(); + * //... + * await uploadTask.result; + * ``` + */ +export function uploadData(input: UploadDataInput): UploadDataOutput; -export const uploadData: UploadData = ( - input: UploadDataInput, -): Output => { +export function uploadData(input: UploadDataInput | UploadDataWithPathInput) { const { data } = input; const dataByteLength = byteLength(data); @@ -148,7 +145,7 @@ export const uploadData: UploadData = ( onCancel: (message?: string) => { abortController.abort(message); }, - }) as Output; + }); } else { // Multipart upload const { multipartUploadJob, onPause, onResume, onCancel } = @@ -162,6 +159,6 @@ export const uploadData: UploadData = ( }, onPause, onResume, - }) as Output; + }); } -}; +} 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 e0100107492..e216feeede7 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts @@ -4,12 +4,12 @@ import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { UploadDataInput } from '../../../types'; +import { UploadDataInput, UploadDataWithPathInput } from '../../../types'; import { resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../../utils'; -import { ItemWithKeyAndPath } from '../../../types/outputs'; +import { ItemWithKey, ItemWithPath } from '../../../types/outputs'; import { DEFAULT_ACCESS_LEVEL, DEFAULT_QUEUE_SIZE, @@ -43,10 +43,12 @@ import { getDataChunker } from './getDataChunker'; * @internal */ export const getMultipartUploadHandlers = ( - uploadDataInput: UploadDataInput, + uploadDataInput: UploadDataInput | UploadDataWithPathInput, size?: number, ) => { - let resolveCallback: ((value: ItemWithKeyAndPath) => void) | undefined; + let resolveCallback: + | ((value: ItemWithKey | ItemWithPath) => void) + | undefined; let rejectCallback: ((reason?: any) => void) | undefined; let inProgressUpload: | { @@ -67,7 +69,7 @@ export const getMultipartUploadHandlers = ( // This should be replaced by a special abort reason. However,the support of this API is lagged behind. let isAbortSignalFromPause = false; - const startUpload = async (): Promise => { + const startUpload = async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const resolvedS3Options = await resolveS3ConfigAndInput( Amplify, @@ -218,8 +220,8 @@ export const getMultipartUploadHandlers = ( }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, path: finalKey, ...result } - : { path: finalKey, key: finalKey, ...result }; + ? { key: objectKey, ...result } + : { path: objectKey, ...result }; }; const startUploadWithResumability = () => @@ -236,7 +238,7 @@ export const getMultipartUploadHandlers = ( }); const multipartUploadJob = () => - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { resolveCallback = resolve; rejectCallback = reject; startUploadWithResumability(); diff --git a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts index 81b2fe6fa94..bb9b5ec4519 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts @@ -4,13 +4,13 @@ import { Amplify } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { UploadDataInput } from '../../types'; +import { UploadDataInput, UploadDataWithPathInput } from '../../types'; import { calculateContentMd5, resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../utils'; -import { ItemWithKeyAndPath } from '../../types/outputs'; +import { ItemWithKey, ItemWithPath } from '../../types/outputs'; import { putObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { STORAGE_INPUT_KEY } from '../../utils/constants'; @@ -22,11 +22,11 @@ import { STORAGE_INPUT_KEY } from '../../utils/constants'; */ export const putObjectJob = ( - uploadDataInput: UploadDataInput, + uploadDataInput: UploadDataInput | UploadDataWithPathInput, abortSignal: AbortSignal, totalLength?: number, ) => - async (): Promise => { + async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const { bucket, keyPrefix, s3Config, isObjectLockEnabled, identityId } = await resolveS3ConfigAndInput(Amplify, uploadDataOptions); @@ -75,6 +75,6 @@ export const putObjectJob = }; return inputType === STORAGE_INPUT_KEY - ? { key: objectKey, path: finalKey, ...result } - : { path: finalKey, key: finalKey, ...result }; + ? { key: objectKey, ...result } + : { path: objectKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/index.ts b/packages/storage/src/providers/s3/index.ts index dd2f2eb015e..2ec8bb61527 100644 --- a/packages/storage/src/providers/s3/index.ts +++ b/packages/storage/src/providers/s3/index.ts @@ -13,22 +13,38 @@ export { export { UploadDataInput, + UploadDataWithPathInput, DownloadDataInput, + DownloadDataWithPathInput, RemoveInput, + RemoveWithPathInput, ListAllInput, + ListAllWithPathInput, ListPaginateInput, + ListPaginateWithPathInput, GetPropertiesInput, + GetPropertiesWithPathInput, CopyInput, + CopyWithPathInput, GetUrlInput, + GetUrlWithPathInput, } from './types/inputs'; export { UploadDataOutput, + UploadDataWithPathOutput, DownloadDataOutput, + DownloadDataWithPathOutput, RemoveOutput, + RemoveWithPathOutput, ListAllOutput, + ListAllWithPathOutput, ListPaginateOutput, + ListPaginateWithPathOutput, GetPropertiesOutput, + GetPropertiesWithPathOutput, CopyOutput, + CopyWithPathOutput, GetUrlOutput, + GetUrlWithPathOutput, } from './types/outputs'; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 17c0a7dd28d..4299687cd8e 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -19,40 +19,41 @@ export { CopySourceOptionsWithKey, } from './options'; export { - DownloadDataOutput, - GetUrlOutput, UploadDataOutput, + UploadDataWithPathOutput, + DownloadDataOutput, + DownloadDataWithPathOutput, + RemoveOutput, + RemoveWithPathOutput, ListAllOutput, + ListAllWithPathOutput, ListPaginateOutput, + ListPaginateWithPathOutput, GetPropertiesOutput, + GetPropertiesWithPathOutput, CopyOutput, - RemoveOutput, - ItemWithKeyAndPath, + CopyWithPathOutput, + GetUrlOutput, + GetUrlWithPathOutput, + ListOutputItem, + ListOutputItemWithPath, } from './outputs'; export { CopyInput, - CopyInputWithKey, - CopyInputWithPath, + CopyWithPathInput, GetPropertiesInput, - GetPropertiesInputWithKey, - GetPropertiesInputWithPath, + GetPropertiesWithPathInput, GetUrlInput, - GetUrlInputWithKey, - GetUrlInputWithPath, - RemoveInputWithKey, - RemoveInputWithPath, + GetUrlWithPathInput, + RemoveWithPathInput, RemoveInput, DownloadDataInput, - DownloadDataInputWithKey, - DownloadDataInputWithPath, + DownloadDataWithPathInput, UploadDataInput, - UploadDataInputWithPath, - UploadDataInputWithKey, + UploadDataWithPathInput, ListAllInput, ListPaginateInput, - ListAllInputWithPath, - ListPaginateInputWithPath, - ListAllInputWithPrefix, - ListPaginateInputWithPrefix, + ListAllWithPathInput, + ListPaginateWithPathInput, } from './inputs'; export { S3Exception } from './errors'; diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index fa16e636b49..f7bd6c5db44 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { StrictUnion } from '@aws-amplify/core/internals/utils'; - import { StorageCopyInputWithKey, StorageCopyInputWithPath, @@ -39,120 +37,101 @@ import { // TODO: support use accelerate endpoint option /** + * @deprecated Use {@link CopyWithPathInput} instead. * Input type for S3 copy API. */ -export type CopyInput = CopyInputWithKey | CopyInputWithPath; - -/** @deprecated Use {@link CopyInputWithPath} instead. */ -export type CopyInputWithKey = StorageCopyInputWithKey< +export type CopyInput = StorageCopyInputWithKey< CopySourceOptionsWithKey, CopyDestinationOptionsWithKey >; -export type CopyInputWithPath = StorageCopyInputWithPath; +/** + * Input type with path for S3 copy API. + */ +export type CopyWithPathInput = StorageCopyInputWithPath; /** + * @deprecated Use {@link GetPropertiesWithPathInput} instead. * Input type for S3 getProperties API. */ -export type GetPropertiesInput = StrictUnion< - GetPropertiesInputWithKey | GetPropertiesInputWithPath ->; - -/** @deprecated Use {@link GetPropertiesInputWithPath} instead. */ -export type GetPropertiesInputWithKey = +export type GetPropertiesInput = StorageGetPropertiesInputWithKey; -export type GetPropertiesInputWithPath = +/** + * Input type with for S3 getProperties API. + */ +export type GetPropertiesWithPathInput = StorageGetPropertiesInputWithPath; /** + * @deprecated Use {@link GetUrlWithPathInput} instead. * Input type for S3 getUrl API. */ -export type GetUrlInput = StrictUnion; - -/** @deprecated Use {@link GetUrlInputWithPath} instead. */ -export type GetUrlInputWithKey = - StorageGetUrlInputWithKey; -export type GetUrlInputWithPath = - StorageGetUrlInputWithPath; - +export type GetUrlInput = StorageGetUrlInputWithKey; /** - * Input type for S3 list API. Lists all bucket objects. + * Input type with path for S3 getUrl API. */ -export type ListAllInput = StrictUnion< - ListAllInputWithPath | ListAllInputWithPrefix ->; - -/** - * Input type for S3 list API. Lists bucket objects with pagination. - */ -export type ListPaginateInput = StrictUnion< - ListPaginateInputWithPath | ListPaginateInputWithPrefix ->; +export type GetUrlWithPathInput = + StorageGetUrlInputWithPath; /** - * Input type for S3 list API. Lists all bucket objects. + * Input type with path for S3 list API. Lists all bucket objects. */ -export type ListAllInputWithPath = +export type ListAllWithPathInput = StorageListInputWithPath; /** - * Input type for S3 list API. Lists bucket objects with pagination. + * Input type with path for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateInputWithPath = +export type ListPaginateWithPathInput = StorageListInputWithPath; /** - * @deprecated Use {@link ListAllInputWithPath} instead. + * @deprecated Use {@link ListAllWithPathInput} instead. * Input type for S3 list API. Lists all bucket objects. */ -export type ListAllInputWithPrefix = - StorageListInputWithPrefix; +export type ListAllInput = StorageListInputWithPrefix; /** - * @deprecated Use {@link ListPaginateInputWithPath} instead. + * @deprecated Use {@link ListPaginateWithPathInput} instead. * Input type for S3 list API. Lists bucket objects with pagination. */ -export type ListPaginateInputWithPrefix = +export type ListPaginateInput = StorageListInputWithPrefix; /** - * @deprecated Use {@link RemoveInputWithPath} instead. + * @deprecated Use {@link RemoveWithPathInput} instead. * Input type with key for S3 remove API. */ -export type RemoveInputWithKey = StorageRemoveInputWithKey; +export type RemoveInput = StorageRemoveInputWithKey; /** * Input type with path for S3 remove API. */ -export type RemoveInputWithPath = StorageRemoveInputWithPath< +export type RemoveWithPathInput = StorageRemoveInputWithPath< Omit >; /** - * Input type for S3 remove API. + * @deprecated Use {@link DownloadDataWithPathInput} instead. + * Input type for S3 downloadData API. */ -export type RemoveInput = StrictUnion; +export type DownloadDataInput = + StorageDownloadDataInputWithKey; /** - * Input type for S3 downloadData API. + * Input type with path for S3 downloadData API. */ -export type DownloadDataInput = StrictUnion< - DownloadDataInputWithKey | DownloadDataInputWithPath ->; -/** @deprecated Use {@link DownloadDataInputWithPath} instead. */ -export type DownloadDataInputWithKey = - StorageDownloadDataInputWithKey; -export type DownloadDataInputWithPath = +export type DownloadDataWithPathInput = StorageDownloadDataInputWithPath; /** + * @deprecated Use {@link UploadDataWithPathInput} instead. * Input type for S3 uploadData API. */ -export type UploadDataInput = StrictUnion< - UploadDataInputWithKey | UploadDataInputWithPath ->; - -/** @deprecated Use {@link UploadDataInputWithPath} instead. */ -export type UploadDataInputWithKey = +export type UploadDataInput = StorageUploadDataInputWithKey; -export type UploadDataInputWithPath = + +/** + * Input type with path for S3 uploadData API. + */ +export type UploadDataWithPathInput = StorageUploadDataInputWithPath; diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index 5d4b7f484a7..44524536a3b 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -5,7 +5,8 @@ import { DownloadTask, StorageDownloadDataOutput, StorageGetUrlOutput, - StorageItem, + StorageItemWithKey, + StorageItemWithPath, StorageListOutput, UploadTask, } from '../../../types'; @@ -25,52 +26,114 @@ export interface ItemBase { } /** - * type for S3 list item. + * @deprecated Use {@link ListOutputItemWithPath} instead. + * type for S3 list item with key. */ -export type ListOutputItem = Omit; +export type ListOutputItem = Omit; -export type ItemWithKeyAndPath = ItemBase & StorageItem; +/** + * type for S3 list item with path. + */ +export type ListOutputItemWithPath = Omit; + +/** + * @deprecated Use {@link ItemWithPath} instead. + */ +export type ItemWithKey = ItemBase & StorageItemWithKey; + +/** + * type for S3 list item with path. + */ +export type ItemWithPath = ItemBase & StorageItemWithPath; /** * Output type for S3 downloadData API. + * @deprecated Use {@link DownloadDataWithPathOutput} instead. */ export type DownloadDataOutput = DownloadTask< - StorageDownloadDataOutput + StorageDownloadDataOutput >; - /** - * Output type for S3 uploadData API. + * Output type with path for S3 downloadData API. */ -export type UploadDataOutput = UploadTask; +export type DownloadDataWithPathOutput = DownloadTask< + StorageDownloadDataOutput +>; /** * Output type for S3 getUrl API. + * @deprecated Use {@link GetUrlWithPathOutput} instead. */ export type GetUrlOutput = StorageGetUrlOutput; - /** - * Output type for S3 getProperties API. - */ -export type GetPropertiesOutput = ItemWithKeyAndPath; + * Output type with path for S3 getUrl API. + * */ +export type GetUrlWithPathOutput = StorageGetUrlOutput; /** - * Output type for S3 Copy API. + * Output type for S3 uploadData API. + * @deprecated Use {@link UploadDataWithPathOutput} instead. */ -export type CopyOutput = Pick; +export type UploadDataOutput = UploadTask; +/** + * Output type with path for S3 uploadData API. + * */ +export type UploadDataWithPathOutput = UploadTask; /** - * Output type for S3 remove API. - */ -export type RemoveOutput = Pick; + * Output type for S3 getProperties API. + * @deprecated Use {@link GetPropertiesWithPathOutput} instead. + * */ +export type GetPropertiesOutput = ItemBase & StorageItemWithKey; +/** + * Output type with path for S3 getProperties API. + * */ +export type GetPropertiesWithPathOutput = ItemBase & StorageItemWithPath; /** + * @deprecated Use {@link ListAllWithPathOutput} instead. * Output type for S3 list API. Lists all bucket objects. */ export type ListAllOutput = StorageListOutput; /** + * Output type with path for S3 list API. Lists all bucket objects. + */ +export type ListAllWithPathOutput = StorageListOutput; + +/** + * @deprecated Use {@link ListPaginateWithPathOutput} instead. * Output type for S3 list API. Lists bucket objects with pagination. */ export type ListPaginateOutput = StorageListOutput & { nextToken?: string; }; + +/** + * Output type with path for S3 list API. Lists bucket objects with pagination. + */ +export type ListPaginateWithPathOutput = + StorageListOutput & { + nextToken?: string; + }; + +/** + * Output type with path for S3 copy API. + * @deprecated Use {@link CopyWithPathOutput} instead. + */ +export type CopyOutput = Pick; +/** + * Output type with path for S3 copy API. + */ +export type CopyWithPathOutput = Pick; + +/** + * @deprecated Use {@link RemoveWithPathOutput} instead. + * Output type with key for S3 remove API. + */ +export type RemoveOutput = Pick; + +/** + * Output type with path for S3 remove API. + */ +export type RemoveWithPathOutput = Pick; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 311922811a2..317fa20104c 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -32,6 +32,8 @@ export { } from './options'; export { StorageItem, + StorageItemWithKey, + StorageItemWithPath, StorageListOutput, StorageDownloadDataOutput, StorageGetUrlOutput, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index f371cbbb234..403a2a14332 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -86,23 +86,13 @@ export interface StorageCopyInputWithKey< SourceOptions extends StorageOptions, DestinationOptions extends StorageOptions, > { - source: SourceOptions & { - path?: never; - }; - destination: DestinationOptions & { - path?: never; - }; + source: SourceOptions; + destination: DestinationOptions; } export interface StorageCopyInputWithPath { - source: StorageOperationInputWithPath & { - /** @deprecated Use path instead. */ - key?: never; - }; - destination: StorageOperationInputWithPath & { - /** @deprecated Use path instead. */ - key?: never; - }; + source: StorageOperationInputWithPath; + destination: StorageOperationInputWithPath; } /** diff --git a/packages/storage/src/types/outputs.ts b/packages/storage/src/types/outputs.ts index aa947602ca5..e38482729b8 100644 --- a/packages/storage/src/types/outputs.ts +++ b/packages/storage/src/types/outputs.ts @@ -27,15 +27,27 @@ export interface StorageItemBase { metadata?: Record; } -/** - * A storage item can be identified either by a key or a path. - */ -export type StorageItem = StorageItemBase & { - /** @deprecated This may be removed in next major version */ +/** @deprecated Use {@link StorageItemWithPath} instead. */ +export type StorageItemWithKey = StorageItemBase & { + /** + * @deprecated This may be removed in next major version. + * Key of the object. + */ key: string; +}; + +export type StorageItemWithPath = StorageItemBase & { + /** + * Path of the object. + */ path: string; }; +/** + * A storage item can be identified either by a key or a path. + */ +export type StorageItem = StorageItemWithKey | StorageItemWithPath; + export type StorageDownloadDataOutput = Item & { body: ResponseBodyMixin; }; From 3cca6822ad4b76ab8966654923363635d92587f0 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 24 Apr 2024 14:23:44 -0700 Subject: [PATCH 26/33] fix(geo): update amplify config geo schema (#13290) Co-authored-by: Ashwin Kumar --- packages/core/__tests__/parseAmplifyOutputs.test.ts | 3 +-- packages/core/src/singleton/AmplifyOutputs/types.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/__tests__/parseAmplifyOutputs.test.ts b/packages/core/__tests__/parseAmplifyOutputs.test.ts index 3efca756732..53496954be5 100644 --- a/packages/core/__tests__/parseAmplifyOutputs.test.ts +++ b/packages/core/__tests__/parseAmplifyOutputs.test.ts @@ -222,7 +222,7 @@ describe('parseAmplifyOutputs tests', () => { aws_region: 'us-east-1', maps: { items: { - map1: { name: 'map1', style: 'color' }, + map1: { style: 'color' }, }, default: 'map1', }, @@ -249,7 +249,6 @@ describe('parseAmplifyOutputs tests', () => { items: { map1: { style: 'color', - name: 'map1', }, }, }, diff --git a/packages/core/src/singleton/AmplifyOutputs/types.ts b/packages/core/src/singleton/AmplifyOutputs/types.ts index a09bf792267..9f03f49a7fb 100644 --- a/packages/core/src/singleton/AmplifyOutputs/types.ts +++ b/packages/core/src/singleton/AmplifyOutputs/types.ts @@ -51,7 +51,7 @@ export interface AmplifyOutputsStorageProperties { export interface AmplifyOutputsGeoProperties { aws_region: string; maps?: { - items: Record; + items: Record; default: string; }; search_indices?: { items: string[]; default: string }; From f257c7550ddd3802a35f19abebace85d7894f68d Mon Sep 17 00:00:00 2001 From: ashika112 Date: Thu, 25 Apr 2024 17:05:41 -0700 Subject: [PATCH 27/33] bundle size fix --- packages/aws-amplify/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index c6294b2c49c..bf1660a5271 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -335,7 +335,7 @@ "name": "[API] generateClient (AppSync)", "path": "./dist/esm/api/index.mjs", "import": "{ generateClient }", - "limit": "39.80 kB" + "limit": "39.99 kB" }, { "name": "[API] REST API handlers", @@ -437,7 +437,7 @@ "name": "[Auth] signInWithRedirect (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect }", - "limit": "20.98 kB" + "limit": "21.10 kB" }, { "name": "[Auth] fetchUserAttributes (Cognito)", @@ -455,7 +455,7 @@ "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "21.42 kB" + "limit": "21.45 kB" }, { "name": "[Storage] copy (S3)", From 00bec214cb27435f904a850f0bace5230baa1b13 Mon Sep 17 00:00:00 2001 From: ashika112 Date: Fri, 26 Apr 2024 09:07:43 -0700 Subject: [PATCH 28/33] Revert "chore: Enable pre-id publication for gen2-storage (#13159)" This reverts commit 77b2b20d021de3804feb8041414e9c7b68ef0bbf. --- .github/workflows/push-preid-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push-preid-release.yml b/.github/workflows/push-preid-release.yml index 5a7d27ef8b8..9837290ed14 100644 --- a/.github/workflows/push-preid-release.yml +++ b/.github/workflows/push-preid-release.yml @@ -9,7 +9,7 @@ on: push: branches: # Change this to your branch name where "example-preid" corresponds to the preid you want your changes released on - - gen2-storage + - feat/example-preid-branch/main jobs: e2e: @@ -35,4 +35,4 @@ jobs: # The preid should be detected from the branch name recommending feat/{PREID}/whatever as branch naming pattern # if your branch doesn't follow this pattern, you can override it here for your branch. with: - preid: gen2-storage + preid: ${{ needs.parse-preid.outputs.preid }} From 64acd4909fb6d16354aa3d8f12a70e7c57f530d4 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Fri, 26 Apr 2024 12:04:27 -0500 Subject: [PATCH 29/33] chore: bump to data-schema version ^0.18.0 (#13297) * chore: bump to data-schema version ^0.18.0 * update yarn lock and tests associated with changed data-schema behavior * fix modelintro and fixtures --- .../fixtures/modeled/amplifyconfiguration.ts | 4 ++-- .../__snapshots__/generateClient.test.ts.snap | 8 +++---- .../internals/generateClient.test.ts | 21 +++++++++++-------- packages/api-graphql/package.json | 2 +- yarn.lock | 8 +++---- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts index ab3ce689644..0a8fe6e2c44 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts @@ -1041,7 +1041,7 @@ const amplifyConfig = { type: 'key', properties: { name: 'secondaryIndexModelsByTitle', - queryField: 'listByTitle', + queryField: 'listSecondaryIndexModelByTitle', fields: ['title'], }, }, @@ -1049,7 +1049,7 @@ const amplifyConfig = { type: 'key', properties: { name: 'secondaryIndexModelsByDescriptionAndViewCount', - queryField: 'listByDescriptionAndViewCount', + queryField: 'listSecondaryIndexModelByDescriptionAndViewCount', fields: ['description', 'viewCount'], }, }, diff --git a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap index 7550d94648c..ac5ab4860d6 100644 --- a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap +++ b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap @@ -5292,7 +5292,7 @@ exports[`generateClient index queries PK and SK index query 1`] = ` "options": { "body": { "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { - listByDescriptionAndViewCount( + listSecondaryIndexModelByDescriptionAndViewCount( description: $description viewCount: $viewCount filter: $filter @@ -5344,7 +5344,7 @@ exports[`generateClient index queries PK and SK index query, with sort direction "options": { "body": { "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { - listByDescriptionAndViewCount( + listSecondaryIndexModelByDescriptionAndViewCount( description: $description viewCount: $viewCount filter: $filter @@ -5397,7 +5397,7 @@ exports[`generateClient index queries PK and SK index query, with sort direction "options": { "body": { "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { - listByDescriptionAndViewCount( + listSecondaryIndexModelByDescriptionAndViewCount( description: $description viewCount: $viewCount filter: $filter @@ -5450,7 +5450,7 @@ exports[`generateClient index queries PK-only index query 1`] = ` "options": { "body": { "query": "query ($title: String!, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { - listByTitle( + listSecondaryIndexModelByTitle( title: $title filter: $filter sortDirection: $sortDirection diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index 629e37efed3..af1ecd7db17 100644 --- a/packages/api-graphql/__tests__/internals/generateClient.test.ts +++ b/packages/api-graphql/__tests__/internals/generateClient.test.ts @@ -5337,9 +5337,10 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); - const { data } = await client.models.SecondaryIndexModel.listByTitle({ - title: 'Hello World', - }); + const { data } = + await client.models.SecondaryIndexModel.listSecondaryIndexModelByTitle({ + title: 'Hello World', + }); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -5374,10 +5375,12 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); const { data } = - await client.models.SecondaryIndexModel.listByDescriptionAndViewCount({ - description: 'something something', - viewCount: { gt: 4 }, - }); + await client.models.SecondaryIndexModel.listSecondaryIndexModelByDescriptionAndViewCount( + { + description: 'something something', + viewCount: { gt: 4 }, + }, + ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -5427,7 +5430,7 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); const { data } = - await client.models.SecondaryIndexModel.listByDescriptionAndViewCount( + await client.models.SecondaryIndexModel.listSecondaryIndexModelByDescriptionAndViewCount( { description: 'match', viewCount: { lt: 4 }, @@ -5476,7 +5479,7 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); const { data } = - await client.models.SecondaryIndexModel.listByDescriptionAndViewCount( + await client.models.SecondaryIndexModel.listSecondaryIndexModelByDescriptionAndViewCount( { description: 'match', viewCount: { lt: 4 }, diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index f3ce9e0cc0e..489326db344 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -86,7 +86,7 @@ "dependencies": { "@aws-amplify/api-rest": "4.0.28", "@aws-amplify/core": "6.0.28", - "@aws-amplify/data-schema": "^0.17.0", + "@aws-amplify/data-schema": "^0.18.0", "@aws-sdk/types": "3.387.0", "graphql": "15.8.0", "rxjs": "^7.8.1", diff --git a/yarn.lock b/yarn.lock index 182e8479aee..25d09a78e47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,10 +24,10 @@ graphql "15.8.0" rxjs "^7.8.1" -"@aws-amplify/data-schema@^0.17.0": - version "0.17.0" - resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema/-/data-schema-0.17.0.tgz#f6950c3e66fcc17acfdceb67f562e87c15e47698" - integrity sha512-UTKz2Jpd7aLPlLql/eY1hINXRsIIW7bUxNU0uVzxUaj8Jk31delAT2qvOeSkCE8He66VSPrpYKXLp3w8tlMUAA== +"@aws-amplify/data-schema@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema/-/data-schema-0.18.0.tgz#99cd0f4f2ceff14612c86a50a36c4a671c851b9f" + integrity sha512-HIKV5HfKqHuEihbN9qt3jmd0mua2XGjaPmRAN7hvfF6THyUJAVpvDbh4QFQ5Q9LtRnXUyRwTzrI9pRcAlYS8Ow== dependencies: "@aws-amplify/data-schema-types" "*" "@types/aws-lambda" "^8.10.134" From a5d176b3c9ec1cecd265740e8b4579327ef98e1f Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:41:06 -0400 Subject: [PATCH 30/33] feat(api-graphql): alias authMode identityPool -> iam (#13299) --- .../api-graphql/__tests__/GraphQLAPI.test.ts | 162 ++++++++++++++++++ .../AWSAppSyncRealTimeProvider/index.ts | 6 +- .../src/internals/InternalGraphQLAPI.ts | 15 +- packages/core/src/singleton/API/types.ts | 2 + 4 files changed, 180 insertions(+), 5 deletions(-) diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index 1dcec2bfc23..43779fee5d6 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -3,7 +3,9 @@ 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 * as typedSubscriptions from './fixtures/with-types/subscriptions'; import { expectGet } from './utils/expects'; +import { InternalGraphQLAPIClass } from '../src/internals/InternalGraphQLAPI'; import { __amplify, @@ -668,6 +670,75 @@ describe('API test', () => { ); }); + test('multi-auth default case api-key, using identityPool 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: 'identityPool', + }); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + Auth: expect.any(Object), + configure: expect.any(Function), + getConfig: expect.any(Function), + }), + { + 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: { @@ -1395,5 +1466,96 @@ describe('API test', () => { }), ); }); + + test('identityPool alias with query', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: {}, + }, + }; + + const spy = jest.spyOn( + InternalGraphQLAPIClass.prototype as any, + '_headerBasedAuth', + ); + + const spy2 = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'identityPool', + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + Auth: expect.any(Object), + configure: expect.any(Function), + getConfig: expect.any(Function), + }), + 'iam', + {}, + ); + }); + + test('identityPool alias with subscription', async () => { + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const graphqlResponse = { + data: { + getThread: {}, + }, + }; + + const spy = jest.spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe'); + + const _spy2 = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + await client.graphql({ + query: typedSubscriptions.onCreateThread, + authMode: 'identityPool', + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + authenticationType: 'iam', + }), + expect.objectContaining({}), + ); + }); }); }); diff --git a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts index 3abde3f469d..37cafbf719e 100644 --- a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts @@ -61,6 +61,8 @@ const dispatchApiEvent = (payload: HubPayload) => { Hub.dispatch('api', payload, 'PubSub', AMPLIFY_SYMBOL); }; +// resolved/actual AuthMode values. identityPool gets resolves to IAM upstream in InternalGraphQLAPI._graphqlSubscribe +type ResolvedGraphQLAuthModes = Exclude; export interface ObserverQuery { observer: PubSubContentObserver; query: string; @@ -96,7 +98,7 @@ interface ParsedMessagePayload { export interface AWSAppSyncRealTimeProviderOptions { appSyncGraphqlEndpoint?: string; - authenticationType?: GraphQLAuthMode; + authenticationType?: ResolvedGraphQLAuthModes; query?: string; variables?: Record; apiKey?: string; @@ -935,7 +937,7 @@ export class AWSAppSyncRealTimeProvider { Record | undefined > { const headerHandler: { - [key in GraphQLAuthMode]: ( + [key in ResolvedGraphQLAuthModes]: ( arg0: AWSAppSyncRealTimeAuthInput, ) => Promise> | Record; } = { diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index 75dfb2e0f4e..15b89670314 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -259,7 +259,10 @@ export class InternalGraphQLAPIClass { defaultAuthMode, } = resolveConfig(amplify); - const authMode = explicitAuthMode || defaultAuthMode || 'iam'; + const initialAuthMode = explicitAuthMode || defaultAuthMode || 'iam'; + // identityPool is an alias for iam. TODO: remove 'iam' in v7 + const authMode = + initialAuthMode === 'identityPool' ? 'iam' : initialAuthMode; /** * Retrieve library options from Amplify configuration. @@ -425,13 +428,19 @@ export class InternalGraphQLAPIClass { private _graphqlSubscribe( amplify: AmplifyClassV6, - { query, variables, authMode }: GraphQLOptions, + { query, variables, authMode: explicitAuthMode }: GraphQLOptions, additionalHeaders: CustomHeaders = {}, customUserAgentDetails?: CustomUserAgentDetails, authToken?: string, ): Observable { const config = resolveConfig(amplify); + const initialAuthMode = + explicitAuthMode || config?.defaultAuthMode || 'iam'; + // identityPool is an alias for iam. TODO: remove 'iam' in v7 + const authMode = + initialAuthMode === 'identityPool' ? 'iam' : initialAuthMode; + /** * Retrieve library options from Amplify configuration. * `libraryConfigHeaders` are from the Amplify configuration options, @@ -449,7 +458,7 @@ export class InternalGraphQLAPIClass { variables, appSyncGraphqlEndpoint: config?.endpoint, region: config?.region, - authenticationType: authMode || config?.defaultAuthMode, + authenticationType: authMode, apiKey: config?.apiKey, additionalHeaders, authToken, diff --git a/packages/core/src/singleton/API/types.ts b/packages/core/src/singleton/API/types.ts index 418b2f65a0e..20dd5bd3dfc 100644 --- a/packages/core/src/singleton/API/types.ts +++ b/packages/core/src/singleton/API/types.ts @@ -81,7 +81,9 @@ export type GraphQLAuthMode = | 'apiKey' | 'oidc' | 'userPool' + // @deprecated; use 'identityPool' | 'iam' + | 'identityPool' | 'lambda' | 'none'; From 89ecc6d94e6cc9b3de412866eaf2f0efc367183a Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 26 Apr 2024 14:22:02 -0700 Subject: [PATCH 31/33] chore: pin to use macos 12 in gh actions (#13300) Co-authored-by: Ashwin Kumar --- .github/workflows/callable-canary-e2e.yml | 2 +- .github/workflows/callable-e2e-test-detox.yml | 2 +- .github/workflows/callable-release-verification.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/callable-canary-e2e.yml b/.github/workflows/callable-canary-e2e.yml index 7c820c601a5..29ae6e6115c 100644 --- a/.github/workflows/callable-canary-e2e.yml +++ b/.github/workflows/callable-canary-e2e.yml @@ -15,7 +15,7 @@ jobs: prebuild-macos: uses: ./.github/workflows/callable-prebuild-amplify-js.yml with: - runs_on: macos-latest + runs_on: macos-12 prebuild-samples-staging: secrets: inherit uses: ./.github/workflows/callable-prebuild-samples-staging.yml diff --git a/.github/workflows/callable-e2e-test-detox.yml b/.github/workflows/callable-e2e-test-detox.yml index 8d430bb72c1..168f960c048 100644 --- a/.github/workflows/callable-e2e-test-detox.yml +++ b/.github/workflows/callable-e2e-test-detox.yml @@ -17,7 +17,7 @@ on: jobs: e2e-test: name: E2E-Detox ${{ inputs.test_name }} - runs-on: macos-latest + runs-on: macos-12 timeout-minutes: ${{ inputs.timeout_minutes }} steps: diff --git a/.github/workflows/callable-release-verification.yml b/.github/workflows/callable-release-verification.yml index 30fd4b0c7e0..d5288a615b2 100644 --- a/.github/workflows/callable-release-verification.yml +++ b/.github/workflows/callable-release-verification.yml @@ -10,7 +10,7 @@ jobs: prebuild-macos: uses: ./.github/workflows/callable-prebuild-amplify-js.yml with: - runs_on: macos-latest + runs_on: macos-12 prebuild-samples-staging: secrets: inherit uses: ./.github/workflows/callable-prebuild-samples-staging.yml From fa02396112561ff9a202fbd528839ed3e9855117 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Sat, 27 Apr 2024 17:18:44 -0700 Subject: [PATCH 32/33] Fix/geo location services config (#13303) * fix: Kebab case to Camel case config props * fix: Typo on I18n comment * address fix in tests * Resolve build error * Resolve build error * resolve PR failures * update Geo tests * Integ test check * integ test * integ test * fix parseAWSExports duplicating "search_indices" and "searchIndicies" * fix prettier error * fix bundle size test * fix: update parseAWSExports * cleanup * update bundle size * cleanup * chore: cleanup --------- Co-authored-by: Dan Kiuna Co-authored-by: Dan Kiuna Co-authored-by: ashika112 Co-authored-by: Ashwin Kumar --- packages/aws-amplify/package.json | 6 +++--- packages/core/__tests__/parseAWSExports.test.ts | 16 ++++++++++------ packages/core/src/parseAWSExports.ts | 17 ++++++++--------- .../AmazonLocationServiceProvider.ts | 9 ++++----- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 04526e4a6a9..b2ab1341bd1 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -335,7 +335,7 @@ "name": "[API] generateClient (AppSync)", "path": "./dist/esm/api/index.mjs", "import": "{ generateClient }", - "limit": "39.99 kB" + "limit": "40.09 kB" }, { "name": "[API] REST API handlers", @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "28.09 kB" + "limit": "28.10 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -455,7 +455,7 @@ "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "21.45 kB" + "limit": "21.47 kB" }, { "name": "[Storage] copy (S3)", diff --git a/packages/core/__tests__/parseAWSExports.test.ts b/packages/core/__tests__/parseAWSExports.test.ts index 4e26fc34b2d..1ad4520f7d5 100644 --- a/packages/core/__tests__/parseAWSExports.test.ts +++ b/packages/core/__tests__/parseAWSExports.test.ts @@ -27,9 +27,13 @@ describe('parseAWSExports', () => { items: ['geoJSSearchExample'], default: 'geoJSSearchExample', }, + geofenceCollections: { + items: ['geofenceCollection-dev'], + default: 'geofenceCollection-dev', + }, region, }; - const amazonLocationServiceV4 = { + const expectedAmazonLocationServiceV4 = { maps: { items: { geoJsExampleMap1: { @@ -41,14 +45,14 @@ describe('parseAWSExports', () => { }, default: 'geoJsExampleMap1', }, - search_indices: { - items: ['geoJSSearchExample'], - default: 'geoJSSearchExample', - }, searchIndices: { items: ['geoJSSearchExample'], default: 'geoJSSearchExample', }, + geofenceCollections: { + items: ['geofenceCollection-dev'], + default: 'geofenceCollection-dev', + }, region, }; const restEndpoint1 = { @@ -114,7 +118,7 @@ describe('parseAWSExports', () => { }, }, Geo: { - LocationService: amazonLocationServiceV4, + LocationService: expectedAmazonLocationServiceV4, }, Storage: { S3: { diff --git a/packages/core/src/parseAWSExports.ts b/packages/core/src/parseAWSExports.ts index 5bb29e1be37..295c9e211e8 100644 --- a/packages/core/src/parseAWSExports.ts +++ b/packages/core/src/parseAWSExports.ts @@ -249,15 +249,14 @@ export const parseAWSExports = ( // Geo if (geo) { const { amazon_location_service } = geo; - (amplifyConfig as any).Geo = amazon_location_service - ? { - LocationService: { - ...amazon_location_service, - searchIndices: amazon_location_service.search_indices, - region: amazon_location_service.region, - }, - } - : { ...geo }; + amplifyConfig.Geo = { + LocationService: { + maps: amazon_location_service.maps, + geofenceCollections: amazon_location_service.geofenceCollections, + searchIndices: amazon_location_service.search_indices, + region: amazon_location_service.region, + }, + }; } // REST API diff --git a/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts b/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts index 1e466020357..b5248d5ccbc 100644 --- a/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts +++ b/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts @@ -151,7 +151,7 @@ export class AmazonLocationServiceProvider implements GeoProvider { */ let locationServiceInput: SearchPlaceIndexForTextCommandInput = { Text: text, - IndexName: this._config.search_indices.default, + IndexName: this._config.searchIndices.default, }; /** @@ -217,7 +217,7 @@ export class AmazonLocationServiceProvider implements GeoProvider { */ let locationServiceInput: SearchPlaceIndexForSuggestionsCommandInput = { Text: text, - IndexName: this._config.search_indices.default, + IndexName: this._config.searchIndices.default, }; /** @@ -286,8 +286,7 @@ export class AmazonLocationServiceProvider implements GeoProvider { const searchByPlaceIdInput: GetPlaceCommandInput = { PlaceId: placeId, - IndexName: - options?.searchIndexName || this._config.search_indices.default, + IndexName: options?.searchIndexName || this._config.searchIndices.default, }; const command = new GetPlaceCommand(searchByPlaceIdInput); @@ -325,7 +324,7 @@ export class AmazonLocationServiceProvider implements GeoProvider { const locationServiceInput: SearchPlaceIndexForPositionCommandInput = { Position: coordinates, - IndexName: this._config.search_indices.default, + IndexName: this._config.searchIndices.default, }; if (options) { From 8fb751ad0f7ae9102cb5ae7935ccd6436bb7dbb0 Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:40:27 -0400 Subject: [PATCH 33/33] chore: bump data-schema dep to latest (#13302) chore: bump data-schema dep to latest --- packages/api-graphql/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index 489326db344..7ba5727b314 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -86,7 +86,7 @@ "dependencies": { "@aws-amplify/api-rest": "4.0.28", "@aws-amplify/core": "6.0.28", - "@aws-amplify/data-schema": "^0.18.0", + "@aws-amplify/data-schema": "^1.0.0", "@aws-sdk/types": "3.387.0", "graphql": "15.8.0", "rxjs": "^7.8.1", diff --git a/yarn.lock b/yarn.lock index 25d09a78e47..83c82754b5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,10 +24,10 @@ graphql "15.8.0" rxjs "^7.8.1" -"@aws-amplify/data-schema@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema/-/data-schema-0.18.0.tgz#99cd0f4f2ceff14612c86a50a36c4a671c851b9f" - integrity sha512-HIKV5HfKqHuEihbN9qt3jmd0mua2XGjaPmRAN7hvfF6THyUJAVpvDbh4QFQ5Q9LtRnXUyRwTzrI9pRcAlYS8Ow== +"@aws-amplify/data-schema@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema/-/data-schema-1.0.0.tgz#435f7320bdde034e130a12d0c3613e18d5089b08" + integrity sha512-pvXko9gDnBqyNshXwrkAer3CbFGkwtlMI35wJk48+N1rxLXKLYElADZj6kWwZXmBbI67uD4ZJ3s62Qzsb6lZZA== dependencies: "@aws-amplify/data-schema-types" "*" "@types/aws-lambda" "^8.10.134"