diff --git a/example/index.js b/example/index.js index 702209f..3734c00 100644 --- a/example/index.js +++ b/example/index.js @@ -1,100 +1,135 @@ -const ddbGeo = require('dynamodb-geo'); -const AWS = require('aws-sdk'); -const uuid = require('uuid'); +const { + GeoDataManager, + GeoDataManagerConfiguration, + GeoTableUtil, +} = require("dynamodb-geo-v3"); +const { + DynamoDB, + DynamoDBClient, + Endpoint, + waitUntilTableExists, +} = require("@aws-sdk/client-dynamodb"); +const uuid = require("uuid"); -// Set up AWS -AWS.config.update({ +// Use a local DB for the example. +const ddb = new DynamoDB({ + credentials: { accessKeyId: YOUR_AWS_KEY_ID, secretAccessKey: YOUR_AWS_SECRET_ACCESS_KEY, - region: YOUR_AWS_REGION + }, + endpoint: new Endpoint("http://localhost:8000"), + region: YOUR_AWS_REGION, +}); +const ddbClient = new DynamoDBClient({ + credentials: { + accessKeyId: YOUR_AWS_KEY_ID, + secretAccessKey: YOUR_AWS_SECRET_ACCESS_KEY, + }, + endpoint: new Endpoint("http://localhost:8000"), + region: YOUR_AWS_REGION, }); - -// Use a local DB for the example. -const ddb = new AWS.DynamoDB({ endpoint: new AWS.Endpoint('http://localhost:8000') }); // Configuration for a new instance of a GeoDataManager. Each GeoDataManager instance represents a table -const config = new ddbGeo.GeoDataManagerConfiguration(ddb, 'capitals'); +const config = new GeoDataManagerConfiguration(ddb, "capitals"); // Instantiate the table manager -const capitalsManager = new ddbGeo.GeoDataManager(config); +const capitalsManager = new GeoDataManager(config); // Use GeoTableUtil to help construct a CreateTableInput. -const createTableInput = ddbGeo.GeoTableUtil.getCreateTableRequest(config); +const createTableInput = GeoTableUtil.getCreateTableRequest(config); // Tweak the schema as desired createTableInput.ProvisionedThroughput.ReadCapacityUnits = 2; -console.log('Creating table with schema:'); +console.log("Creating table with schema:"); console.dir(createTableInput, { depth: null }); // Create the table -ddb.createTable(createTableInput).promise() - // Wait for it to become ready - .then(function () { return ddb.waitFor('tableExists', { TableName: config.tableName }).promise() }) - // Load sample data in batches - .then(function () { - console.log('Loading sample data from capitals.json'); - const data = require('./capitals.json'); - const putPointInputs = data.map(function (capital) { - return { - RangeKeyValue: { S: uuid.v4() }, // Use this to ensure uniqueness of the hash/range pairs. - GeoPoint: { - latitude: capital.latitude, - longitude: capital.longitude - }, - PutItemInput: { - Item: { - country: { S: capital.country }, - capital: { S: capital.capital } - } - } - } - }); - - const BATCH_SIZE = 25; - const WAIT_BETWEEN_BATCHES_MS = 1000; - var currentBatch = 1; +ddb + .createTable(createTableInput) + // Wait for it to become ready + .then(function () { + return waitUntilTableExists( + { client: ddbClient, maxWaitTime: 30000 }, + { TableName: config.tableName } + ); + }) + // Load sample data in batches + .then(function () { + console.log("Loading sample data from capitals.json"); + const data = require("./capitals.json"); + const putPointInputs = data.map(function (capital) { + return { + RangeKeyValue: { S: uuid.v4() }, // Use this to ensure uniqueness of the hash/range pairs. + GeoPoint: { + latitude: capital.latitude, + longitude: capital.longitude, + }, + PutItemInput: { + Item: { + country: { S: capital.country }, + capital: { S: capital.capital }, + }, + }, + }; + }); - function resumeWriting() { - if (putPointInputs.length === 0) { - return Promise.resolve(); - } - const thisBatch = []; - for (var i = 0, itemToAdd = null; i < BATCH_SIZE && (itemToAdd = putPointInputs.shift()); i++) { - thisBatch.push(itemToAdd); - } - console.log('Writing batch ' + (currentBatch++) + '/' + Math.ceil(data.length / BATCH_SIZE)); - return capitalsManager.batchWritePoints(thisBatch).promise() - .then(function () { - return new Promise(function (resolve) { - setInterval(resolve,WAIT_BETWEEN_BATCHES_MS); - }); - }) - .then(function () { - return resumeWriting() - }); - } + const BATCH_SIZE = 25; + const WAIT_BETWEEN_BATCHES_MS = 1000; + var currentBatch = 1; - return resumeWriting().catch(function (error) { - console.warn(error); - }); - }) - // Perform a radius query - .then(function () { - console.log('Querying by radius, looking 100km from Cambridge, UK.'); - return capitalsManager.queryRadius({ - RadiusInMeter: 100000, - CenterPoint: { - latitude: 52.225730, - longitude: 0.149593 - } + function resumeWriting() { + if (putPointInputs.length === 0) { + return Promise.resolve(); + } + const thisBatch = []; + for ( + var i = 0, itemToAdd = null; + i < BATCH_SIZE && (itemToAdd = putPointInputs.shift()); + i++ + ) { + thisBatch.push(itemToAdd); + } + console.log( + "Writing batch " + + currentBatch++ + + "/" + + Math.ceil(data.length / BATCH_SIZE) + ); + return capitalsManager + .batchWritePoints(thisBatch) + .then(function () { + return new Promise(function (resolve) { + setInterval(resolve, WAIT_BETWEEN_BATCHES_MS); + }); }) - }) - // Print the results, an array of DynamoDB.AttributeMaps - .then(console.log) - // Clean up - .then(function() { return ddb.deleteTable({ TableName: config.tableName }).promise() }) - .catch(console.warn) - .then(function () { - process.exit(0); - }); \ No newline at end of file + .then(function () { + return resumeWriting(); + }); + } + + return resumeWriting().catch(function (error) { + console.warn(error); + }); + }) + // Perform a radius query + .then(function () { + console.log("Querying by radius, looking 100km from Cambridge, UK."); + return capitalsManager.queryRadius({ + RadiusInMeter: 100000, + CenterPoint: { + latitude: 52.22573, + longitude: 0.149593, + }, + }); + }) + // Print the results, an array of DynamoDB.AttributeMaps + .then(console.log) + // Clean up + .then(function () { + return ddb.deleteTable({ TableName: config.tableName }); + }) + .catch(console.warn) + .then(function () { + process.exit(0); + }); diff --git a/example/package.json b/example/package.json index 69fc3e4..dba9507 100644 --- a/example/package.json +++ b/example/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "aws-sdk": "^2.48.0", + "@aws-sdk/client-dynamodb": "^3.34.0", "dynamodb-geo": "*", "uuid": "^3.0.1" } diff --git a/package.json b/package.json index 7fe6ebd..699cd72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "dynamodb-geo", - "version": "0.4.0", + "name": "dynamodb-geo-v3", + "version": "1.0.0", "description": "A javascript port of awslabs/dynamodb-geo, for dynamodb geospatial querying", "scripts": { "prepublish": "tsc -d", @@ -10,8 +10,8 @@ }, "main": "dist/index.js", "types": "dist/index.d.ts", - "repository": "rh389/dynamodb-geo.js", - "author": "Rob Hogan ", + "repository": "renet/dynamodb-geo-v3", + "author": "René Schubert ", "license": "Apache-2.0", "dependencies": { "@types/long": ">=3", diff --git a/src/dynamodb/DynamoDBManager.ts b/src/dynamodb/DynamoDBManager.ts index d714037..2b74e52 100644 --- a/src/dynamodb/DynamoDBManager.ts +++ b/src/dynamodb/DynamoDBManager.ts @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -import { GeoDataManagerConfiguration } from "../GeoDataManagerConfiguration"; import { AttributeValue, Condition, @@ -23,6 +22,8 @@ import { QueryOutput, WriteRequest, } from "@aws-sdk/client-dynamodb"; +import * as Long from "long"; +import { GeoDataManagerConfiguration } from "../GeoDataManagerConfiguration"; import { DeletePointInput, GetPointInput, @@ -32,7 +33,6 @@ import { } from "../types"; import { S2Manager } from "../s2/S2Manager"; import { GeohashRange } from "../model/GeohashRange"; -import * as Long from "long"; export class DynamoDBManager { private config: GeoDataManagerConfiguration; diff --git a/src/s2/S2Util.ts b/src/s2/S2Util.ts index fe91837..c3f36f6 100644 --- a/src/s2/S2Util.ts +++ b/src/s2/S2Util.ts @@ -2,45 +2,68 @@ import { QueryRadiusInput, QueryRectangleInput } from "../types"; import { S2LatLng, S2LatLngRect } from "nodes2ts"; export class S2Util { - public static latLngRectFromQueryRectangleInput(geoQueryRequest: QueryRectangleInput): S2LatLngRect { - const queryRectangleRequest = geoQueryRequest as QueryRectangleInput; + public static latLngRectFromQueryRectangleInput( + geoQueryRequest: QueryRectangleInput + ): S2LatLngRect { + const queryRectangleRequest = geoQueryRequest as QueryRectangleInput; - const minPoint = queryRectangleRequest.MinPoint; - const maxPoint = queryRectangleRequest.MaxPoint; + const minPoint = queryRectangleRequest.MinPoint; + const maxPoint = queryRectangleRequest.MaxPoint; - let latLngRect: S2LatLngRect = null; + let latLngRect: S2LatLngRect = null; - if (minPoint != null && maxPoint != null) { - const minLatLng = S2LatLng.fromDegrees(minPoint.latitude, minPoint.longitude); - const maxLatLng = S2LatLng.fromDegrees(maxPoint.latitude, maxPoint.longitude); + if (minPoint != null && maxPoint != null) { + const minLatLng = S2LatLng.fromDegrees( + minPoint.latitude, + minPoint.longitude + ); + const maxLatLng = S2LatLng.fromDegrees( + maxPoint.latitude, + maxPoint.longitude + ); - latLngRect = S2LatLngRect.fromLatLng(minLatLng, maxLatLng); - } + latLngRect = S2LatLngRect.fromLatLng(minLatLng, maxLatLng); + } - return latLngRect; - } + return latLngRect; + } - public static getBoundingLatLngRectFromQueryRadiusInput(geoQueryRequest: QueryRadiusInput): S2LatLngRect { - const centerPoint = geoQueryRequest.CenterPoint; - const radiusInMeter = geoQueryRequest.RadiusInMeter; + public static getBoundingLatLngRectFromQueryRadiusInput( + geoQueryRequest: QueryRadiusInput + ): S2LatLngRect { + const centerPoint = geoQueryRequest.CenterPoint; + const radiusInMeter = geoQueryRequest.RadiusInMeter; - const centerLatLng = S2LatLng.fromDegrees(centerPoint.latitude, centerPoint.longitude); + const centerLatLng = S2LatLng.fromDegrees( + centerPoint.latitude, + centerPoint.longitude + ); - const latReferenceUnit = centerPoint.latitude > 0.0 ? -1.0 : 1.0; - const latReferenceLatLng = S2LatLng.fromDegrees(centerPoint.latitude + latReferenceUnit, - centerPoint.longitude); - const lngReferenceUnit = centerPoint.longitude > 0.0 ? -1.0 : 1.0; - const lngReferenceLatLng = S2LatLng.fromDegrees(centerPoint.latitude, centerPoint.longitude - + lngReferenceUnit); + const latReferenceUnit = centerPoint.latitude > 0.0 ? -1.0 : 1.0; + const latReferenceLatLng = S2LatLng.fromDegrees( + centerPoint.latitude + latReferenceUnit, + centerPoint.longitude + ); + const lngReferenceUnit = centerPoint.longitude > 0.0 ? -1.0 : 1.0; + const lngReferenceLatLng = S2LatLng.fromDegrees( + centerPoint.latitude, + centerPoint.longitude + lngReferenceUnit + ); - const latForRadius = radiusInMeter / (centerLatLng.getEarthDistance(latReferenceLatLng) as any).toNumber(); - const lngForRadius = radiusInMeter / (centerLatLng.getEarthDistance(lngReferenceLatLng) as any).toNumber(); + const latForRadius = + radiusInMeter / centerLatLng.getEarthDistance(latReferenceLatLng); + const lngForRadius = + radiusInMeter / centerLatLng.getEarthDistance(lngReferenceLatLng); - const minLatLng = S2LatLng.fromDegrees(centerPoint.latitude - latForRadius, - centerPoint.longitude - lngForRadius); - const maxLatLng = S2LatLng.fromDegrees(centerPoint.latitude + latForRadius, - centerPoint.longitude + lngForRadius); + const minLatLng = S2LatLng.fromDegrees( + centerPoint.latitude - latForRadius, + centerPoint.longitude - lngForRadius + ); + const maxLatLng = S2LatLng.fromDegrees( + centerPoint.latitude + latForRadius, + centerPoint.longitude + lngForRadius + ); - return S2LatLngRect.fromLatLng(minLatLng, maxLatLng); - } + return S2LatLngRect.fromLatLng(minLatLng, maxLatLng); + } } diff --git a/test/dynamodb/DynamoDBManager.ts b/test/dynamodb/DynamoDBManager.ts index 528682c..69a9af6 100644 --- a/test/dynamodb/DynamoDBManager.ts +++ b/test/dynamodb/DynamoDBManager.ts @@ -1,75 +1,83 @@ +import { DynamoDB } from "@aws-sdk/client-dynamodb"; import { DynamoDBManager } from "../../src/dynamodb/DynamoDBManager"; import { expect } from "chai"; import { GeoDataManagerConfiguration } from "../../src"; -describe('DynamoDBManager.deletePoint', () => { - it('calls deleteItem with the correct arguments ', () => { +describe("DynamoDBManager.deletePoint", () => { + it("calls deleteItem with the correct arguments ", () => { let called = false; - const config = new GeoDataManagerConfiguration({ - deleteItem: (args: any) => { - called = true; - expect(args).to.deep.equal({ - TableName: 'MyTable', + const config = new GeoDataManagerConfiguration( + { + deleteItem: async (args: any) => { + called = true; + expect(args).to.deep.equal({ + TableName: "MyTable", Key: { - hashKey: { N: '44' }, - rangeKey: { S: '1234' } - } - } - ); - } - }, 'MyTable'); + hashKey: { N: "44" }, + rangeKey: { S: "1234" }, + }, + }); + }, + } as unknown as DynamoDB, + "MyTable" + ); const ddb = new DynamoDBManager(config); ddb.deletePoint({ - RangeKeyValue: { S: '1234' }, + RangeKeyValue: { S: "1234" }, GeoPoint: { longitude: 50, - latitude: 1 - } + latitude: 1, + }, }); expect(called).to.be.true; }); }); -describe('DynamoDBManager.putPoint', () => { - it('calls putItem with the correct arguments ', () => { +describe("DynamoDBManager.putPoint", () => { + it("calls putItem with the correct arguments ", () => { let called = false; - const config = new GeoDataManagerConfiguration({ - putItem: (args: any) => { - called = true; - expect(args).to.deep.equal({ - TableName: 'MyTable', + const config = new GeoDataManagerConfiguration( + { + putItem: (args: any) => { + called = true; + expect(args).to.deep.equal({ + TableName: "MyTable", Item: { - geoJson: { S: "{\"type\":\"Point\",\"coordinates\":[-0.13,51.51]}" }, + geoJson: { S: '{"type":"Point","coordinates":[-0.13,51.51]}' }, geohash: { N: "5221366118452580119" }, hashKey: { N: "52" }, rangeKey: { S: "1234" }, - country: { S: 'UK' }, - capital: { S: 'London' } + country: { S: "UK" }, + capital: { S: "London" }, }, - ConditionExpression: "attribute_not_exists(capital)" - } - ); - } - }, 'MyTable'); + ConditionExpression: "attribute_not_exists(capital)", + }); + }, + } as unknown as DynamoDB, + "MyTable" + ); const ddb: any = new DynamoDBManager(config); ddb.putPoint({ - RangeKeyValue: { S: '1234' }, // Use this to ensure uniqueness of the hash/range pairs. - GeoPoint: { // An object specifying latitutde and longitude as plain numbers. Used to build the geohash, the hashkey and geojson data + RangeKeyValue: { S: "1234" }, // Use this to ensure uniqueness of the hash/range pairs. + GeoPoint: { + // An object specifying latitutde and longitude as plain numbers. Used to build the geohash, the hashkey and geojson data latitude: 51.51, - longitude: -0.13 + longitude: -0.13, }, - PutItemInput: { // Passed through to the underlying DynamoDB.putItem request. TableName is filled in for you. - Item: { // The primary key, geohash and geojson data is filled in for you - country: { S: 'UK' }, // Specify attribute values using { type: value } objects, like the DynamoDB API. - capital: { S: 'London' } + PutItemInput: { + // Passed through to the underlying DynamoDB.putItem request. TableName is filled in for you. + Item: { + // The primary key, geohash and geojson data is filled in for you + country: { S: "UK" }, // Specify attribute values using { type: value } objects, like the DynamoDB API. + capital: { S: "London" }, }, - ConditionExpression: "attribute_not_exists(capital)" - } + ConditionExpression: "attribute_not_exists(capital)", + }, }); expect(called).to.be.true; diff --git a/test/integration/example.ts b/test/integration/example.ts index 7acc822..91a84f4 100644 --- a/test/integration/example.ts +++ b/test/integration/example.ts @@ -1,19 +1,39 @@ import * as ddbGeo from "../../src"; -import * as AWS from "aws-sdk"; +import { + DynamoDB, + DynamoDBClient, + waitUntilTableExists, +} from "@aws-sdk/client-dynamodb"; import { expect } from "chai"; -AWS.config.update({ - accessKeyId: 'dummy', - secretAccessKey: 'dummy', - region: 'eu-west-1' -}); +type Capital = { + country: string; + capital: string; + latitude: number; + longitude: number; +}; -describe('Example', function () { +describe("Example", function () { // Use a local DB for the example. - const ddb = new AWS.DynamoDB({ endpoint: 'http://127.0.0.1:8000' }); + const ddb = new DynamoDB({ + credentials: { + accessKeyId: "dummy", + secretAccessKey: "dummy", + }, + endpoint: "http://127.0.0.1:8000", + region: "eu-west-1", + }); + const ddbClient = new DynamoDBClient({ + credentials: { + accessKeyId: "dummy", + secretAccessKey: "dummy", + }, + endpoint: "http://127.0.0.1:8000", + region: "eu-west-1", + }); // Configuration for a new instance of a GeoDataManager. Each GeoDataManager instance represents a table - const config = new ddbGeo.GeoDataManagerConfiguration(ddb, 'test-capitals'); + const config = new ddbGeo.GeoDataManagerConfiguration(ddb, "test-capitals"); // Instantiate the table manager const capitalsManager = new ddbGeo.GeoDataManager(config); @@ -26,28 +46,29 @@ describe('Example', function () { // Use GeoTableUtil to help construct a CreateTableInput. const createTableInput = ddbGeo.GeoTableUtil.getCreateTableRequest(config); createTableInput.ProvisionedThroughput.ReadCapacityUnits = 2; - await ddb.createTable(createTableInput).promise(); + await ddb.createTable(createTableInput); // Wait for it to become ready - await ddb.waitFor('tableExists', { TableName: config.tableName }).promise() + await waitUntilTableExists( + { client: ddbClient, maxWaitTime: 30000 }, + { TableName: config.tableName } + ); // Load sample data in batches - console.log('Loading sample data from capitals.json'); - const data = require('../../example/capitals.json'); - const putPointInputs = data.map(function (capital, i) { - return { - RangeKeyValue: { S: String(i) }, // Use this to ensure uniqueness of the hash/range pairs. - GeoPoint: { - latitude: capital.latitude, - longitude: capital.longitude + console.log("Loading sample data from capitals.json"); + const data: Capital[] = require("../../example/capitals.json"); + const putPointInputs = data.map((capital, i) => ({ + RangeKeyValue: { S: String(i) }, // Use this to ensure uniqueness of the hash/range pairs. + GeoPoint: { + latitude: capital.latitude, + longitude: capital.longitude, + }, + PutItemInput: { + Item: { + country: { S: capital.country }, + capital: { S: capital.capital }, }, - PutItemInput: { - Item: { - country: { S: capital.country }, - capital: { S: capital.capital } - } - } - } - }); + }, + })); const BATCH_SIZE = 25; const WAIT_BETWEEN_BATCHES_MS = 1000; @@ -55,45 +76,58 @@ describe('Example', function () { async function resumeWriting() { if (putPointInputs.length === 0) { - console.log('Finished loading'); + console.log("Finished loading"); return; } const thisBatch = []; - for (var i = 0, itemToAdd = null; i < BATCH_SIZE && (itemToAdd = putPointInputs.shift()); i++) { + for ( + var i = 0, itemToAdd = null; + i < BATCH_SIZE && (itemToAdd = putPointInputs.shift()); + i++ + ) { thisBatch.push(itemToAdd); } - console.log('Writing batch ' + (currentBatch++) + '/' + Math.ceil(data.length / BATCH_SIZE)); - await capitalsManager.batchWritePoints(thisBatch).promise(); + console.log( + "Writing batch " + + currentBatch++ + + "/" + + Math.ceil(data.length / BATCH_SIZE) + ); + await capitalsManager.batchWritePoints(thisBatch); // Sleep - await new Promise((resolve) => setInterval(resolve, WAIT_BETWEEN_BATCHES_MS)); + await new Promise((resolve) => + setInterval(resolve, WAIT_BETWEEN_BATCHES_MS) + ); return resumeWriting(); } return resumeWriting(); }); - it('queryRadius', async function () { + it("queryRadius", async function () { this.timeout(20000); // Perform a radius query const result = await capitalsManager.queryRadius({ RadiusInMeter: 100000, CenterPoint: { - latitude: 52.225730, - longitude: 0.149593 - } + latitude: 52.22573, + longitude: 0.149593, + }, }); - expect(result).to.deep.equal([{ - rangeKey: { S: '50' }, - country: { S: 'United Kingdom' }, - capital: { S: 'London' }, - hashKey: { N: '522' }, - geoJson: { S: '{"type":"Point","coordinates":[-0.13,51.51]}' }, - geohash: { N: '5221366118452580119' } - }]); + expect(result).to.deep.equal([ + { + rangeKey: { S: "50" }, + country: { S: "United Kingdom" }, + capital: { S: "London" }, + hashKey: { N: "522" }, + geoJson: { S: '{"type":"Point","coordinates":[-0.13,51.51]}' }, + geohash: { N: "5221366118452580119" }, + }, + ]); }); after(async function () { this.timeout(10000); - await ddb.deleteTable({ TableName: config.tableName }).promise() + await ddb.deleteTable({ TableName: config.tableName }); }); });