Skip to content

Commit

Permalink
feat(storage): getProperties API (#11378)
Browse files Browse the repository at this point in the history
* feat: add AWSS3Provider getProperties Implementation

---------

Co-authored-by: Jim Blanchard <jim.l.blanchard@gmail.com>
Co-authored-by: AllanZhengYP <zheallan@amazon.com>
  • Loading branch information
3 people authored May 22, 2023
1 parent 144de47 commit 3bed12b
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 25 deletions.
2 changes: 1 addition & 1 deletion packages/predictions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"name": "Predictions (Identify provider)",
"path": "./lib-esm/index.js",
"import": "{ Amplify, Predictions, AmazonAIIdentifyPredictionsProvider }",
"limit": "104.5 kB"
"limit": "105 kB"
},
{
"name": "Predictions (Interpret provider)",
Expand Down
74 changes: 74 additions & 0 deletions packages/storage/__tests__/Storage-unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class TestCustomProvider implements StorageProvider {
return Promise.resolve({ newKey: 'get' });
}

getProperties(key: string, options?: CustomProviderConfig) {
return Promise.resolve({ newKey: 'getProperties' });
}

put(key: string, object: any, config: CustomProviderConfig) {
return Promise.resolve({ newKey: 'put' });
}
Expand Down Expand Up @@ -614,6 +618,76 @@ describe('Storage', () => {
});
});

describe('getProperties test', () => {
let storage: StorageClass;
let provider: StorageProvider;
let getPropertiesSpy: jest.SpyInstance;

beforeEach(() => {
storage = new StorageClass();
provider = new AWSStorageProvider();
storage.addPluggable(provider);
storage.configure(options);
getPropertiesSpy = jest
.spyOn(AWSStorageProvider.prototype, 'getProperties')
.mockImplementation(() =>
Promise.resolve({
contentType: 'text/plain',
contentLength: 100,
eTag: 'etag',
lastModified: new Date('20 Oct 2023'),
metadata: { key: '' },
})
);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('with default S3 provider', () => {
test('get properties of object successfully', async () => {
const result = await storage.getProperties('key');
expect(getPropertiesSpy).toBeCalled();
expect(result).toEqual({
contentType: 'text/plain',
contentLength: 100,
eTag: 'etag',
lastModified: new Date('20 Oct 2023'),
metadata: { key: '' },
});
getPropertiesSpy.mockClear();
});

test('get properties of object with available config', async () => {
await storage.getProperties('key', {
SSECustomerAlgorithm: 'aes256',
SSECustomerKey: 'key',
SSECustomerKeyMD5: 'md5',
});
});
});

test('get properties without provider', async () => {
const storage = new StorageClass();
try {
await storage.getProperties('key');
} catch (err) {
expect(err).toEqual(new Error('No plugin found with providerName'));
}
});

test('get properties with custom provider should work with no generic type provided', async () => {
const customProvider = new TestCustomProvider();
const customProviderGetSpy = jest.spyOn(customProvider, 'getProperties');
storage.addPluggable(customProvider);
await storage.getProperties('key', {
provider: 'customProvider',
});
expect(customProviderGetSpy).toBeCalled();
});
});

describe('put test', () => {
let storage: StorageClass;
let provider: StorageProvider;
Expand Down
72 changes: 67 additions & 5 deletions packages/storage/__tests__/providers/AWSS3Provider-unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ListObjectsV2Command,
CreateMultipartUploadCommand,
UploadPartCommand,
HeadObjectCommand,
} from '@aws-sdk/client-s3';
import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner';

Expand Down Expand Up @@ -52,6 +53,14 @@ S3Client.prototype.send = jest.fn(async command => {
Contents: [resultObj],
IsTruncated: false,
};
} else if (command instanceof HeadObjectCommand) {
return {
ContentLength: '100',
ContentType: 'text/plain',
ETag: 'etag',
LastModified: 'lastmodified',
Metadata: { key: 'value' },
};
}
return 'data';
});
Expand Down Expand Up @@ -519,6 +528,7 @@ describe('StorageProvider test', () => {
return Promise.resolve(credentials);
});
});

test('get existing object with validateObjectExistence option', async () => {
expect.assertions(5);
const options_with_validateObjectExistence = Object.assign(
Expand Down Expand Up @@ -556,11 +566,6 @@ describe('StorageProvider test', () => {

test('get non-existing object with validateObjectExistence option', async () => {
expect.assertions(2);
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return new Promise((res, rej) => {
res({});
});
});
const dispatchSpy = jest.spyOn(StorageUtils, 'dispatchStorageEvent');
jest
.spyOn(S3Client.prototype, 'send')
Expand All @@ -586,6 +591,63 @@ describe('StorageProvider test', () => {
});
});

describe('getProperties test', () => {
beforeEach(() => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});
});

test('getProperties successfully', async () => {
expect.assertions(4);
const spyon = jest.spyOn(S3Client.prototype, 'send');
const dispatchSpy = jest.spyOn(StorageUtils, 'dispatchStorageEvent');
const metadata = { key: 'value' };
expect(await storage.getProperties('key')).toEqual({
contentLength: '100',
contentType: 'text/plain',
eTag: 'etag',
lastModified: 'lastmodified',
metadata,
});
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toBeCalledWith(
false,
'getProperties',
{ method: 'getProperties', result: 'success' },
null,
'getProperties successful for key'
);
expect(spyon.mock.calls[0][0].input).toEqual({
Bucket: 'bucket',
Key: 'public/key',
});
spyon.mockClear();
});

test('get properties of non-existing object', async () => {
expect.assertions(2);
const dispatchSpy = jest.spyOn(StorageUtils, 'dispatchStorageEvent');
jest
.spyOn(S3Client.prototype, 'send')
.mockImplementationOnce(async params => {
throw { $metadata: { httpStatusCode: 404 }, name: 'NotFound' };
});
try {
await storage.getProperties('invalid_key');
} catch (error) {
expect(error.$metadata.httpStatusCode).toBe(404);
expect(dispatchSpy).toBeCalledWith(
false,
'getProperties',
{ method: 'getProperties', result: 'failed' },
null,
'invalid_key not found'
);
}
});
});

