From c4ee6f320b37ca01a151e88bc2c54f8c81ad34c4 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 18 Sep 2024 22:40:27 -0700 Subject: [PATCH] feat(stroage): add foundation CopyObjectClient --- .../CreateCopyObjectDeserializer.test.ts | 76 +++++++++++++++++++ .../serde/CreateCopyObjectSerializer.test.ts | 37 +++++++++ .../__tests__/providers/s3/apis/copy.test.ts | 32 ++++---- .../factories/serviceClients/s3/index.ts | 2 +- .../s3/s3data/createCopyObjectClient.ts | 21 +++++ .../serviceClients/s3/s3data/index.ts | 1 + .../serde/CreateCopyObjectDeserializer.ts | 28 +++++++ .../serde/CreateCopyObjectSerializer.ts | 53 +++++++++++++ .../s3/s3data/shared/serde/index.ts | 2 + .../src/providers/s3/apis/internal/copy.ts | 3 +- 10 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectDeserializer.test.ts create mode 100644 packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectSerializer.test.ts create mode 100644 packages/storage/src/foundation/factories/serviceClients/s3/s3data/createCopyObjectClient.ts create mode 100644 packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectDeserializer.ts create mode 100644 packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectSerializer.ts diff --git a/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectDeserializer.test.ts b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectDeserializer.test.ts new file mode 100644 index 00000000000..bc8e46de3ae --- /dev/null +++ b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectDeserializer.test.ts @@ -0,0 +1,76 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { HttpResponse } from '@aws-amplify/core/internals/aws-client-utils'; +import * as clientUtils from '@aws-amplify/core/internals/aws-client-utils'; + +import { createCopyObjectDeserializer } from '../../../../../../../../src/foundation/factories/serviceClients/s3/s3data/shared/serde'; +import { StorageError } from '../../../../../../../../src/errors/StorageError'; + +describe('createCopyObjectDeserializer', () => { + const deserializer = createCopyObjectDeserializer(); + + it('returns body for 2xx status code', async () => { + const response: HttpResponse = { + statusCode: 200, + headers: { + 'x-amz-id-2': 'requestId2', + 'x-amz-request-id': 'requestId', + }, + body: { + json: () => Promise.resolve({}), + blob: () => Promise.resolve(new Blob()), + text: () => + Promise.resolve( + '' + + 'string' + + 'timestamp' + + 'string' + + 'string' + + 'string' + + 'string' + + '', + ), + }, + }; + const output = await deserializer(response); + + expect(output).toEqual( + expect.objectContaining({ + $metadata: { + requestId: response.headers['x-amz-request-id'], + extendedRequestId: response.headers['x-amz-id-2'], + httpStatusCode: 200, + }, + }), + ); + }); + + it('throws StorageError for 4xx status code', async () => { + const expectedErrorName = 'TestError'; + const expectedErrorMessage = '400'; + const expectedError = new Error(expectedErrorMessage); + expectedError.name = expectedErrorName; + + jest + .spyOn(clientUtils, 'parseJsonError') + .mockReturnValueOnce(expectedError as any); + + const response: HttpResponse = { + statusCode: 400, + body: { + json: () => Promise.resolve({}), + blob: () => Promise.resolve(new Blob()), + text: () => Promise.resolve(''), + }, + headers: {}, + }; + + expect(deserializer(response as any)).rejects.toThrow( + new StorageError({ + name: expectedErrorName, + message: expectedErrorMessage, + }), + ); + }); +}); diff --git a/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectSerializer.test.ts b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectSerializer.test.ts new file mode 100644 index 00000000000..b768d983cd0 --- /dev/null +++ b/packages/storage/__tests__/foundation/factories/serviceClients/s3/data/shared/serde/CreateCopyObjectSerializer.test.ts @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; + +import { createCopyObjectSerializer } from '../../../../../../../../src/foundation/factories/serviceClients/s3/s3data/shared/serde'; + +describe('createCopyObjectSerializer', () => { + it('should serialize copyObject request', async () => { + const input = { + Bucket: 'bucket', + CopySource: 'sourceBucket/sourceKey', + Key: 'mykey', + CacheControl: 'cacheControl', + ContentType: 'contentType', + ACL: 'acl', + }; + const endPointUrl = 'http://test.com'; + const endpoint = { url: new AmplifyUrl(endPointUrl) }; + + const serializer = createCopyObjectSerializer(); + const result = await serializer(input, endpoint); + + expect(result).toEqual({ + url: expect.objectContaining({ + href: `${endPointUrl}/${input.Key}`, + }), + method: 'PUT', + headers: expect.objectContaining({ + 'x-amz-copy-source': 'sourceBucket/sourceKey', + 'cache-control': 'cacheControl', + 'content-type': 'contentType', + 'x-amz-acl': 'acl', + }), + }); + }); +}); diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index 56104e84d17..faaba0b87c2 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -6,7 +6,7 @@ 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/s3data'; +import { createCopyObjectClient } from '../../../../src/foundation/factories/serviceClients/s3'; import { copy } from '../../../../src/providers/s3/apis'; import { CopyInput, @@ -17,7 +17,7 @@ import { import './testUtils'; import { BucketInfo } from '../../../../src/providers/s3/types/options'; -jest.mock('../../../../src/providers/s3/utils/client/s3data'); +jest.mock('../../../../src/foundation/factories/serviceClients/s3'); jest.mock('@aws-amplify/core', () => ({ ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { return { debug: jest.fn() }; @@ -29,7 +29,10 @@ jest.mock('@aws-amplify/core', () => ({ }, }, })); -const mockCopyObject = copyObject as jest.Mock; + +const mockCopyObject = jest.fn(); +const mockCreateCopyObjectClient = jest.mocked(createCopyObjectClient); + const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; @@ -81,6 +84,7 @@ describe('copy API', () => { Metadata: { key: 'value' }, }; }); + mockCreateCopyObjectClient.mockReturnValueOnce(mockCopyObject); }); afterEach(() => { jest.clearAllMocks(); @@ -188,8 +192,8 @@ describe('copy API', () => { }, }); expect(key).toEqual(destinationKey); - expect(copyObject).toHaveBeenCalledTimes(1); - await expect(copyObject).toBeLastCalledWithConfigAndInput( + expect(mockCopyObject).toHaveBeenCalledTimes(1); + await expect(mockCopyObject).toBeLastCalledWithConfigAndInput( copyObjectClientConfig, { ...copyObjectClientBaseParams, @@ -213,8 +217,8 @@ describe('copy API', () => { bucket: bucketInfo, }, }); - expect(copyObject).toHaveBeenCalledTimes(1); - await expect(copyObject).toBeLastCalledWithConfigAndInput( + expect(mockCopyObject).toHaveBeenCalledTimes(1); + await expect(mockCopyObject).toBeLastCalledWithConfigAndInput( { credentials, region: bucketInfo.region, @@ -241,6 +245,7 @@ describe('copy API', () => { Metadata: { key: 'value' }, }; }); + mockCreateCopyObjectClient.mockReturnValueOnce(mockCopyObject); }); afterEach(() => { jest.clearAllMocks(); @@ -272,8 +277,8 @@ describe('copy API', () => { destination: { path: destinationPath }, }); expect(path).toEqual(expectedDestinationPath); - expect(copyObject).toHaveBeenCalledTimes(1); - await expect(copyObject).toBeLastCalledWithConfigAndInput( + expect(mockCopyObject).toHaveBeenCalledTimes(1); + await expect(mockCopyObject).toBeLastCalledWithConfigAndInput( copyObjectClientConfig, { ...copyObjectClientBaseParams, @@ -295,8 +300,8 @@ describe('copy API', () => { bucket: bucketInfo, }, }); - expect(copyObject).toHaveBeenCalledTimes(1); - await expect(copyObject).toBeLastCalledWithConfigAndInput( + expect(mockCopyObject).toHaveBeenCalledTimes(1); + await expect(mockCopyObject).toBeLastCalledWithConfigAndInput( { credentials, region: bucketInfo.region, @@ -324,6 +329,7 @@ describe('copy API', () => { name: 'NotFound', }), ); + mockCreateCopyObjectClient.mockReturnValueOnce(mockCopyObject); expect.assertions(3); const missingSourceKey = 'SourceKeyNotFound'; try { @@ -332,8 +338,8 @@ describe('copy API', () => { destination: { key: destinationKey }, }); } catch (error: any) { - expect(copyObject).toHaveBeenCalledTimes(1); - await expect(copyObject).toBeLastCalledWithConfigAndInput( + expect(mockCopyObject).toHaveBeenCalledTimes(1); + await expect(mockCopyObject).toBeLastCalledWithConfigAndInput( copyObjectClientConfig, { ...copyObjectClientBaseParams, diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/index.ts b/packages/storage/src/foundation/factories/serviceClients/s3/index.ts index 050bfa63351..db25f3a2e3b 100644 --- a/packages/storage/src/foundation/factories/serviceClients/s3/index.ts +++ b/packages/storage/src/foundation/factories/serviceClients/s3/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { createDeleteObjectClient } from './s3data/createDeleteObjectClient'; +export { createDeleteObjectClient, createCopyObjectClient } from './s3data'; diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/createCopyObjectClient.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/createCopyObjectClient.ts new file mode 100644 index 00000000000..330cda31a42 --- /dev/null +++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/createCopyObjectClient.ts @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers'; + +import { s3TransferHandler } from '../../../../dI'; + +import { + createCopyObjectDeserializer, + createCopyObjectSerializer, +} from './shared/serde'; +import { DEFAULT_SERVICE_CLIENT_API_CONFIG } from './constants'; + +export const createCopyObjectClient = () => { + return composeServiceApi( + s3TransferHandler, + createCopyObjectSerializer(), + createCopyObjectDeserializer(), + { ...DEFAULT_SERVICE_CLIENT_API_CONFIG, responseType: 'text' }, + ); +}; diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts index fd2f388c326..3c184be5b4a 100644 --- a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts +++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/index.ts @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { createDeleteObjectClient } from './createDeleteObjectClient'; +export { createCopyObjectClient } from './createCopyObjectClient'; diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectDeserializer.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectDeserializer.ts new file mode 100644 index 00000000000..bbbdfc6c03a --- /dev/null +++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectDeserializer.ts @@ -0,0 +1,28 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + HttpResponse, + parseMetadata, +} from '@aws-amplify/core/internals/aws-client-utils'; + +import { buildStorageServiceError } from '../../../deserializeHelpers'; +import { parseXmlBody, parseXmlError } from '../../../parsePayload'; +import type { CopyObjectCommandOutput } from '../../types'; + +type CopyObjectOutput = CopyObjectCommandOutput; + +export const createCopyObjectDeserializer = + (): ((response: HttpResponse) => Promise) => + async (response: HttpResponse): Promise => { + if (response.statusCode >= 300) { + const error = (await parseXmlError(response)) as Error; + throw buildStorageServiceError(error, response.statusCode); + } else { + await parseXmlBody(response); + + return { + $metadata: parseMetadata(response), + }; + } + }; diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectSerializer.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectSerializer.ts new file mode 100644 index 00000000000..1147695c07e --- /dev/null +++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/CreateCopyObjectSerializer.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + Endpoint, + HttpRequest, +} from '@aws-amplify/core/internals/aws-client-utils'; +import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; + +import { + assignStringVariables, + serializeObjectConfigsToHeaders, + serializePathnameObjectKey, + validateS3RequiredParameter, +} from '../../../serializeHelpers'; +import type { CopyObjectCommandInput } from '../../types'; + +type CopyObjectInput = Pick< + CopyObjectCommandInput, + | 'Bucket' + | 'CopySource' + | 'Key' + | 'MetadataDirective' + | 'CacheControl' + | 'ContentType' + | 'ContentDisposition' + | 'ContentLanguage' + | 'Expires' + | 'ACL' + | 'Tagging' + | 'Metadata' +>; + +export const createCopyObjectSerializer = + (): ((input: CopyObjectInput, endpoint: Endpoint) => Promise) => + async (input: CopyObjectInput, endpoint: Endpoint): Promise => { + const headers = { + ...(await serializeObjectConfigsToHeaders(input)), + ...assignStringVariables({ + 'x-amz-copy-source': input.CopySource, + 'x-amz-metadata-directive': input.MetadataDirective, + }), + }; + const url = new AmplifyUrl(endpoint.url.toString()); + validateS3RequiredParameter(!!input.Key, 'Key'); + url.pathname = serializePathnameObjectKey(url, input.Key); + + return { + method: 'PUT', + headers, + url, + }; + }; diff --git a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts index eda9a6571ec..ff83f8334cd 100644 --- a/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts +++ b/packages/storage/src/foundation/factories/serviceClients/s3/s3data/shared/serde/index.ts @@ -3,3 +3,5 @@ export { createDeleteObjectSerializer } from './CreateDeleteObjectSerializer'; export { createDeleteObjectDeserializer } from './CreateDeleteObjectDeserializer'; +export { createCopyObjectDeserializer } from './CreateCopyObjectDeserializer'; +export { createCopyObjectSerializer } from './CreateCopyObjectSerializer'; diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts index 9119917efa1..7cf32c94ed1 100644 --- a/packages/storage/src/providers/s3/apis/internal/copy.ts +++ b/packages/storage/src/providers/s3/apis/internal/copy.ts @@ -18,7 +18,7 @@ import { } from '../../utils'; import { StorageValidationErrorCode } from '../../../../errors/types/validation'; import { assertValidationError } from '../../../../errors/utils/assertValidationError'; -import { copyObject } from '../../utils/client/s3data'; +import { createCopyObjectClient } from '../../../../foundation/factories/serviceClients/s3'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; @@ -180,6 +180,7 @@ const serviceCopy = async ({ bucket: string; s3Config: ResolvedS3Config; }) => { + const copyObject = createCopyObjectClient(); await copyObject( { ...s3Config,