Skip to content

Commit

Permalink
feat(geo): batch deleteGeofences call
Browse files Browse the repository at this point in the history
  • Loading branch information
Tré Ammatuna committed Dec 6, 2021
1 parent 2554a83 commit cacab94
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 0 additions & 5 deletions packages/geo/__tests__/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};
}
Expand Down
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
131 changes: 47 additions & 84 deletions packages/geo/src/Providers/AmazonLocationServiceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
}

0 comments on commit cacab94

Please sign in to comment.