Skip to content
Draft
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
128 changes: 109 additions & 19 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-env es6 */
var AWS = require('aws-sdk');
var DynamoDBSet = require('aws-sdk/lib/dynamodb/set');
var { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
var { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb');
var _ = require('underscore');
const { ListTablesCommand, DescribeTableCommand } = require('@aws-sdk/client-dynamodb');
const util = require('./lib/util');
const { promisify } = require('util');

Expand Down Expand Up @@ -88,24 +89,62 @@ function Dyno(options) {
if (!options.region) throw new Error('region is required');

config = {
region: options.region,
endpoint: options.endpoint,
region: options.region === 'local' ? 'us-east-1' : options.region,
params: { TableName: options.table }, // Sets `TableName` in every request
httpOptions: options.httpOptions || { timeout: 5000 }, // Default appears to be 2 min
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
sessionToken: options.sessionToken,
requestHandler: options.httpOptions ? {
requestTimeout: options.httpOptions.timeout || 5000
} : { requestTimeout: 5000 },
logger: options.logger,
maxRetries: options.maxRetries
maxAttempts: options.maxRetries ? options.maxRetries + 1 : undefined
};

client = new AWS.DynamoDB(config);
docClient = new AWS.DynamoDB.DocumentClient({ service: client });
tableFreeClient = new AWS.DynamoDB(_(config).omit('params')); // no TableName in batch requests
tableFreeDocClient = new AWS.DynamoDB.DocumentClient({ service: tableFreeClient });

if (options.accessKeyId) {
config.credentials = {
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
sessionToken: options.sessionToken
};
} else if (options.region === 'local') {
config.credentials = {
accessKeyId: 'fake',
secretAccessKey: 'fake'
};
}

if (options.region === 'local') {
config.endpoint = options.endpoint || 'http://localhost:4567';
config.disableHostPrefix = true;
} else if (options.endpoint) {
config.endpoint = options.endpoint;
}

client = new DynamoDBClient(config);
docClient = DynamoDBDocumentClient.from(client, {
marshallOptions: {
convertEmptyValues: false,
removeUndefinedValues: false,
convertClassInstanceToMap: false
},
unmarshallOptions: {
wrapNumbers: false
}
});

const tableFreeConfig = _(config).omit('params');
tableFreeClient = new DynamoDBClient(tableFreeConfig);
tableFreeDocClient = DynamoDBDocumentClient.from(tableFreeClient, {
marshallOptions: {
convertEmptyValues: false,
removeUndefinedValues: false,
convertClassInstanceToMap: false
},
unmarshallOptions: {
wrapNumbers: false
}
});

}

const wrappedDocClient = util.wrapDocClient(docClient, options.costLogger);
const wrappedTableFreeDocClient = util.wrapDocClient(tableFreeDocClient, options.costLogger);

Expand All @@ -120,7 +159,13 @@ function Dyno(options) {
* @param {function} [callback] - a function to handle the response. See [DynamoDB.listTables](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#listTables-property) for details.
* @returns {Request}
*/
listTables: client.listTables.bind(client),
listTables: function(params, callback) {
const command = new ListTablesCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
/**
* Get table information. Passthrough to [DynamoDB.describeTable](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#describTable-property).
*
Expand All @@ -130,7 +175,13 @@ function Dyno(options) {
* @param {function} [callback] - a function to handle the response. See [DynamoDB.describeTable](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#describeTable-property) for details.
* @returns {Request}
*/
describeTable: client.describeTable.bind(client),
describeTable: function(params, callback) {
const command = new DescribeTableCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
/**
* Perform a batch of get operations. Passthrough to [DocumentClient.batchGet](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#batchGet-property).
*
Expand Down Expand Up @@ -392,7 +443,7 @@ function Dyno(options) {
delete dynoExtensions.queryStream;
delete dynoExtensions.scanStream;
}

for (const name of Object.keys(nativeFunctions)) {
nativeFunctions[`${name}Async`] = promisify(nativeFunctions[name]);
}
Expand Down Expand Up @@ -456,7 +507,46 @@ Dyno.multi = function(readOptions, writeOptions) {
* };
*/
Dyno.createSet = function(list) {
return new DynamoDBSet(list);
// Create a compatible DynamoDBSet-like object for AWS SDK v3
if (!Array.isArray(list)) {
throw new Error('DynamoDB set must be created from an array');
}

if (list.length === 0) {
throw new Error('DynamoDB set cannot be empty');
}

// Determine the type based on the first element
let type;
const firstElement = list[0];

if (typeof firstElement === 'string') {
type = 'String';
} else if (typeof firstElement === 'number') {
type = 'Number';
} else if (Buffer.isBuffer(firstElement)) {
type = 'Binary';
} else {
throw new Error('DynamoDB set must contain strings, numbers, or buffers');
}

// Validate all elements are the same type
for (let i = 1; i < list.length; i++) {
const element = list[i];
if (type === 'String' && typeof element !== 'string') {
throw new Error('All elements in a DynamoDB set must be the same type');
} else if (type === 'Number' && typeof element !== 'number') {
throw new Error('All elements in a DynamoDB set must be the same type');
} else if (type === 'Binary' && !Buffer.isBuffer(element)) {
throw new Error('All elements in a DynamoDB set must be the same type');
}
}

return {
wrapperName: 'Set',
type: type,
values: list
};
};

/**
Expand Down
11 changes: 6 additions & 5 deletions lib/requests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var big = require('big.js');
var converter = require('aws-sdk/lib/dynamodb/converter');
var { marshall } = require('@aws-sdk/util-dynamodb');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var queue = require('queue-async');
var reduceCapacity = require('./util').reduceCapacity;

Expand Down Expand Up @@ -334,10 +334,11 @@ module.exports = function(client) {
};

function itemSize(item, skipAttr) {
item = Object.keys(item).reduce(function(obj, key) {
obj[key] = converter.input(item[key]);
return obj;
}, {});
item = marshall(item, {
convertEmptyValues: false,
removeUndefinedValues: false,
convertClassInstanceToMap: false
});

var size = 0;
var attr;
Expand Down
19 changes: 9 additions & 10 deletions lib/serialization.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var _ = require('underscore');
var converter = require('aws-sdk/lib/dynamodb/converter');
var { marshall, unmarshall } = require('@aws-sdk/util-dynamodb');

module.exports.serialize = function(item) {
function replacer(key, value) {
Expand All @@ -21,10 +21,11 @@ module.exports.serialize = function(item) {
return value;
}

return JSON.stringify(Object.keys(item).reduce(function(obj, key) {
obj[key] = converter.input(item[key]);
return obj;
}, {}), replacer);
return JSON.stringify(marshall(item, {
convertEmptyValues: false,
removeUndefinedValues: false,
convertClassInstanceToMap: false
}), replacer);
};
module.exports.deserialize = function(str) {
function reviver(key, value) {
Expand All @@ -47,9 +48,7 @@ module.exports.deserialize = function(str) {
}

var obj = JSON.parse(str, reviver);
return Object.keys(obj).reduce(function(item, key) {
var value = converter.output(obj[key]);
item[key] = value;
return item;
}, {});
return unmarshall(obj, {
wrapNumbers: false
});
};
87 changes: 79 additions & 8 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function reduceCapacity(existing, incoming) {
* Cast CapacityUnits to ReadCapacityUnits or WriteCapacityUnits based on the cast type
* @param {Object} indexes GlobalSecondaryIndexes or LocalSecondaryIndexes see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ConsumedCapacity.html#DDB-Type-ConsumedCapacity-GlobalSecondaryIndexes
* @param {string} castKey ReadCapacityUnits | WriteCapacityUnits
*
*
*/
function castIndexesCapacity (indexes, castKey) {
if (!indexes) return null;
Expand All @@ -72,7 +72,7 @@ function castIndexesCapacity (indexes, castKey) {
}

/**
* Call the costLogger function configured in the options with consumed CapacityUnits
* Call the costLogger function configured in the options with consumed CapacityUnits
* @param {object} res The response of dynamoDB request
* @param {string} type Read | Write
* @param {function} costLogger configured in the options
Expand All @@ -99,7 +99,7 @@ function callCostLogger (res, type, costLogger, Time) {
/**
* Wrap the native method of DynamoDB to call the costLogger in the callback.
* If no costLogger is provided, just return the native method directly
*
*
* @param {function} costLogger The function that will be called with casted consumedCapacity
* @param {function} nativeMethod The native method of dynamoDB
* @param {string} type Indicate the method consumes Read or Write capacity
Expand Down Expand Up @@ -138,7 +138,7 @@ function requestHandler(costLogger, nativeMethod, type) {

/**
* Get if a method is a write or read method
* @param {string} fnName
* @param {string} fnName
* @returns Write|Read
*/
function getMethodType (fnName) {
Expand All @@ -154,15 +154,86 @@ function getMethodType (fnName) {
* Wrap all methods used of DynamoDB DocumentClient with requestHandler
* @param {object} client DynamoDB DocumentClient
* @param {function} costLogger The function that will be called with casted consumedCapacity
* @returns
* @returns
*/
function wrapDocClient (client, costLogger) {
const methods = ['batchGet', 'batchWrite', 'put', 'get', 'update', 'delete', 'query', 'scan'];
// Import commands from @aws-sdk/lib-dynamodb
const {
GetCommand,
PutCommand,
UpdateCommand,
DeleteCommand,
QueryCommand,
ScanCommand,
BatchGetCommand,
BatchWriteCommand
} = require('@aws-sdk/lib-dynamodb');

// Create wrapper methods that use the send method with commands
const methods = {
get: function(params, callback) {
const command = new GetCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
put: function(params, callback) {
const command = new PutCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
update: function(params, callback) {
const command = new UpdateCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
delete: function(params, callback) {
const command = new DeleteCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
query: function(params, callback) {
const command = new QueryCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
scan: function(params, callback) {
const command = new ScanCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
batchGet: function(params, callback) {
const command = new BatchGetCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
},
batchWrite: function(params, callback) {
const command = new BatchWriteCommand(params);
if (callback) {
return client.send(command, callback);
}
return client.send(command);
}
};

return Object.fromEntries(
methods.map(function (m) {
Object.keys(methods).map(function (m) {
return [
m,
requestHandler(costLogger, client[m].bind(client), getMethodType(m)),
requestHandler(costLogger, methods[m], getMethodType(m)),
];
})
);
Expand Down
Loading