Skip to content

Commit

Permalink
feat: add deleteAttributes to PersistenceAdapter interface and delete…
Browse files Browse the repository at this point in the history
…PersistentAttributes to AttributesManager interface (#507)
  • Loading branch information
tianrenz committed Feb 15, 2019
1 parent d8b431d commit e7409f1
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 25 deletions.
6 changes: 6 additions & 0 deletions ask-sdk-core/lib/attributes/AttributesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ export interface AttributesManager {
* @return {Promise<void>}
*/
savePersistentAttributes() : Promise<void>;

/**
* Delete persistent attributes from the persistent layer if a persistence adapter is provided.
* @return {Promise<void>}
*/
deletePersistentAttributes?() : Promise<void>;
}
12 changes: 12 additions & 0 deletions ask-sdk-core/lib/attributes/AttributesManagerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export class AttributesManagerFactory {
await options.persistenceAdapter.saveAttributes(options.requestEnvelope, thisPersistentAttributes);
}
},
async deletePersistentAttributes() : Promise<void> {
if (!options.persistenceAdapter) {
throw createAskSdkError(
'AttributesManager',
'Cannot delete PersistentAttributes without persistence adapter!');
}

await options.persistenceAdapter.deleteAttributes(options.requestEnvelope);

thisPersistentAttributes = undefined;
persistentAttributesSet = false;
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ import { RequestEnvelope } from 'ask-sdk-model';
export interface PersistenceAdapter {
getAttributes(requestEnvelope : RequestEnvelope) : Promise<{[key : string] : any}>;
saveAttributes(requestEnvelope : RequestEnvelope, attributes : {[key : string] : any}) : Promise<void>;
deleteAttributes?(requestEnvelope : RequestEnvelope) : Promise<void>;
}
47 changes: 43 additions & 4 deletions ask-sdk-core/tst/attributes/AttributesManagerFactory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('AttributesManagerFactory', () => {
});
});

