UNPKG

ddbitemsizer

Version:
214 lines (213 loc) 7.22 kB
"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'); } }