Skip to content

Commit 2129998

Browse files
committed
fix: create migrations table when missing and update document client params signature
1 parent eaad1e3 commit 2129998

File tree

4 files changed

+77
-24
lines changed

4 files changed

+77
-24
lines changed

src/core/migrator.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as AWS from 'aws-sdk';
12
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
23
import nodePlop from 'node-plop';
34
import path from 'path';
@@ -14,8 +15,9 @@ export interface MigratorOptions {
1415
region?: string;
1516
accessKeyId?: string;
1617
secretAccessKey?: string;
17-
endpointUrl?: string;
18-
dynamodb?: DocumentClient;
18+
endpoint?: string;
19+
dynamodb?: AWS.DynamoDB;
20+
documentClient?: DocumentClient;
1921
tableName?: string;
2022
attributeName?: string;
2123
migrationsPath?: string;
@@ -33,23 +35,25 @@ export class Migrator extends Umzug implements Generator {
3335
* Migrator factory function, creates an umzug instance with dynamodb storage.
3436
* @param options
3537
* @param options.region - an AWS Region
36-
* @param options.dynamodb - a DynamoDB document client instance
37-
* @param options.endpointUrl - an optional endpoint URL for local DynamoDB instances
38+
* @param options.dynamodb - a DynamoDB instance
39+
* @param options.documentClient - a DynamoDB document client instance
40+
* @param options.endpoint - an optional endpoint URL for local DynamoDB instances
3841
* @param options.tableName - a name of migration table in DynamoDB
3942
* @param options.attributeName - name of the table primaryKey attribute in DynamoDB
4043
*/
4144
constructor(options: MigratorOptions = {}) {
42-
let { dynamodb, tableName, attributeName, migrationsPath } = options;
45+
let { dynamodb, documentClient, tableName, attributeName, migrationsPath } = options;
4346

44-
dynamodb = dynamodb || new DocumentClient(pick(['region', 'accessKeyId', 'secretAccessKey', 'endpointUrl'], options));
47+
dynamodb = dynamodb || new AWS.DynamoDB(pick(['region', 'accessKeyId', 'secretAccessKey', 'endpoint'], options));
48+
documentClient = documentClient || new DocumentClient({ service: dynamodb });
4549
tableName = tableName || 'migrations';
4650
attributeName = attributeName || 'name';
4751
migrationsPath = migrationsPath || 'migrations';
4852

4953
super({
50-
storage: new DynamoDBStorage({ dynamodb, tableName, attributeName }),
54+
storage: new DynamoDBStorage({ dynamodb, documentClient, tableName, attributeName }),
5155
migrations: {
52-
params: [dynamodb, options],
56+
params: [documentClient, options],
5357
path: migrationsPath,
5458
},
5559
logging: logger.log

src/core/yargs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface BaseCliOptions {
99
accessKeyId: string;
1010
secretAccessKey: string;
1111
region: string;
12-
endpointUrl: string;
12+
endpoint: string;
1313
tableName: string;
1414
attributeName: string;
1515
}
@@ -33,7 +33,7 @@ export function baseOptions(yargs: Argv<BaseCliOptions>) {
3333
describe: 'AWS service region',
3434
type: 'string'
3535
})
36-
.option('endpoint-url', {
36+
.option('endpoint', {
3737
describe: 'The DynamoDB endpoint url to use. The DynamoDB local instance url could be specified here.',
3838
type: 'string'
3939
})
@@ -52,7 +52,7 @@ export function baseOptions(yargs: Argv<BaseCliOptions>) {
5252
export function baseHandler<T extends BaseCliOptions>(callback: (args: Arguments<T>, migrator: Migrator) => void) {
5353
return (args: Arguments<T>): void => {
5454
const migrator = new Migrator({
55-
...pick(['region', 'accessKeyId', 'secretAccessKey', 'endpointUrl'], args),
55+
...pick(['region', 'accessKeyId', 'secretAccessKey', 'endpoint'], args),
5656
tableName: args.tableName,
5757
attributeName: args.attributeName,
5858
migrationsPath: args.migrationsPath,

src/storages/dynamodb.ts

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import * as AWS from 'aws-sdk';
12
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
23
import { sort } from 'ramda';
34
import { Storage } from 'umzug';
45

6+
const RESOURCE_NOT_FOUND_EXCEPTION = 'ResourceNotFoundException';
57
interface DynamoDBStorageOptions {
6-
dynamodb?: DocumentClient;
8+
dynamodb?: AWS.DynamoDB;
9+
documentClient?: DocumentClient;
710
tableName?: string;
811
attributeName?: string;
912
timestamp?: boolean;
@@ -13,7 +16,8 @@ interface DynamoDBStorageOptions {
1316
* @class DynamoDBStorage
1417
*/
1518
export default class DynamoDBStorage implements Storage {
16-
private dynamodb: DocumentClient;
19+
private dynamodb: AWS.DynamoDB;
20+
private documentClient: DocumentClient;
1721
private tableName: string;
1822
private attributeName: string;
1923
private timestamp: boolean;
@@ -22,16 +26,21 @@ export default class DynamoDBStorage implements Storage {
2226
* Constructs DynamoDB table storage.
2327
*
2428
* @param options
25-
* @param options.dynamodb - a DynamoDB document client instance
29+
* @param options.dynamodb - a DynamoDB instance
30+
* @param options.documentClient - a DynamoDB document client instance
2631
* @param options.tableName - name of migration table in DynamoDB
2732
* @param options.attributeName - name of the table primaryKey attribute in DynamoDB
2833
* @param options.timestamp - option to add timestamps to the DynamoDB table
2934
*/
30-
constructor({ dynamodb, tableName, attributeName, timestamp }: DynamoDBStorageOptions = {}) {
31-
if (dynamodb && !(dynamodb instanceof DocumentClient)) {
32-
throw new Error('"dynamodb" must be a DocumentClient instance');
35+
constructor({ dynamodb, documentClient, tableName, attributeName, timestamp }: DynamoDBStorageOptions = {}) {
36+
if (dynamodb && !(dynamodb instanceof AWS.DynamoDB)) {
37+
throw new Error('"dynamodb" must be a AWS.DynamoDB instance');
3338
}
34-
this.dynamodb = dynamodb || new DocumentClient();
39+
if (documentClient && !(documentClient instanceof DocumentClient)) {
40+
throw new Error('"documentClient" must be a DocumentClient instance');
41+
}
42+
this.dynamodb = dynamodb || new AWS.DynamoDB();
43+
this.documentClient = documentClient || new DocumentClient({ service: this.dynamodb });
3544
this.tableName = tableName || 'migrations';
3645
this.attributeName = attributeName || 'name';
3746
this.timestamp = timestamp || false;
@@ -49,7 +58,7 @@ export default class DynamoDBStorage implements Storage {
4958
item.createdAt = Date.now();
5059
}
5160

52-
await this.dynamodb.put({ TableName: this.tableName, Item: item }).promise();
61+
await this.documentClient.put({ TableName: this.tableName, Item: item }).promise();
5362
}
5463

5564
/**
@@ -60,7 +69,7 @@ export default class DynamoDBStorage implements Storage {
6069
async unlogMigration(migrationName: string) {
6170
const key: DocumentClient.Key = { [this.attributeName]: migrationName };
6271

63-
await this.dynamodb.delete({ TableName: this.tableName, Key: key }).promise();
72+
await this.documentClient.delete({ TableName: this.tableName, Key: key }).promise();
6473
}
6574

6675
/**
@@ -71,10 +80,23 @@ export default class DynamoDBStorage implements Storage {
7180
let startKey: DocumentClient.Key;
7281

7382
do {
74-
const { Items, LastEvaluatedKey } = await this.dynamodb.scan({
83+
const { Items, LastEvaluatedKey } = await this.documentClient.scan({
7584
TableName: this.tableName,
7685
ExclusiveStartKey: startKey,
77-
}).promise();
86+
})
87+
.promise()
88+
.catch(error => {
89+
if (error === RESOURCE_NOT_FOUND_EXCEPTION) {
90+
return this.createMigrationTable()
91+
.then(() => ({
92+
LastEvaluatedKey: [],
93+
Items: null
94+
}));
95+
96+
} else {
97+
throw error;
98+
}
99+
});
78100

79101
for (const item of Items) {
80102
executedItems.push(item[this.attributeName]);
@@ -85,4 +107,26 @@ export default class DynamoDBStorage implements Storage {
85107

86108
return sort((a, b) => a.localeCompare(b), executedItems);
87109
}
110+
111+
/**
112+
* Create migration table.
113+
*
114+
* @returns Promise
115+
*/
116+
createMigrationTable() {
117+
const params = {
118+
TableName: 'migrations',
119+
AttributeDefinitions: [
120+
{ AttributeName: 'name', AttributeType: 'S' },
121+
],
122+
KeySchema: [
123+
{ AttributeName: 'name', KeyType: 'HASH' },
124+
],
125+
ProvisionedThroughput: {
126+
ReadCapacityUnits: 1,
127+
WriteCapacityUnits: 1,
128+
}
129+
};
130+
return this.dynamodb.createTable(params).promise();
131+
}
88132
}

tests/storages/dynamodb.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ describe('DynamoDBStorage tests', () => {
1010
done();
1111
});
1212

13+
it('Should documentClient option throw', async () => {
14+
const constructor = () => new DynamoDBStorage({ dynamodb: new DocumentClient() as never, documentClient: new DocumentClient() });
15+
expect(constructor).toThrowError(/^"dynamodb" must be a AWS.DynamoDB instance$/);
16+
});
17+
1318
it('Should dynamodb option throw', async () => {
14-
const constructor = () => new DynamoDBStorage({ dynamodb: new AWS.DynamoDB() as never });
15-
expect(constructor).toThrowError(/^"dynamodb" must be a DocumentClient instance$/);
19+
const constructor = () => new DynamoDBStorage({ dynamodb: new AWS.DynamoDB(), documentClient: new AWS.DynamoDB() as never });
20+
expect(constructor).toThrowError(/^"documentClient" must be a DocumentClient instance$/);
1621
});
1722

1823
it('Should get migrations', async () => {

0 commit comments

Comments
 (0)