it('should throw an error when trying to get persistent attributes without persistence manager', async() => {
it('should throw an error when trying to get persistent attributes without persistence adapter', async() => {
const requestEnvelope = JsonProvider.requestEnvelope();
requestEnvelope.context.System.user.userId = 'userId';
const defaultAttributesManager = AttributesManagerFactory.init({requestEnvelope});
Expand Down Expand Up @@ -145,7 +145,7 @@ describe('AttributesManagerFactory', () => {
expect(await defaultAttributesManager.getPersistentAttributes()).deep.equal({key : 'value'});
});

it('should throw an error when trying to set persistent attributes without persistence manager', () => {
it('should throw an error when trying to set persistent attributes without persistence adapter', () => {
const requestEnvelope = JsonProvider.requestEnvelope();
requestEnvelope.context.System.user.userId = 'userId';
const defaultAttributesManager = AttributesManagerFactory.init({requestEnvelope});
Expand All @@ -162,6 +162,45 @@ describe('AttributesManagerFactory', () => {
throw new Error('should have thrown an error!');
});

it('should be able to delete persistent attributes', async() => {
const requestEnvelope = JsonProvider.requestEnvelope();
requestEnvelope.context.System.user.userId = 'userId';
const mockPersistenceAdapter = new MockPersistenceAdapter();

const defaultAttributesManager = AttributesManagerFactory.init({
persistenceAdapter : mockPersistenceAdapter,
requestEnvelope,
});

expect(await defaultAttributesManager.getPersistentAttributes()).deep.equal({
key_1 : 'v1',
key_2 : 'v2',
state : 'mockState',
});

await defaultAttributesManager.deletePersistentAttributes();

expect(await defaultAttributesManager.getPersistentAttributes()).deep.equal({});
expect(mockPersistenceAdapter.getCounter).eq(2);
});

it('should throw an error when trying to delete persistent attributes without persistence adapter', async() => {
const requestEnvelope = JsonProvider.requestEnvelope();
requestEnvelope.context.System.user.userId = 'userId';
const defaultAttributesManager = AttributesManagerFactory.init({requestEnvelope});

try {
await defaultAttributesManager.deletePersistentAttributes();
} catch (err) {
expect(err.name).equal('AskSdk.AttributesManager Error');
expect(err.message).equal('Cannot delete PersistentAttributes without persistence adapter!');

return;
}

throw new Error('should have thrown an error!');
});

it('should be able to set request attributes', async() => {
const requestEnvelope = JsonProvider.requestEnvelope();
const defaultAttributesManager = AttributesManagerFactory.init({requestEnvelope});
Expand Down Expand Up @@ -189,7 +228,7 @@ describe('AttributesManagerFactory', () => {

});

it('should thrown an error when trying to save persistent attributes without persistence manager', async() => {
it('should thrown an error when trying to save persistent attributes without persistence adapter', async() => {
const requestEnvelope = JsonProvider.requestEnvelope();
requestEnvelope.context.System.user.userId = 'userId';
const defaultAttributesManager = AttributesManagerFactory.init({requestEnvelope});
Expand All @@ -206,7 +245,7 @@ describe('AttributesManagerFactory', () => {
throw new Error('should have thrown an error!');
});

it('should should do nothing if persistentAttributes has not been changed', async() => {
it('should do nothing if persistentAttributes has not been changed', async() => {
const mockPersistenceAdapter = new MockPersistenceAdapter();

const requestEnvelope = JsonProvider.requestEnvelope();
Expand Down
40 changes: 22 additions & 18 deletions ask-sdk-core/tst/mocks/persistence/MockPersistenceAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,38 @@ export class MockPersistenceAdapter implements PersistenceAdapter {
state : 'mockState',
};

public getAttributes(requestEnvelope : RequestEnvelope) : Promise<{[key : string] : any}> {
public async getAttributes(requestEnvelope : RequestEnvelope) : Promise<{[key : string] : any}> {
this.getCounter++;

const id = requestEnvelope.context.System.user.userId;

return new Promise<{[key : string] : any}>((resolve, reject) => {
if (id === this.partitionKey) {
resolve(this.savedAttributes);
} else {
reject(new Error('Resource Not Found'));
}
});
if (id === this.partitionKey) {
return this.savedAttributes;
}

throw new Error('Resource Not Found');
}

public saveAttributes(requestEnvelope : RequestEnvelope, attributes : {[key : string] : any}) : Promise<void> {
public async saveAttributes(requestEnvelope : RequestEnvelope, attributes : {[key : string] : any}) : Promise<void> {
this.saveCounter ++;

const id = requestEnvelope.context.System.user.userId;

return new Promise<void>((resolve, reject) => {
// Enforce the mock DB to only have one entry capacity
if (id === this.partitionKey) {
this.savedAttributes = attributes;
resolve();
} else {
reject(new Error('Maximum Capacity Reached'));
}
});
if (id === this.partitionKey) {
this.savedAttributes = attributes;

return;
}

throw new Error('Maximum Capacity Reached');
}

public async deleteAttributes(requestEnvelope : RequestEnvelope) : Promise<void> {
const id = requestEnvelope.context.System.user.userId;

if (id === this.partitionKey) {
this.savedAttributes = {};
}
}

public resetCounter() : void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,29 @@ export class DynamoDbPersistenceAdapter implements PersistenceAdapter {
);
}
}

/**
* Delete persistence attributes from AWS DynamoDB.
* @param {RequestEnvelope} requestEnvelope Request envelope used to generate partition key.
* @return {Promise<void>}
*/
public async deleteAttributes(requestEnvelope : RequestEnvelope) : Promise<void> {
const attributesId = this.partitionKeyGenerator(requestEnvelope);

const deleteParams : DynamoDB.DocumentClient.DeleteItemInput = {
Key : {
[this.partitionKeyName] : attributesId,
},
TableName : this.tableName,
};

try {
await this.dynamoDBDocumentClient.delete(deleteParams).promise();
} catch (err) {
throw createAskSdkError(
this.constructor.name,
`Could not delete item (${attributesId}) from table (${deleteParams.TableName}): ${err.message}`,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ describe('DynamoDbPersistenceAdapter', () => {
callback(null, {});
}
});
AWS_MOCK.mock('DynamoDB.DocumentClient', 'delete', (params, callback) => {
if (params.TableName !== tableName) {
// table name not valid
callback(resourceNotFoundError, null);
} else {
callback(null, {});
}
});
AWS_MOCK.mock('DynamoDB', 'createTable', (params, callback) => {
if (params.TableName === tableName) {
callback(resourceInUseError, null);
Expand Down Expand Up @@ -131,6 +139,14 @@ describe('DynamoDbPersistenceAdapter', () => {
await persistenceAdapter.saveAttributes(requestEnvelope, {});
});

it('should be able to delete an item from table', async() => {
const persistenceAdapter = new DynamoDbPersistenceAdapter({
tableName,
});

await persistenceAdapter.saveAttributes(requestEnvelope, {});
});

it('should return an empty object when getting item that does not exist in table', async() => {
const persistenceAdapter = new DynamoDbPersistenceAdapter({
tableName,
Expand Down Expand Up @@ -160,6 +176,23 @@ describe('DynamoDbPersistenceAdapter', () => {
throw new Error('should have thrown an error!');
});

it('should throw an error when deleting and the table does not exist', async() => {
const persistenceAdapter = new DynamoDbPersistenceAdapter({
tableName : 'NonExistentTable',
});

try {
await persistenceAdapter.deleteAttributes(requestEnvelope);
} catch (err) {
expect(err.name).equal('AskSdk.DynamoDbPersistenceAdapter Error');
expect(err.message).equal('Could not delete item (userId) from table (NonExistentTable): '
+ 'Requested resource not found');

return;
}
throw new Error('should have thrown an error!');
});

describe('with AutoCreateTable', () => {
it('should throw an error when create table returns error other than ResourceInUseException', () => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,22 @@ export class S3PersistenceAdapter implements PersistenceAdapter {
);
}
}

public async deleteAttributes(requestEnvelope : RequestEnvelope) : Promise<void> {
const objectId = path.join(this.pathPrefix, this.objectKeyGenerator(requestEnvelope));

const deleteParams : S3.DeleteObjectRequest = {
Bucket : this.bucketName,
Key : objectId,
};

try {
await this.s3Client.deleteObject(deleteParams).promise();
} catch (err) {
throw createAskSdkError(
this.constructor.name,
`Could not delete item (${objectId}) from bucket (${deleteParams.Bucket}): ${err.message}`,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('S3PersistenceAdapter', () => {
} else if (params.Key === customObjectKey) {
callback(null, {Body : Buffer.from(JSON.stringify(customAttributes))});
} else if (params.Key === pathPrefixObjectKey) {
callback(null, {Body : Buffer.from(JSON.stringify(pathPrefixObjectAttributes))})
callback(null, {Body : Buffer.from(JSON.stringify(pathPrefixObjectAttributes))});
} else if (params.Key === nonJsonObjectKey) {
callback(null, {Body : Buffer.from(nonJsonObjectAttributes)});
} else if (params.Key === emptyBodyKey) {
Expand All @@ -84,6 +84,13 @@ describe('S3PersistenceAdapter', () => {
callback(null, {});
}
});
AWS_MOCK.mock('S3', 'deleteObject', (params, callback) => {
if (params.Bucket !== bucketName) {
callback(bucketInvalidError, null);
} else {
callback(null, {});
}
});
done();
});

Expand All @@ -104,7 +111,7 @@ describe('S3PersistenceAdapter', () => {
const pathPrefixPersistenceAdapter = new S3PersistenceAdapter({
bucketName,
pathPrefix : 'folder',
})
});

const defaultResult = await defaultPersistenceAdapter.getAttributes(requestEnvelope);
expect(defaultResult.defaultKey).eq('defaultValue');
Expand All @@ -124,6 +131,14 @@ describe('S3PersistenceAdapter', () => {
await persistenceAdapter.saveAttributes(requestEnvelope, {});
});

it('should be able to delete an item from bucket', async() => {
const persistenceAdapter = new S3PersistenceAdapter({
bucketName,
});

await persistenceAdapter.deleteAttributes(requestEnvelope);
});

it('should return an empty object when getting item that does not exist in bucket', async() => {
const persistenceAdapter = new S3PersistenceAdapter({
bucketName,
Expand All @@ -136,7 +151,7 @@ describe('S3PersistenceAdapter', () => {
expect(result).deep.equal({});
});

it('should return an emtpy object when getting item that has empty value', async() => {
it('should return an empty object when getting item that has empty value', async() => {
const persistenceAdapter = new S3PersistenceAdapter({
bucketName,
});
Expand Down Expand Up @@ -182,6 +197,23 @@ describe('S3PersistenceAdapter', () => {
throw new Error('should have thrown an error!');
});

it('should throw an error when deleting and the bucket does not exist', async() => {
const persistenceAdapter = new S3PersistenceAdapter({
bucketName : 'NonExistentBucket',
});

try {
await persistenceAdapter.deleteAttributes(requestEnvelope);
} catch (err) {
expect(err.name).equal('AskSdk.S3PersistenceAdapter Error');
expect(err.message).equal('Could not delete item (userId) from bucket (NonExistentBucket): ' +
'The specified bucket is not valid.');

return;
}
throw new Error('should have thrown an error!');
});

it('should throw an error when getting invalid json object', async() => {
const persistenceAdapter = new S3PersistenceAdapter({
bucketName,
Expand Down

0 comments on commit e7409f1

Please sign in to comment.