ddbitemsizer
Version:
Get the byte size of your DynamoDB input.
214 lines (213 loc) • 7.22 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamoDbItemSizer = void 0;
const bytes_1 = require("./bytes");
class DynamoDbItemSizer {
/**
* @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypeDescriptors
*/
ddbDataTypeKeys = ['S', 'N', 'B', 'BOOL', 'NULL', 'M', 'L', 'SS', 'NS', 'BS'];
/**
* @description Gets the approximate byte size for a DynamoDB input.
* @example
* ```
* import { DynamoDbItemSizer } from 'ddbitemsizer';
*
* const itemSizer = new DynamoDbItemSizer();
*
* itemSize.get({
* key: {
* S: 'Hello World'
* }
* });
*```
*/
get(object) {
if (!object)
throw new MissingInputError();
const entries = Object.entries(object);
return (entries.reduce((total, current) => {
const [key, value] = current;
return total + (0, bytes_1.bytes)(key) + this.bytesForValue(value);
}, 0) || 0);
}
bytesForValue(val) {
const { innerKey, value } = this.getStructure(val);
const type = this.getType(value, innerKey);
return this.getSize(value, type);
}
////////////////////////////
// Input handling section //
////////////////////////////
/**
* @description Check if this a nested, DynamoDB-style object with a data-typed key.
*/
getStructure(value) {
const isObject = this.isValueAnObject(value);
const innerKey = Object.keys(value)[0];
const fixedValue = isObject ? value[innerKey] : value;
return {
innerKey,
value: fixedValue
};
}
isValueAnObject(value) {
return this.isKeyReserved(Object.keys(value));
}
isKeyReserved(keys) {
return keys.length === 1 && this.ddbDataTypeKeys.includes(keys[0]);
}
///////////////////////////
// Type handling section //
///////////////////////////
/**
* @description Get a cleaned type for the input key.
*/
// eslint-disable-next-line complexity
getType(val, innerKey) {
if (innerKey === 'NULL' && this.isNull(val))
return 'null';
if (this.isSet(val)) {
if (innerKey === 'SS' && this.isSet(val))
return 'stringset';
else if (innerKey === 'NS' && this.isSet(val))
return 'numberset';
else if (innerKey === 'BS' && this.isSet(val))
return 'binaryset';
else if (innerKey === 'L' && this.isSet(val))
return 'list';
}
else {
if (innerKey === 'M')
return 'map';
else if (this.isNumber(val))
return 'number';
else if (this.isString(val))
return 'string';
else if (this.isBinary(val))
return 'binary';
else if (this.isBoolean(val))
return 'boolean';
}
throw new MissingTypeError();
}
isString(val) {
return typeof val === 'string';
}
isNumber(val) {
return !isNaN(val);
//return new RegExp(/^-?[0-9]\d+$/).test(`${val}`); // Also checks for negative numbers
}
isBinary(val) {
return Buffer.isBuffer(val);
}
isBoolean(val) {
return typeof val === 'boolean';
}
isNull(val) {
return typeof val === 'boolean' || val === '';
}
isSet(val) {
return Array.isArray(val);
}
///////////////////////////
// Size handling section //
///////////////////////////
/**
* @description Get the size of the input.
*/
// eslint-disable-next-line complexity
getSize(val, type) {
if (type === 'number')
return this.numberSize(val);
else if (type === 'string')
return this.stringSize(val);
else if (type === 'binary')
return this.binarySize(val);
else if (type === 'boolean')
return this.booleanOrNullSize();
else if (type === 'null')
return this.booleanOrNullSize();
else if (type === 'stringset')
return this.stringSetSize(val);
else if (type === 'numberset')
return this.numberSetSize(val);
else if (type === 'binaryset')
return this.binarySetSize(val);
else if (type === 'list')
return this.listSetSize(val);
else if (type === 'map')
return this.mapSize(val);
throw new Error(`Unable to get a size for type: "${type}" and value: ${val}`);
}
stringSize(val) {
return (0, bytes_1.bytes)(val);
}
numberSize(val) {
const fixedValue = this.removeTrailingZeroes(parseFloat(val)).toString();
const result = fixedValue.match(/.{1,2}/g)?.length || 0;
const extraBytes = parseFloat(val) > 0 ? 1 : 2;
return result + extraBytes;
}
removeTrailingZeroes(val) {
return parseFloat(val.toString().replace(/0+$/, ''));
}
binarySize(val) {
return Buffer.from(val).byteLength;
}
booleanOrNullSize() {
return 1;
}
stringSetSize(val) {
return val.reduce((total, current) => total + this.stringSize(current), 0);
}
numberSetSize(val) {
return val.reduce((total, current) => total + this.numberSize(current), 0);
}
binarySetSize(val) {
return val.reduce((total, current) => total + this.binarySize(current), 0);
}
listSetSize(val) {
return (val.reduce((total, current) => {
const innerKey = Object.keys(current)[0];
return total + (0, bytes_1.bytes)(innerKey) + this.bytesForValue(current);
}, 0) + 3 // This is, presumably, for the "L" attribute and the array brackets
);
}
mapSize(val) {
const entries = Object.entries(val);
const values = entries.map((entry) => {
return entry.reduce((total, current) => {
const innerKey = Object.keys(current)[0];
const keyBytes = isNaN(innerKey) ? this.bytesForValue(innerKey) : 0; // Add bytes if there is a valid inner key
return total + this.bytesForValue(current) + keyBytes;
}, 0);
});
return values.reduce((total, current) => total + current) + 3; // This is, presumably, for the "M" attribute and the array brackets
}
}
exports.DynamoDbItemSizer = DynamoDbItemSizer;
/**
* @description Used when missing input.
*/
class MissingInputError extends Error {
constructor() {
super();
this.name = 'MissingInputError';
const message = 'Missing input!';
this.message = message;
process.stdout.write(JSON.stringify(message) + '\n');
}
}
/**
* @description Used when unable to match input to a DynamoDB data type.
*/
class MissingTypeError extends Error {
constructor() {
super();
this.name = 'MissingTypeError';
const message = 'No matching type!';
this.message = message;
process.stdout.write(JSON.stringify(message) + '\n');
}
}