Skip to content

Commit

Permalink
refactor!: migrate to aws-sdk 3 (#447)
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl authored Nov 18, 2024
1 parent ac4445c commit 18fbc66
Show file tree
Hide file tree
Showing 16 changed files with 1,616 additions and 558 deletions.
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,41 @@
```bash
npm install -g dynamodb-admin

# For Windows:
set DYNAMO_ENDPOINT=http://localhost:8000
dynamodb-admin

# For Mac/Linux:
DYNAMO_ENDPOINT=http://localhost:8000 dynamodb-admin
dynamodb-admin --dynamo-endpoint=http://localhost:8000
```

Options:
- --open / -o - opens server URL in a default browser on start
- --port PORT / -p PORT - Port to run on (default: 8001)
- --host HOST / -h HOST - Host to run on (default: localhost)
- `--open` / `-o` - opens server URL in a default browser on start
- `--port PORT` / `-p PORT` - Port to run on (default: 8001)
- `--host HOST` / `-h HOST` - Host to run on (default: localhost)
- `--dynamo-endpoint` - DynamoDB endpoint to connect to (default: http://localhost:8000).
- `--skip-default-credentials` - Skip setting default credentials and region. By default the accessKeyId/secretAccessKey are set to "key" and "secret" and the region is set to "us-east-1". If you specify this argument then you need to ensure that credentials are provided some other way. See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html for more details on how default credentials provider works.

You can specify host & port to run on by setting environment variables `HOST` and `PORT` respectively. This will override value specified on the command line. This is legacy way to specify the HOST & PORT.
Environment variables `HOST`, `PORT` and `DYNAMO_ENDPOINT` can also be used to set the respective options. Those are not recommended.

If you use a local dynamodb that cares about credentials, you can configure them by using the following environment variables `AWS_REGION` `AWS_ACCESS_KEY_ID` `AWS_SECRET_ACCESS_KEY`
If you use a local dynamodb that cares about credentials, you can configure them by using the following environment variables `AWS_REGION` `AWS_ACCESS_KEY_ID` `AWS_SECRET_ACCESS_KEY` or specify the `--skip-default-credentials` argument and rely on the default AWS SDK credentials resolving behavior.

For example with the `amazon/dynamodb-local` docker image you can launch `dynamodb-admin` with:

```bash
AWS_REGION=eu-west-1 AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local dynamodb-admin
```

If you are accessing your database from another piece of software, the `AWS_ACCESS_KEY_ID` used by that application must match the `AWS_ACCESS_KEY_ID` you used with `dynamodb-admin` if you want both to see the same data.

By default `dynamodb-admin` sets a default key/secret to values "key" and "secret" and the region to "us-east-1".

### Use as a library in your project

This requires AWS SDK v3.
If you depend on AWS SDK v2 then you need to use dynamodb-admin v4.

```js
const AWS = require('aws-sdk');
const {createServer} = require('dynamodb-admin');
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { createServer } from 'dynamodb-admin';

const dynamodb = new AWS.DynamoDB();
const dynClient = new AWS.DynamoDB.DocumentClient({service: dynamodb});
const dynamoDbClient = new DynamoDBClient();

const app = createServer(dynamodb, dynClient);
const app = createServer({ dynamoDbClient });

const host = 'localhost';
const port = 8001;
Expand All @@ -59,7 +59,7 @@ server.on('listening', () => {

Run `npm run build` and then `DYNAMO_ENDPOINT=http://localhost:8000 npm run start` to start dynamodb-admin.

You can set up a build watcher in a separate terminal using `npm run build:watch` which will re-complile the code on change and restart the dynamodb-admin instance.
You can set up a build watcher in a separate terminal using `npm run build:watch` which will re-compile the code on change and cause the dynamodb-admin instance to restart.

## See also

Expand Down
27 changes: 19 additions & 8 deletions bin/dynamodb-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,27 @@ parser.add_argument('-p', '--port', {
help: 'Port to run on (default: 8001)',
});

const args = parser.parse_args();
parser.add_argument('--dynamo-endpoint', {
type: 'str',
default: 'http://localhost:8000',
help: 'DynamoDB endpoint to connect to.',
});

parser.add_argument('--skip-default-credentials', {
action: 'store_true',
help: 'Skip setting default credentials and region. By default the accessKeyId/secretAccessKey are set to "key" and "secret" and the region is set to "us-east-1". If you specify this argument then you need to ensure that credentials are provided some other way. See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html for more details on how default credentials provider works.',
});

const { host, port, open: openUrl, dynamo_endpoint: dynamoEndpoint, skip_default_credentials: skipDefaultCredentials } = parser.parse_args();

const app = createServer();
const host = process.env.HOST || args.host;
const port = process.env.PORT || args.port;
const server = app.listen(port, host);
const app = createServer({ dynamoEndpoint, skipDefaultCredentials });
const resolvedHost = process.env.HOST || host;
const resolvedPort = process.env.PORT || port;
const server = app.listen(resolvedPort, resolvedHost);
server.on('listening', () => {
const address = server.address();
if (!address) {
throw new Error(`Not able to listen on host and port "${host}:${port}"`);
throw new Error(`Not able to listen on host and port "${resolvedHost}:${resolvedPort}"`);
}
let listenAddress;
let listenPort;
Expand All @@ -57,12 +68,12 @@ server.on('listening', () => {
listenPort = address.port;
}
let url = `http://${listenAddress}${listenPort ? ':' + listenPort : ''}`;
if (!host && listenAddress !== '0.0.0.0') {
if (!resolvedHost && listenAddress !== '0.0.0.0') {
url += ` (alternatively http://0.0.0.0:${listenPort})`;
}
console.info(` dynamodb-admin listening on ${url}`);

if (args.open) {
if (openUrl) {
open(url);
}
});
Expand Down
10 changes: 5 additions & 5 deletions lib/actions/getPage.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { DynamoDB } from 'aws-sdk';
import type { KeySchemaElement } from '@aws-sdk/client-dynamodb';
import { extractKey, doSearch, type ScanParams } from '../util';
import type { DynamoDbApi } from '../dynamoDbApi';
import type { ItemList, Key } from '../types';

export async function getPage(
ddbApi: DynamoDbApi,
keySchema: DynamoDB.KeySchema,
keySchema: KeySchemaElement[],
TableName: string,
scanParams: ScanParams,
pageSize: number,
startKey: DynamoDB.Key,
operationType: 'query' | 'scan',
): Promise<{ pageItems: Record<string, any>[]; nextKey: any }> {
const pageItems: Record<string, any>[] = [];

function onNewItems(items: DynamoDB.ItemList | undefined, lastStartKey: DynamoDB.Key | undefined): boolean {
function onNewItems(items: ItemList | undefined, lastStartKey: Key | undefined): boolean {
if (items) {
for (let i = 0; i < items.length && pageItems.length < pageSize + 1; i++) {
pageItems.push(items[i]);
Expand All @@ -26,7 +26,7 @@ export async function getPage(
return pageItems.length > pageSize || !lastStartKey;
}

let items = await doSearch(ddbApi, TableName, scanParams, 10, startKey, onNewItems, operationType);
let items = await doSearch(ddbApi, TableName, scanParams, 10, onNewItems, operationType);
let nextKey = null;

if (items.length > pageSize) {
Expand Down
6 changes: 3 additions & 3 deletions lib/actions/listAllTables.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { DynamoDB } from 'aws-sdk';
import type { TableDescription } from '@aws-sdk/client-dynamodb';
import type { DynamoDbApi } from '../dynamoDbApi';

export async function listAllTables(ddbApi: DynamoDbApi): Promise<DynamoDB.TableDescription[]> {
const allTableNames: DynamoDB.TableNameList = [];
export async function listAllTables(ddbApi: DynamoDbApi): Promise<TableDescription[]> {
const allTableNames: string[] = [];
let lastEvaluatedTableName: string | undefined = undefined;

do {
Expand Down
32 changes: 16 additions & 16 deletions lib/actions/purgeTable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { DynamoDB } from 'aws-sdk';
import type { BatchWriteItemOutput, KeySchemaElement } from '@aws-sdk/client-dynamodb';
import type { BatchWriteCommandInput, BatchWriteCommandOutput } from '@aws-sdk/lib-dynamodb';
import { doSearch, type ScanParams } from '../util';
import type { DynamoDbApi } from '../dynamoDbApi';
import type { ItemList } from '../types';

/**
* This function deletes all record from a given table within dynamodb.
Expand All @@ -27,12 +29,12 @@ async function findPrimaryKeys(tableName: string, ddbApi: DynamoDbApi): Promise<

return ['HASH', 'RANGE']
.map(keyType => tableDescription.KeySchema!.find(element => element.KeyType === keyType))
.filter<DynamoDB.KeySchemaElement>(attribute => attribute !== undefined)
.map(attribute => attribute.AttributeName);
.filter<KeySchemaElement>(attribute => attribute !== undefined)
.map(attribute => attribute.AttributeName as string);
}

async function findAllElements(tableName: string, primaryKeys: string[], ddbApi: DynamoDbApi): Promise<DynamoDB.DocumentClient.ItemList> {
const ExpressionAttributeNames: DynamoDB.ExpressionAttributeNameMap = {};
async function findAllElements(tableName: string, primaryKeys: string[], ddbApi: DynamoDbApi): Promise<ItemList> {
const ExpressionAttributeNames: ScanParams['ExpressionAttributeNames'] = {};

for (const [index, key] of primaryKeys.entries()) {
ExpressionAttributeNames[`#KEY${index}`] = key;
Expand All @@ -46,18 +48,16 @@ async function findAllElements(tableName: string, primaryKeys: string[], ddbApi:
return await doSearch(ddbApi, tableName, scanParams);
}

async function deleteAllElements(tableName: string, items: DynamoDB.Key[], ddbApi: DynamoDbApi): Promise<DynamoDB.BatchWriteItemOutput[]> {
const deleteRequests: Promise<DynamoDB.BatchWriteItemOutput>[] = [];
async function deleteAllElements(tableName: string, items: ItemList, ddbApi: DynamoDbApi): Promise<BatchWriteItemOutput[]> {
const deleteRequests: Promise<BatchWriteCommandOutput>[] = [];
let counter = 0;
const MAX_OPERATIONS = 25;
const params: DynamoDB.Types.BatchWriteItemInput = {
RequestItems: {
[tableName]: [],
},
const requestItems: BatchWriteCommandInput['RequestItems'] = {
[tableName]: [],
};

for (const item of items) {
params.RequestItems[tableName].push({
requestItems[tableName].push({
DeleteRequest: {
Key: item,
},
Expand All @@ -66,14 +66,14 @@ async function deleteAllElements(tableName: string, items: DynamoDB.Key[], ddbAp
counter++;

if (counter % MAX_OPERATIONS === 0) {
deleteRequests.push(ddbApi.batchWriteItem(params));
params.RequestItems[tableName] = [];
deleteRequests.push(ddbApi.batchWriteItem({ RequestItems: requestItems }));
requestItems[tableName] = [];
}
}

if (counter % MAX_OPERATIONS !== 0) {
deleteRequests.push(ddbApi.batchWriteItem(params));
params.RequestItems[tableName] = [];
deleteRequests.push(ddbApi.batchWriteItem({ RequestItems: requestItems }));
requestItems[tableName] = [];
}

return await Promise.all(deleteRequests);
Expand Down
47 changes: 14 additions & 33 deletions lib/backend.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,31 @@
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import express, { type Express } from 'express';
import AWSSDK, { DynamoDB } from 'aws-sdk';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { createAwsConfig } from './config';
import { setupRoutes } from './routes';
import { createDynamoDbApi } from './dynamoDbApi';

function getHomeDir(): string | null {
const env = process.env;
const home = env.HOME || env.USERPROFILE
|| (env.HOMEPATH ? (env.HOMEDRIVE || 'C:/') + env.HOMEPATH : null);
export type CreateServerOptions = {
dynamoDbClient?: DynamoDBClient;
expressInstance?: Express;
dynamoEndpoint?: string;
skipDefaultCredentials?: boolean;
};

if (home) {
return home;
}

if (typeof os.homedir === 'function') {
return os.homedir();
}

return null;
}
export function createServer(options?: CreateServerOptions): Express {
const { dynamoDbClient, expressInstance, dynamoEndpoint, skipDefaultCredentials } = options || {};
const app = expressInstance || express();
let dynamoClient = dynamoDbClient;

export function createServer(dynamodb?: DynamoDB, docClient?: DynamoDB.DocumentClient, expressInstance = express()): Express {
const app = expressInstance;
app.set('json spaces', 2);
app.set('view engine', 'ejs');
app.set('views', path.resolve(__dirname, '..', 'views'));

if (!dynamodb || !docClient) {
const homeDir = getHomeDir();

if (homeDir && fs.existsSync(path.join(homeDir, '.aws', 'credentials')) &&
fs.existsSync(path.join(homeDir, '.aws', 'config'))) {
process.env.AWS_SDK_LOAD_CONFIG = '1';
}

if (!dynamodb) {
dynamodb = new DynamoDB(createAwsConfig(AWSSDK));
}

docClient = docClient || new DynamoDB.DocumentClient({ service: dynamodb });
if (!dynamoClient) {
dynamoClient = new DynamoDBClient(createAwsConfig({ dynamoEndpoint, skipDefaultCredentials }));
}

const ddbApi = createDynamoDbApi(dynamodb, docClient);
const ddbApi = createDynamoDbApi(dynamoClient);

setupRoutes(app, ddbApi);

Expand Down
Loading

0 comments on commit 18fbc66

Please sign in to comment.