From cacab94887b218bd982c519d6371fa3f63c184b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A9=20Ammatuna?= Date: Sat, 4 Dec 2021 02:51:46 -0800 Subject: [PATCH] feat(geo): batch deleteGeofences call --- .../AmazonLocationServiceProvider.test.ts | 84 +++++++++++ packages/geo/__tests__/data.ts | 5 - packages/geo/src/Geo.ts | 59 +++++++- .../AmazonLocationServiceProvider.ts | 131 +++++++----------- 4 files changed, 187 insertions(+), 92 deletions(-) diff --git a/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts b/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts index ec86d013182..86009f9668d 100644 --- a/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts +++ b/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts @@ -750,6 +750,90 @@ describe('AmazonLocationServiceProvider', () => { expect(results).toEqual(expected); }); + test('deleteGeofences calls batchDeleteGeofences in batches of 10 from input', async () => { + jest.spyOn(Credentials, 'get').mockImplementation(() => { + return Promise.resolve(credentials); + }); + + const locationProvider = new AmazonLocationServiceProvider(); + locationProvider.configure(awsConfig.geo.amazon_location_service); + + const geofenceIds = validGeofences.map(({ geofenceId }) => geofenceId); + + const spyonProvider = jest.spyOn(locationProvider, 'deleteGeofences'); + const spyonClient = jest.spyOn(LocationClient.prototype, 'send'); + spyonClient.mockImplementation(mockDeleteGeofencesCommand); + + const results = await locationProvider.deleteGeofences(geofenceIds); + + const expected = { + successes: geofenceIds, + errors: [], + }; + expect(results).toEqual(expected); + + const spyProviderInput = spyonProvider.mock.calls[0][0]; + + const spyClientInput = spyonClient.mock.calls; + + expect(spyClientInput.length).toEqual( + Math.ceil(spyProviderInput.length / 10) + ); + }); + + test('deleteGeofences properly handles errors with bad network calls', async () => { + jest.spyOn(Credentials, 'get').mockImplementation(() => { + return Promise.resolve(credentials); + }); + + const locationProvider = new AmazonLocationServiceProvider(); + locationProvider.configure(awsConfig.geo.amazon_location_service); + + const input = createGeofenceInputArray(44).map( + ({ geofenceId }) => geofenceId + ); + input[22] = 'badId'; + const validEntries = [...input.slice(0, 20), ...input.slice(30, 44)]; + + const spyonClient = jest.spyOn(LocationClient.prototype, 'send'); + spyonClient.mockImplementation(geofenceInput => { + const entries = geofenceInput.input as any; + + if (entries.GeofenceIds.some(entry => entry === 'badId')) { + return Promise.reject(new Error('ResourceDoesNotExist')); + } + + const resolution = { + Errors: [ + { + Error: { + Code: 'ResourceDoesNotExist', + Message: 'Resource does not exist', + }, + GeofenceId: 'badId', + }, + ], + }; + return Promise.resolve(resolution); + }); + + const results = await locationProvider.deleteGeofences(input); + const badResults = input.slice(20, 30).map(geofenceId => { + return { + error: { + code: 'ResourceDoesNotExist', + message: 'ResourceDoesNotExist', + }, + geofenceId, + }; + }); + const expected = { + successes: validEntries, + errors: badResults, + }; + expect(results).toEqual(expected); + }); + test('should error if there are no geofenceCollections in config', async () => { jest.spyOn(Credentials, 'get').mockImplementationOnce(() => { return Promise.resolve(credentials); diff --git a/packages/geo/__tests__/data.ts b/packages/geo/__tests__/data.ts index 2563e0721f8..2d51c08a87a 100644 --- a/packages/geo/__tests__/data.ts +++ b/packages/geo/__tests__/data.ts @@ -302,11 +302,6 @@ export function mockListGeofencesCommand(command) { export function mockDeleteGeofencesCommand(command) { if (command instanceof BatchDeleteGeofenceCommand) { return { - Successes: command.input.GeofenceIds.map(geofenceId => { - return { - GeofenceId: geofenceId, - }; - }), Errors: [], }; } diff --git a/packages/geo/src/Geo.ts b/packages/geo/src/Geo.ts index 31290f84cf7..5d4a1ae447a 100644 --- a/packages/geo/src/Geo.ts +++ b/packages/geo/src/Geo.ts @@ -37,6 +37,7 @@ import { Geofence, ListGeofenceOptions, ListGeofenceResults, + DeleteGeofencesResults, } from './types'; const logger = new Logger('Geo'); @@ -194,7 +195,7 @@ export class GeoClass { } /** - * Create geofences inside of a geofence collection + * Create geofences * @param geofences - Single or array of geofence objects to create * @param options? - Optional parameters for creating geofences * @returns {Promise} - Promise that resolves to an object with: @@ -227,7 +228,7 @@ export class GeoClass { } /** - * Update geofences inside of a geofence collection + * Update geofences * @param geofences - Single or array of geofence objects to create * @param options? - Optional parameters for creating geofences * @returns {Promise} - Promise that resolves to an object with: @@ -265,6 +266,12 @@ export class GeoClass { } } + /** + * Get a single geofence by geofenceId + * @param geofenceId: string + * @param options?: GeofenceOptions + * @returns Promise - Promise that resolves to a geofence object + */ public async getGeofence( geofenceId: string, options?: GeofenceOptions @@ -283,7 +290,7 @@ export class GeoClass { } /** - * List geofences from a geofence collection + * List geofences * @param options?: ListGeofenceOptions * @returns {Promise} - Promise that resolves to an object with: * entries: list of geofences - 100 geofences are listed per page @@ -302,6 +309,52 @@ export class GeoClass { throw error; } } + + /** + * Delete geofences + * @param geofenceIds: string|string[] + * @param options?: GeofenceOptions + * @returns {Promise} - Promise that resolves to an object with: + * successes: list of geofences successfully deleted + * errors: list of geofences that failed to delete + */ + public async deleteGeofences( + geofenceIds: string | string[], + options?: GeofenceOptions + ): Promise { + const { providerName = DEFAULT_PROVIDER } = options || {}; + const prov = this.getPluggable(providerName); + + // If single geofence input, make it an array for batch call + let geofenceIdsInputArray; + if (!Array.isArray(geofenceIds)) { + geofenceIdsInputArray = [geofenceIds]; + } else { + geofenceIdsInputArray = geofenceIds; + } + + // Validate all geofenceIds are valid + const badGeofenceIds = geofenceIdsInputArray.filter(geofenceId => { + try { + validateGeofenceId(geofenceId); + } catch (error) { + return false; + } + }); + if (badGeofenceIds.length > 0) { + const errorString = `Invalid geofence ids: ${badGeofenceIds}`; + logger.debug(errorString); + throw new Error(errorString); + } + + // Delete geofences + try { + return await prov.deleteGeofences(geofenceIdsInputArray, options); + } catch (error) { + logger.debug(error); + throw error; + } + } } export const Geo = new GeoClass(); diff --git a/packages/geo/src/Providers/AmazonLocationServiceProvider.ts b/packages/geo/src/Providers/AmazonLocationServiceProvider.ts index bfd1d668153..665df2ef971 100644 --- a/packages/geo/src/Providers/AmazonLocationServiceProvider.ts +++ b/packages/geo/src/Providers/AmazonLocationServiceProvider.ts @@ -508,6 +508,14 @@ export class AmazonLocationServiceProvider implements GeoProvider { return results; } + /** + * Delete geofences from a geofence collection + * @param geofenceIds: string|string[] + * @param options?: GeofenceOptions + * @returns {Promise} - Promise that resolves to an object with: + * successes: list of geofences successfully deleted + * errors: list of geofences that failed to delete + */ public async deleteGeofences( geofenceIds: string[], options?: AmazonLocationServiceGeofenceOptions @@ -525,32 +533,49 @@ export class AmazonLocationServiceProvider implements GeoProvider { throw error; } - // TODO: batchDeleteGeofence due to 10 geofences per request limit - // const results = await this._batchDeleteGeofence(geofenceIds, options); + const results: AmazonLocationServiceDeleteGeofencesResults = { + successes: [], + errors: [], + }; - const response = await this._AmazonLocationServiceBatchDeleteGeofenceCall( - geofenceIds, - options?.collectionName - ); + const batches = []; - // Convert response to camelCase for return - const { Errors } = response; - - const errorGeofenceIds = Errors.map(({ GeofenceId }) => GeofenceId); - const camelcaseErrors: AmazonLocationServiceBatchGeofenceError[] = - Errors.map(({ GeofenceId, Error: { Code, Message } }) => ({ - geofenceId: GeofenceId, - error: { - code: Code, - message: Message as AmazonLocationServiceBatchGeofenceErrorMessages, - }, - })); + let count = 0; + while (count < geofenceIds.length) { + batches.push(geofenceIds.slice(count, (count += 10))); + } - const results: AmazonLocationServiceDeleteGeofencesResults = { - errors: camelcaseErrors, - successes: geofenceIds.filter(Id => !errorGeofenceIds.includes(Id)), - }; + await Promise.all( + batches.map(async batch => { + let response; + try { + response = await this._AmazonLocationServiceBatchDeleteGeofenceCall( + batch, + options?.collectionName || this._config.geofenceCollections.default + ); + } catch (error) { + // If the API call fails, add the geofences to the errors array and move to next batch + batch.forEach(geofenceId => { + const errorObject = { + geofenceId, + error: { + code: error.message, + message: error.message, + }, + }; + results.errors.push(errorObject); + }); + return; + } + const badGeofenceIds = results.errors.map( + ({ geofenceId }) => geofenceId + ); + results.successes.push( + ...batch.filter(Id => !badGeofenceIds.includes(Id)) + ); + }) + ); return results; } @@ -741,66 +766,4 @@ export class AmazonLocationServiceProvider implements GeoProvider { } return response; } - - // TODO: Fix this function - private async _batchDeleteGeofence( - geofenceIds: string[], - options: AmazonLocationServiceGeofenceOptions - ) { - // Convert geofences to PascalCase for Amazon Location Ser - const results: AmazonLocationServiceDeleteGeofencesResults = { - successes: [], - errors: [], - }; - - for (let index = 0; index < geofenceIds.length; index + 10) { - // Slice off 10 geofences from input clone due to Amazon Location Service API limit - const batch = geofenceIds.slice(index, index + 10); - console.log( - '💣🔥>>>>>>> ~ file: AmazonLocationServiceProvider.ts ~ line 744 ~ AmazonLocationServiceProvider ~ _batchDeleteGeofence ~ batch', - batch - ); - - // Make API call for the 10 geofences - let response: BatchDeleteGeofenceCommandOutput; - try { - response = await this._AmazonLocationServiceBatchDeleteGeofenceCall( - geofenceIds, - options?.collectionName - ); - } catch (error) { - // If the API call fails, add the geofences to the errors array and move to next batch - batch.forEach(geofence => { - results.errors.push({ - geofenceId: geofence, - error: { - code: 'APIConnectionError', - message: error.message, - }, - }); - }); - continue; - } - - // Convert response to camelCase for return - - const Errors: BatchDeleteGeofenceError[] = response.Errors; - const errorGeofenceIds = Errors.map(({ GeofenceId }) => GeofenceId); - const camelcaseErrors: AmazonLocationServiceBatchGeofenceError[] = - Errors.map(({ GeofenceId, Error: { Code, Message } }) => ({ - geofenceId: GeofenceId, - error: { - code: Code, - message: Message as AmazonLocationServiceBatchGeofenceErrorMessages, - }, - })); - - results.errors = [...results.errors, ...camelcaseErrors]; - results.successes = [ - ...results.successes, - ...geofenceIds.filter(Id => !errorGeofenceIds.includes(Id)), - ]; - } - return results; - } }