Skip to content

Commit

Permalink
feat(geo): add deleteGeofences api (#9314)
Browse files Browse the repository at this point in the history
* feat(@aws-amplify/geo): verify coordinates

* feat(geo): batch deleteGeofences call

* chore(geo): fix issue with response
  • Loading branch information
TreTuna authored Dec 7, 2021
1 parent 0813d50 commit 232aadd
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 17 deletions.
128 changes: 128 additions & 0 deletions packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
validGeometry,
mockGetGeofenceCommand,
mockListGeofencesCommand,
mockDeleteGeofencesCommand,
} from '../data';
import {
SearchByTextOptions,
Expand Down Expand Up @@ -723,4 +724,131 @@ describe('AmazonLocationServiceProvider', () => {
);
});
});

describe('deleteGeofences', () => {
test('deleteGeofences deletes given geofences successfully', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});

LocationClient.prototype.send = jest
.fn()
.mockImplementation(mockDeleteGeofencesCommand);

const locationProvider = new AmazonLocationServiceProvider();
locationProvider.configure(awsConfig.geo.amazon_location_service);

const geofenceIds = validGeofences.map(({ geofenceId }) => geofenceId);

const results = await locationProvider.deleteGeofences(geofenceIds);

const expected = {
successes: geofenceIds,
errors: [],
};

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);
});

const locationProvider = new AmazonLocationServiceProvider();
locationProvider.configure({});

const geofenceIds = validGeofences.map(({ geofenceId }) => geofenceId);

await expect(
locationProvider.deleteGeofences(geofenceIds)
).rejects.toThrow(
'No Geofence Collections found, please run `amplify add geo` to create one and run `amplify push` after.'
);
});
});
});
15 changes: 4 additions & 11 deletions packages/geo/__tests__/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
BatchPutGeofenceCommand,
GetGeofenceCommand,
ListGeofencesCommand,
BatchDeleteGeofenceCommand,
} from '@aws-sdk/client-location';
import camelcaseKeys from 'camelcase-keys';

Expand Down Expand Up @@ -298,18 +299,10 @@ export function mockListGeofencesCommand(command) {
}
}

export function mockListGeofencesCommand(command) {
if (command instanceof ListGeofencesCommand) {
const geofences = createGeofenceOutputArray(200);
if (command.input.NextToken === 'THIS IS YOUR TOKEN') {
return {
Entries: geofences.slice(100, 200),
NextToken: 'THIS IS YOUR SECOND TOKEN',
};
}
export function mockDeleteGeofencesCommand(command) {
if (command instanceof BatchDeleteGeofenceCommand) {
return {
Entries: geofences.slice(0, 100),
NextToken: 'THIS IS YOUR TOKEN',
Errors: [],
};
}
}
59 changes: 56 additions & 3 deletions packages/geo/src/Geo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
Geofence,
ListGeofenceOptions,
ListGeofenceResults,
DeleteGeofencesResults,
} from './types';

const logger = new Logger('Geo');
Expand Down Expand Up @@ -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<CreateUpdateGeofenceResults>} - Promise that resolves to an object with:
Expand Down Expand Up @@ -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<CreateUpdateGeofenceResults>} - Promise that resolves to an object with:
Expand Down Expand Up @@ -265,6 +266,12 @@ export class GeoClass {
}
}

/**
* Get a single geofence by geofenceId
* @param geofenceId: string
* @param options?: GeofenceOptions
* @returns Promise<Geofence> - Promise that resolves to a geofence object
*/
public async getGeofence(
geofenceId: string,
options?: GeofenceOptions
Expand All @@ -283,7 +290,7 @@ export class GeoClass {
}

/**
* List geofences from a geofence collection
* List geofences
* @param options?: ListGeofenceOptions
* @returns {Promise<ListGeofencesResults>} - Promise that resolves to an object with:
* entries: list of geofences - 100 geofences are listed per page
Expand All @@ -302,6 +309,52 @@ export class GeoClass {
throw error;
}
}

/**
* Delete geofences
* @param geofenceIds: string|string[]
* @param options?: GeofenceOptions
* @returns {Promise<DeleteGeofencesResults>} - 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<DeleteGeofencesResults> {
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();
Expand Down
102 changes: 102 additions & 0 deletions packages/geo/src/Providers/AmazonLocationServiceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import {
ListGeofencesCommand,
ListGeofencesCommandInput,
ListGeofencesCommandOutput,
BatchDeleteGeofenceCommand,
BatchDeleteGeofenceCommandInput,
BatchDeleteGeofenceCommandOutput,
} from '@aws-sdk/client-location';

import {
Expand All @@ -51,6 +54,7 @@ import {
CreateUpdateGeofenceResults,
AmazonLocationServiceGeofence,
GeofencePolygon,
AmazonLocationServiceDeleteGeofencesResults,
} from '../types';

const logger = new Logger('AmazonLocationServiceProvider');
Expand Down Expand Up @@ -500,6 +504,77 @@ export class AmazonLocationServiceProvider implements GeoProvider {
return results;
}

/**
* Delete geofences from a geofence collection
* @param geofenceIds: string|string[]
* @param options?: GeofenceOptions
* @returns {Promise<DeleteGeofencesResults>} - 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
): Promise<AmazonLocationServiceDeleteGeofencesResults> {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error('No credentials');
}

// Verify geofence collection exists in aws-config.js
try {
this._verifyGeofenceCollections(options?.collectionName);
} catch (error) {
logger.debug(error);
throw error;
}

const results: AmazonLocationServiceDeleteGeofencesResults = {
successes: [],
errors: [],
};

const batches = [];

let count = 0;
while (count < geofenceIds.length) {
batches.push(geofenceIds.slice(count, (count += 10)));
}

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 = response.Errors.map(
({ geofenceId }) => geofenceId
);
results.successes.push(
...batch.filter(Id => !badGeofenceIds.includes(Id))
);
})
);
return results;
}

/**
* @private
*/
Expand Down Expand Up @@ -660,4 +735,31 @@ export class AmazonLocationServiceProvider implements GeoProvider {
);
return results;
}

private async _AmazonLocationServiceBatchDeleteGeofenceCall(
geofenceIds: string[],
collectionName?: string
): Promise<BatchDeleteGeofenceCommandOutput> {
// Create the BatchDeleteGeofence input
const deleteGeofencesInput: BatchDeleteGeofenceCommandInput = {
GeofenceIds: geofenceIds,
CollectionName:
collectionName || this._config.geofenceCollections.default,
};

const client = new LocationClient({
credentials: this._config.credentials,
region: this._config.region,
customUserAgent: getAmplifyUserAgent(),
});
const command = new BatchDeleteGeofenceCommand(deleteGeofencesInput);

let response: BatchDeleteGeofenceCommandOutput;
try {
response = await client.send(command);
} catch (error) {
throw error;
}
return response;
}
}
Loading

0 comments on commit 232aadd

Please sign in to comment.