Skip to content

Commit

Permalink
Merge pull request #1651 from jeskew/feature/document-client-number-w…
Browse files Browse the repository at this point in the history
…rapper

Add a `wrapNumbers` option to the DynamoDB Document Client
  • Loading branch information
AllanZhengYP authored Jul 31, 2017
2 parents 53e196e + 91608d5 commit 4e1e02c
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 125 deletions.
5 changes: 5 additions & 0 deletions .changes/next-release/feature-DynamoDB-9194166f.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "DynamoDB",
"description": "Add a `wrapNumbers` option to the Document Client to direct the client not to convert number attributes to JavaScript numbers."
}
10 changes: 8 additions & 2 deletions lib/dynamodb/converter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ export class Converter {
options?: Converter.ConverterOptions
): DynamoDB.AttributeMap;

static output(data: DynamoDB.AttributeValue): any;
static output(
data: DynamoDB.AttributeValue,
options?: Converter.ConverterOptions
): any;

static unmarshall(data: DynamoDB.AttributeMap): {[key: string]: any};
static unmarshall(
data: DynamoDB.AttributeMap,
options?: Converter.ConverterOptions
): {[key: string]: any};
}

export namespace Converter {
Expand Down
59 changes: 51 additions & 8 deletions lib/dynamodb/converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var AWS = require('../core');
var util = AWS.util;
var typeOf = require('./types').typeOf;
var DynamoDBSet = require('./set');
var NumberValue = require('./numberValue');

AWS.DynamoDB.Converter = {
/**
Expand All @@ -12,6 +13,12 @@ AWS.DynamoDB.Converter = {
* @option options convertEmptyValues [Boolean] Whether to automatically
* convert empty strings, blobs,
* and sets to `null`
* @option options wrapNumbers [Boolean] Whether to return numbers as a
* NumberValue object instead of
* converting them to native JavaScript
* numbers. This allows for the safe
* round-trip transport of numbers of
* arbitrary size.
* @return [map] An object in the Amazon DynamoDB AttributeValue format
*
* @see AWS.DynamoDB.Converter.marshall AWS.DynamoDB.Converter.marshall to
Expand All @@ -31,7 +38,7 @@ AWS.DynamoDB.Converter = {
return convertInput(null);
}
return { S: data };
} else if (type === 'Number') {
} else if (type === 'Number' || type === 'NumberValue') {
return { N: data.toString() };
} else if (type === 'Binary') {
if (data.length === 0 && options.convertEmptyValues) {
Expand All @@ -56,6 +63,12 @@ AWS.DynamoDB.Converter = {
* @option options convertEmptyValues [Boolean] Whether to automatically
* convert empty strings, blobs,
* and sets to `null`
* @option options wrapNumbers [Boolean] Whether to return numbers as a
* NumberValue object instead of
* converting them to native JavaScript
* numbers. This allows for the safe
* round-trip transport of numbers of
* arbitrary size.
*
* @return [map] An object in the DynamoDB record format.
*
Expand All @@ -82,26 +95,37 @@ AWS.DynamoDB.Converter = {
* Convert a DynamoDB AttributeValue object to its equivalent JavaScript type.
*
* @param data [map] An object in the Amazon DynamoDB AttributeValue format
* @param options [map]
* @option options convertEmptyValues [Boolean] Whether to automatically
* convert empty strings, blobs,
* and sets to `null`
* @option options wrapNumbers [Boolean] Whether to return numbers as a
* NumberValue object instead of
* converting them to native JavaScript
* numbers. This allows for the safe
* round-trip transport of numbers of
* arbitrary size.
*
* @return [Object|Array|String|Number|Boolean|null]
*
* @see AWS.DynamoDB.Converter.unmarshall AWS.DynamoDB.Converter.unmarshall to
* convert entire records (rather than individual attributes)
*/
output: function convertOutput(data) {
output: function convertOutput(data, options) {
options = options || {};
var list, map, i;
for (var type in data) {
var values = data[type];
if (type === 'M') {
map = {};
for (var key in values) {
map[key] = convertOutput(values[key]);
map[key] = convertOutput(values[key], options);
}
return map;
} else if (type === 'L') {
list = [];
for (i = 0; i < values.length; i++) {
list.push(convertOutput(values[i]));
list.push(convertOutput(values[i], options));
}
return list;
} else if (type === 'SS') {
Expand All @@ -113,7 +137,7 @@ AWS.DynamoDB.Converter = {
} else if (type === 'NS') {
list = [];
for (i = 0; i < values.length; i++) {
list.push(Number(values[i]));
list.push(convertNumber(values[i], options.wrapNumbers));
}
return new DynamoDBSet(list);
} else if (type === 'BS') {
Expand All @@ -125,7 +149,7 @@ AWS.DynamoDB.Converter = {
} else if (type === 'S') {
return values + '';
} else if (type === 'N') {
return Number(values);
return convertNumber(values, options.wrapNumbers);
} else if (type === 'B') {
return new util.Buffer(values);
} else if (type === 'BOOL') {
Expand All @@ -140,6 +164,16 @@ AWS.DynamoDB.Converter = {
* Convert a DynamoDB record into a JavaScript object.
*
* @param data [any] The DynamoDB record
* @param options [map]
* @option options convertEmptyValues [Boolean] Whether to automatically
* convert empty strings, blobs,
* and sets to `null`
* @option options wrapNumbers [Boolean] Whether to return numbers as a
* NumberValue object instead of
* converting them to native JavaScript
* numbers. This allows for the safe
* round-trip transport of numbers of
* arbitrary size.
*
* @return [map] An object whose properties have been converted from
* DynamoDB's AttributeValue format into their corresponding native
Expand All @@ -163,8 +197,8 @@ AWS.DynamoDB.Converter = {
* boolValue: {BOOL: true}
* });
*/
unmarshall: function unmarshall(data) {
return AWS.DynamoDB.Converter.output({M: data});
unmarshall: function unmarshall(data, options) {
return AWS.DynamoDB.Converter.output({M: data}, options);
}
};

Expand All @@ -181,6 +215,15 @@ function formatList(data, options) {
return list;
}

/**
* @api private
* @param value [String]
* @param wrapNumbers [Boolean]
*/
function convertNumber(value, wrapNumbers) {
return wrapNumbers ? new NumberValue(value) : Number(value);
}

/**
* @api private
* @param data [map]
Expand Down
7 changes: 7 additions & 0 deletions lib/dynamodb/document_client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export namespace DocumentClient {
* empty strings, buffers, and sets to NULL shapes
*/
convertEmptyValues?: boolean;

/**
* Whether to return numbers as a NumberValue object instead of
* converting them to native JavaScript numbers. This allows for the
* safe round-trip transport of numbers of arbitrary size.
*/
wrapNumbers?: boolean;
}

export interface DocumentClientOptions extends ConverterOptions {
Expand Down
9 changes: 9 additions & 0 deletions lib/dynamodb/numberValue.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class NumberValue {
constructor(value: string|number);

toJSON(): number;

toNumber(): number;

toString(): string;
}
39 changes: 39 additions & 0 deletions lib/dynamodb/numberValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
var util = require('../core').util;

/**
* An object recognizable as a numeric value that stores the underlying number
* as a string.
*
* Intended to be a deserialization target for the DynamoDB Document Client when
* the `wrapNumbers` flag is set. This allows for numeric values that lose
* precision when converted to JavaScript's `number` type.
*/
var DynamoDBNumberValue = util.inherit({
constructor: function NumberValue(value) {
this.value = value.toString();
},

/**
* Render the underlying value as a number when converting to JSON.
*/
toJSON: function () {
return this.toNumber();
},

/**
* Convert the underlying value to a JavaScript number.
*/
toNumber: function () {
return Number(this.value);
},

/**
* Return a string representing the unaltered value provided to the
* constructor.
*/
toString: function () {
return this.value;
}
});

module.exports = DynamoDBNumberValue;
20 changes: 10 additions & 10 deletions lib/dynamodb/set.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
var util = require('../core').util;
var typeOf = require('./types').typeOf;

var memberTypeToSetType = {
'String': 'String',
'Number': 'Number',
'NumberValue': 'Number',
'Binary': 'Binary'
};

/**
* @api private
*/
Expand All @@ -21,15 +28,8 @@ var DynamoDBSet = util.inherit({
},

detectType: function() {
var self = this;
var value = self.values[0];
if (typeOf(value) === 'String') {
self.type = 'String';
} else if (typeOf(value) === 'Number') {
self.type = 'Number';
} else if (typeOf(value) === 'Binary') {
self.type = 'Binary';
} else {
this.type = memberTypeToSetType[typeOf(this.values[0])];
if (!this.type) {
throw util.error(new Error(), {
code: 'InvalidSetType',
message: 'Sets can contain string, number, or binary values'
Expand All @@ -42,7 +42,7 @@ var DynamoDBSet = util.inherit({
var length = self.values.length;
var values = self.values;
for (var i = 0; i < length; i++) {
if (typeOf(values[i]) !== self.type) {
if (memberTypeToSetType[typeOf(values[i])] !== self.type) {
throw util.error(new Error(), {
code: 'InvalidType',
message: self.type + ' Set contains ' + typeOf(values[i]) + ' value'
Expand Down
6 changes: 5 additions & 1 deletion lib/dynamodb/translator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var Translator = function(options) {
options = options || {};
this.attrValue = options.attrValue;
this.convertEmptyValues = Boolean(options.convertEmptyValues);
this.wrapNumbers = Boolean(options.wrapNumbers);
};

Translator.prototype.translateInput = function(value, shape) {
Expand All @@ -22,7 +23,10 @@ Translator.prototype.translate = function(value, shape) {
if (!shape || value === undefined) return undefined;

if (shape.shape === self.attrValue) {
return convert[self.mode](value, {convertEmptyValues: self.convertEmptyValues});
return convert[self.mode](value, {
convertEmptyValues: self.convertEmptyValues,
wrapNumbers: self.wrapNumbers,
});
}
switch (shape.type) {
case 'structure': return self.translateStructure(value, shape);
Expand Down
Loading

0 comments on commit 4e1e02c

Please sign in to comment.