Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/core/migrator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as AWS from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import nodePlop from 'node-plop';
import path from 'path';
Expand All @@ -14,8 +15,9 @@ export interface MigratorOptions {
region?: string;
accessKeyId?: string;
secretAccessKey?: string;
endpointUrl?: string;
dynamodb?: DocumentClient;
endpoint?: string;
dynamodb?: AWS.DynamoDB;
documentClient?: DocumentClient;
tableName?: string;
attributeName?: string;
migrationsPath?: string;
Expand All @@ -33,23 +35,25 @@ export class Migrator extends Umzug implements Generator {
* Migrator factory function, creates an umzug instance with dynamodb storage.
* @param options
* @param options.region - an AWS Region
* @param options.dynamodb - a DynamoDB document client instance
* @param options.endpointUrl - an optional endpoint URL for local DynamoDB instances
* @param options.dynamodb - a DynamoDB instance
* @param options.documentClient - a DynamoDB document client instance
* @param options.endpoint - an optional endpoint URL for local DynamoDB instances
* @param options.tableName - a name of migration table in DynamoDB
* @param options.attributeName - name of the table primaryKey attribute in DynamoDB
*/
constructor(options: MigratorOptions = {}) {
let { dynamodb, tableName, attributeName, migrationsPath } = options;
let { dynamodb, documentClient, tableName, attributeName, migrationsPath } = options;

dynamodb = dynamodb || new DocumentClient(pick(['region', 'accessKeyId', 'secretAccessKey', 'endpointUrl'], options));
dynamodb = dynamodb || new AWS.DynamoDB(pick(['region', 'accessKeyId', 'secretAccessKey', 'endpoint'], options));
documentClient = documentClient || new DocumentClient({ service: dynamodb });
tableName = tableName || 'migrations';
attributeName = attributeName || 'name';
migrationsPath = migrationsPath || 'migrations';

super({
storage: new DynamoDBStorage({ dynamodb, tableName, attributeName }),
storage: new DynamoDBStorage({ dynamodb, documentClient, tableName, attributeName }),
migrations: {
params: [dynamodb, options],
params: [documentClient, options],
path: migrationsPath,
},
logging: logger.log
Expand Down
6 changes: 3 additions & 3 deletions src/core/yargs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface BaseCliOptions {
accessKeyId: string;
secretAccessKey: string;
region: string;
endpointUrl: string;
endpoint: string;
tableName: string;
attributeName: string;
}
Expand All @@ -33,7 +33,7 @@ export function baseOptions(yargs: Argv<BaseCliOptions>) {
describe: 'AWS service region',
type: 'string'
})
.option('endpoint-url', {
.option('endpoint', {
describe: 'The DynamoDB endpoint url to use. The DynamoDB local instance url could be specified here.',
type: 'string'
})
Expand All @@ -52,7 +52,7 @@ export function baseOptions(yargs: Argv<BaseCliOptions>) {
export function baseHandler<T extends BaseCliOptions>(callback: (args: Arguments<T>, migrator: Migrator) => void) {
return (args: Arguments<T>): void => {
const migrator = new Migrator({
...pick(['region', 'accessKeyId', 'secretAccessKey', 'endpointUrl'], args),
...pick(['region', 'accessKeyId', 'secretAccessKey', 'endpoint'], args),
tableName: args.tableName,
attributeName: args.attributeName,
migrationsPath: args.migrationsPath,
Expand Down
66 changes: 55 additions & 11 deletions src/storages/dynamodb.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as AWS from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { sort } from 'ramda';
import { Storage } from 'umzug';

const RESOURCE_NOT_FOUND_EXCEPTION = 'ResourceNotFoundException';
interface DynamoDBStorageOptions {
dynamodb?: DocumentClient;
dynamodb?: AWS.DynamoDB;
documentClient?: DocumentClient;
tableName?: string;
attributeName?: string;
timestamp?: boolean;
Expand All @@ -13,7 +16,8 @@ interface DynamoDBStorageOptions {
* @class DynamoDBStorage
*/
export default class DynamoDBStorage implements Storage {
private dynamodb: DocumentClient;
private dynamodb: AWS.DynamoDB;
private documentClient: DocumentClient;
private tableName: string;
private attributeName: string;
private timestamp: boolean;
Expand All @@ -22,16 +26,21 @@ export default class DynamoDBStorage implements Storage {
* Constructs DynamoDB table storage.
*
* @param options
* @param options.dynamodb - a DynamoDB document client instance
* @param options.dynamodb - a DynamoDB instance
* @param options.documentClient - a DynamoDB document client instance
* @param options.tableName - name of migration table in DynamoDB
* @param options.attributeName - name of the table primaryKey attribute in DynamoDB
* @param options.timestamp - option to add timestamps to the DynamoDB table
*/
constructor({ dynamodb, tableName, attributeName, timestamp }: DynamoDBStorageOptions = {}) {
if (dynamodb && !(dynamodb instanceof DocumentClient)) {
throw new Error('"dynamodb" must be a DocumentClient instance');
constructor({ dynamodb, documentClient, tableName, attributeName, timestamp }: DynamoDBStorageOptions = {}) {
if (dynamodb && !(dynamodb instanceof AWS.DynamoDB)) {
throw new Error('"dynamodb" must be a AWS.DynamoDB instance');
}
this.dynamodb = dynamodb || new DocumentClient();
if (documentClient && !(documentClient instanceof DocumentClient)) {
throw new Error('"documentClient" must be a DocumentClient instance');
}
this.dynamodb = dynamodb || new AWS.DynamoDB();
this.documentClient = documentClient || new DocumentClient({ service: this.dynamodb });
this.tableName = tableName || 'migrations';
this.attributeName = attributeName || 'name';
this.timestamp = timestamp || false;
Expand All @@ -49,7 +58,7 @@ export default class DynamoDBStorage implements Storage {
item.createdAt = Date.now();
}

await this.dynamodb.put({ TableName: this.tableName, Item: item }).promise();
await this.documentClient.put({ TableName: this.tableName, Item: item }).promise();
}

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

await this.dynamodb.delete({ TableName: this.tableName, Key: key }).promise();
await this.documentClient.delete({ TableName: this.tableName, Key: key }).promise();
}

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

do {
const { Items, LastEvaluatedKey } = await this.dynamodb.scan({
const { Items, LastEvaluatedKey } = await this.documentClient.scan({
TableName: this.tableName,
ExclusiveStartKey: startKey,
}).promise();
})
.promise()
.catch(error => {
if (error.code === RESOURCE_NOT_FOUND_EXCEPTION) {
return this.createMigrationTable()
.then(() => ({
Items: [],
LastEvaluatedKey : null
}));

} else {
throw error;
}
});

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

return sort((a, b) => a.localeCompare(b), executedItems);
}

/**
* Create migration table.
*
* @returns Promise
*/
createMigrationTable() {
const params = {
TableName: 'migrations',
AttributeDefinitions: [
{ AttributeName: 'name', AttributeType: 'S' },
],
KeySchema: [
{ AttributeName: 'name', KeyType: 'HASH' },
],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
}
};
return this.dynamodb.createTable(params).promise();
}
}
9 changes: 7 additions & 2 deletions tests/storages/dynamodb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ describe('DynamoDBStorage tests', () => {
done();
});

it('Should documentClient option throw', async () => {
const constructor = () => new DynamoDBStorage({ dynamodb: new DocumentClient() as never, documentClient: new DocumentClient() });
expect(constructor).toThrowError(/^"dynamodb" must be a AWS.DynamoDB instance$/);
});

it('Should dynamodb option throw', async () => {
const constructor = () => new DynamoDBStorage({ dynamodb: new AWS.DynamoDB() as never });
expect(constructor).toThrowError(/^"dynamodb" must be a DocumentClient instance$/);
const constructor = () => new DynamoDBStorage({ dynamodb: new AWS.DynamoDB(), documentClient: new AWS.DynamoDB() as never });
expect(constructor).toThrowError(/^"documentClient" must be a DocumentClient instance$/);
});

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