describe('put test', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down
55 changes: 38 additions & 17 deletions packages/storage/src/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
StorageListOutput,
StorageCopyOutput,
UploadTask,
StorageGetPropertiesConfig,
StorageGetPropertiesOutput,
} from './types';
import axios, { CancelTokenSource } from 'axios';
import { PutObjectCommandInput } from '@aws-sdk/client-s3';
Expand Down Expand Up @@ -246,22 +248,22 @@ export class Storage {
config?: StorageCopyConfig<T>
): StorageCopyOutput<T> {
const provider = config?.provider || DEFAULT_PROVIDER;
const prov = this._pluggables.find(
const plugin = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
if (plugin === undefined) {
logger.debug('No plugin found with providerName', provider);
return Promise.reject(
'No plugin found in Storage for the provider'
) as StorageCopyOutput<T>;
}
const cancelTokenSource = this.getCancellableTokenSource();
if (typeof prov.copy !== 'function') {
if (typeof plugin.copy !== 'function') {
return Promise.reject(
`.copy is not implemented on provider ${prov.getProviderName()}`
`.copy is not implemented on provider ${plugin.getProviderName()}`
) as StorageCopyOutput<T>;
}
const responsePromise = prov.copy(src, dest, {
const responsePromise = plugin.copy(src, dest, {
...config,
cancelTokenSource,
});
Expand All @@ -285,17 +287,17 @@ export class Storage {
T extends StorageProvider | { [key: string]: any; download?: boolean }
>(key: string, config?: StorageGetConfig<T>): StorageGetOutput<T> {
const provider = config?.provider || DEFAULT_PROVIDER;
const prov = this._pluggables.find(
const plugin = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
if (plugin === undefined) {
logger.debug('No plugin found with providerName', provider);
return Promise.reject(
'No plugin found in Storage for the provider'
) as StorageGetOutput<T>;
}
const cancelTokenSource = this.getCancellableTokenSource();
const responsePromise = prov.get(key, {
const responsePromise = plugin.get(key, {
...config,
cancelTokenSource,
});
Expand All @@ -307,6 +309,25 @@ export class Storage {
return axios.isCancel(error);
}

public getProperties<T extends StorageProvider | { [key: string]: any }>(
key: string,
config?: StorageGetPropertiesConfig<T>
): StorageGetPropertiesOutput<T> {
const provider = config?.provider || DEFAULT_PROVIDER;
const plugin = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (plugin === undefined) {
logger.debug('No plugin found with providerName', provider);
throw new Error('No plugin found with providerName');
}
const cancelTokenSource = this.getCancellableTokenSource();
const responsePromise = plugin.getProperties(key, {
...config,
});
this.updateRequestToBeCancellable(responsePromise, cancelTokenSource);
return responsePromise as StorageGetPropertiesOutput<T>;
}
/**
* Put a file in storage bucket specified to configure method
* @param key - key of the object
Expand All @@ -326,17 +347,17 @@ export class Storage {
config?: StoragePutConfig<T>
): StoragePutOutput<T> {
const provider = config?.provider || DEFAULT_PROVIDER;
const prov = this._pluggables.find(
const plugin = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
if (plugin === undefined) {
logger.debug('No plugin found with providerName', provider);
return Promise.reject(
'No plugin found in Storage for the provider'
) as StoragePutOutput<T>;
}
const cancelTokenSource = this.getCancellableTokenSource();
const response = prov.put(key, object, {
const response = plugin.put(key, object, {
...config,
cancelTokenSource,
});
Expand All @@ -361,16 +382,16 @@ export class Storage {
config?: StorageRemoveConfig<T>
): StorageRemoveOutput<T> {
const provider = config?.provider || DEFAULT_PROVIDER;
const prov = this._pluggables.find(
const plugin = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
if (plugin === undefined) {
logger.debug('No plugin found with providerName', provider);
return Promise.reject(
'No plugin found in Storage for the provider'
) as StorageRemoveOutput<T>;
}
return prov.remove(key, config) as StorageRemoveOutput<T>;
return plugin.remove(key, config) as StorageRemoveOutput<T>;
}

/**
Expand All @@ -388,16 +409,16 @@ export class Storage {
config?: StorageListConfig<T>
): StorageListOutput<T> {
const provider = config?.provider || DEFAULT_PROVIDER;
const prov = this._pluggables.find(
const plugin = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
if (plugin === undefined) {
logger.debug('No plugin found with providerName', provider);
return Promise.reject(
'No plugin found in Storage for the provider'
) as StorageListOutput<T>;
}
return prov.list(path, config) as StorageListOutput<T>;
return plugin.list(path, config) as StorageListOutput<T>;
}
}

Expand Down
Loading

0 comments on commit 3bed12b

Please sign in to comment.