Source: datatypes.js

"use strict";

/**
 * @class Creates a DynamoDBDatatype that takes care of all datatype handling.
 *
 * @name DynamoDBDatatype
 */
function DynamoDBDatatype() {    
    var AWS = typeof(window) === "undefined" ? require("aws-sdk") : window.AWS;
    var Uint8ArrayError = "Uint8Array can only be used for Binary in Browser.";
    var ScalarDatatypeError = "Unrecognized Scalar Datatype to be formatted.";
    var GeneralDatatypeError = "Unrecognized Datatype to be formatted.";
    var BinConversionError = "Need to pass in Buffer or Uint8Array. ";
    var StrConversionError = "Need to pass in string primitive to be converted to binary.";

    function isScalarType(dataType) {
        
        var type = typeof(dataType);
        return  type === "number"  ||
                type === "string"  ||
                type === "boolean" ||
                (dataType instanceof(Uint8Array) && AWS.util.isBrowser()) ||
                dataType instanceof(AWS.util.Buffer) ||
                dataType === null;
    }
    
    function isSetType(dataType) {
        return dataType.datatype === "SS" ||
                dataType.datatype === "NS" ||
                dataType.datatype === "BS";
    }
    
    function isRecursiveType(dataType) {
        
        return Array.isArray(dataType) ||
                typeof(dataType) === "object";
    }
    
    function formatRecursiveType(dataType) {
        
        var recursiveDoc = {};
    
        var value = {};
        var type = "M";
        if (Array.isArray(dataType)) {
            value = [];
            type = "L";
        }
    
        for (var key in dataType) {
            value[key] = this.formatDataType(dataType[key]);
        }
    
        recursiveDoc[type] = value;
        return recursiveDoc; 
    }
    
    /** @throws Uint8ArrayError, ScalarDatatypeError
     *  @private */
    function formatScalarType(dataType) {
        
        if (dataType == null) {
            return { "NULL" : true };
        }
    
        var type = typeof(dataType);
        if (type === "string") {
            return { "S" : dataType };
        } else if (type === "number") {
            return { "N" : String(dataType) };
        } else if (type === "boolean") {
            return { "BOOL" : dataType };
        } else if (dataType instanceof(AWS.util.Buffer)) {
            return { "B" : dataType };
        } else if (dataType instanceof(Uint8Array)) {
            if (AWS.util.isBrowser()) {
                return { "B" : dataType };
            } else {
                throw new Error(Uint8ArrayError);
            }
        } else {
            throw new Error(ScalarDatatypeError);
        }
    }
    
    /**
     * Formats Javascript datatypes into DynamoDB wire format.
     * 
     * @name formatDataType
     * @function
     * @memberOf DynamoDBDatatype#
     * @param dataType Javascript datatype (i.e. string, number. For full information, check out the README).
     * @return {object} DynamoDB JSON-like wire format.
     * @throws GeneralDatatypeError
     */
    this.formatDataType = function(dataType) {
        
        if (isScalarType(dataType)) {
            return formatScalarType(dataType);
        } else if (isSetType(dataType)) {
            return dataType.format();
        } else if (isRecursiveType(dataType)) {
            return formatRecursiveType.call(this, dataType);            
        }  else {
            throw new Error(GeneralDatatypeError);
        }
        
    };

    function str2Bin(value) {
        if (typeof(value) !== "string") {
            throw new Error(StrConversionError);
        }

        if (AWS.util.isBrowser()) {
            var len = value.length;
            var bin = new Uint8Array(new ArrayBuffer(len));
            for (var i = 0; i < len; i++) {
                bin[i] = value.charCodeAt(i);
            }
            return bin;
        } else {
            return AWS.util.Buffer(value);
        }
    }

    /**
     * Utility to convert a String to a Binary object.
     *
     * @function strToBin
     * @memberOf DynamoDBDatatype#
     * @param {string} value String value to converted to Binary object.
     * @return {object} (Buffer | Uint8Array) depending on Node or browser.
     * @throws StrConversionError
     */
    this.strToBin = function(value) {
        return str2Bin.call(this, value);
    };

    function bin2Str(value) {
        if (!(value instanceof(AWS.util.Buffer)) && !(value instanceof(Uint8Array))) {
            throw new Error(BinConversionError);
        }

        if (AWS.util.isBrowser()) {
            return String.fromCharCode.apply(null, value);
        } else {
            return value.toString("utf-8").valueOf();
        }
    }

    /**
     * Utility to convert a Binary object into a decoded String.
     *
     * @function binToStr
     * @memberOf DynamoDBDatatype#
     * @param {object} value Binary value (Buffer | Uint8Array) depending on Node or browser.
     * @return {string} decoded String in UTF-8
     * @throws BinConversionError
     */
    this.binToStr = function(value) {
        return bin2Str.call(this, value);
    };
    
    /**
     * Utility to create the DynamoDB Set Datatype.
     * 
     * @function createSet
     * @memberOf DynamoDBDatatype#
     * @param {array} set An array that contains elements of the same typed as defined by {type}.
     * @param {string} type Can only be a [S]tring, [N]umber, or [B]inary type.
     * @return {Set} Custom Set object that follow {type}.
     * @throws InvalidSetType, InconsistentType
     */
    this.createSet = function(set, type) {
        if (type !== "N" && type !== "S" && type !== "B") {
            throw new Error(this.type + " is an invalid type for Set");
        }
    
        var setObj = function Set(set, type) {
            this.datatype = type + "S";
            this.contents = {};
    
            this.add = function(value) {
                if (this.datatype === "SS" && typeof(value) === "string") {
                    this.contents[value] = value;
                } else if (this.datatype === "NS" && typeof(value) === "number") {
                    this.contents[value] = value;
                } else if (this.datatype === "BS" && value instanceof(AWS.util.Buffer)) {
                    this.contents[bin2Str(value)] = value;
                } else if (this.datatype === "BS" && value instanceof(Uint8Array)) {
                    if (AWS.util.isBrowser()) {
                        this.contents[bin2Str(value)] = value;
                    } else {
                        throw new Error(Uint8ArrayError);
                    }
                } else {
                    throw new Error("Inconsistent in this " + this.type + " Set");
                }
            };
        
            this.contains = function(content) {
                var value = content;
                if (content instanceof AWS.util.Buffer || content instanceof(Uint8Array)) {
                    value = bin2Str(content);
                } 
                if (this.contents[value] === undefined) {
                    return false;
                }
                return true;
            };
    
            this.remove = function(content) {
                var value = content;
                if (content instanceof AWS.util.Buffer || content instanceof(Uint8Array)) {
                    value = bin2Str(content);
                }
                delete this.contents[value];
            };
    
            this.toArray = function() {
                var keys = Object.keys(this.contents);
                var arr = [];
    
                for (var keyIndex in keys) {
                    var key = keys[keyIndex];
                    if (this.contents.hasOwnProperty(key)) {
                        arr.push(this.contents[key]);
                    }
                }
    
                return arr;
            };
    
            this.format = function() {
                var values = this.toArray(); 
                var result = {};
                result[this.datatype] = values;
                return result;
            };
    
            if (set) {
                for (var index in set) {
                    this.add(set[index]);
                }
            }
        };
    
        return new setObj(set, type);
    };

    /**
     * Formats DynamoDB wire format into javascript datatypes.
     * 
     * @name formatWireType
     * @function
     * @memberOf DynamoDBDatatype#
     * @param {string} key Key that represents the type of the attribute value
     * @param value Javascript datatype of the attribute value produced by DynamoDB
     * @throws GeneralDatatypeError
     */
    this.formatWireType = function(key, value) {
        switch (key) {
            case "S":
            case "B":
            case "BOOL":
                return value;
            case "N":
                return Number(value);
            case "NULL":
                return null;
            case "L":
            case "M":
                for (var keyIndex in value) {
                    var subValue = value[keyIndex];
                    var subKey = Object.keys(subValue)[0];
                    value[keyIndex] = this.formatWireType(subKey, subValue[subKey]);
                }
                return value;
            case "SS":
                return new this.createSet(value, "S");
            case "NS":
                value = value.map(function(each) { return Number(each);});
                return new this.createSet(value, "N");
            case "BS":
                return new this.createSet(value, "B");
            default:
                throw "Service returned unrecognized datatype " + key;
        }
    }
} 

if (typeof(module) !== "undefined") {
    var exports = module.exports = {};
    exports.DynamoDBDatatype = DynamoDBDatatype;
}