-
Notifications
You must be signed in to change notification settings - Fork 573
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(util-dynamodb): unmarshall to convert DynamoDB record to JavaScr…
…ipt Object (#1537)
- Loading branch information
Showing
7 changed files
with
450 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
import { AttributeValue } from "@aws-sdk/client-dynamodb"; | ||
|
||
import { convertToNative } from "./convertToNative"; | ||
import { NativeAttributeValue } from "./models"; | ||
|
||
describe("convertToNative", () => { | ||
const emptyAttr = { | ||
B: undefined, | ||
BOOL: undefined, | ||
BS: undefined, | ||
L: undefined, | ||
M: undefined, | ||
N: undefined, | ||
NS: undefined, | ||
NULL: undefined, | ||
S: undefined, | ||
SS: undefined, | ||
}; | ||
|
||
describe("null", () => { | ||
it(`returns for null`, () => { | ||
expect(convertToNative({ ...emptyAttr, NULL: true })).toEqual(null); | ||
}); | ||
}); | ||
|
||
describe("boolean", () => { | ||
[true, false].forEach((bool) => { | ||
it(`returns for boolean: ${bool}`, () => { | ||
expect(convertToNative({ ...emptyAttr, BOOL: bool })).toEqual(bool); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("number", () => { | ||
const wrapNumbers = true; | ||
|
||
[1, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER] | ||
.map((num) => num.toString()) | ||
.forEach((numString) => { | ||
it(`returns for number (integer): ${numString}`, () => { | ||
expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); | ||
}); | ||
it(`returns NumberValue for number (integer) with options.wrapNumbers set: ${numString}`, () => { | ||
expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual({ value: numString }); | ||
}); | ||
}); | ||
|
||
[1.01, Math.PI, Math.E, Number.MIN_VALUE, Number.EPSILON] | ||
.map((num) => num.toString()) | ||
.forEach((numString) => { | ||
it(`returns for number (floating point): ${numString}`, () => { | ||
expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); | ||
}); | ||
it(`returns NumberValue for number (floating point) with options.wrapNumbers set: ${numString}`, () => { | ||
expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual({ value: numString }); | ||
}); | ||
}); | ||
|
||
[Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] | ||
.map((num) => num.toString()) | ||
.forEach((numString) => { | ||
it(`returns for number (special numeric value): ${numString}`, () => { | ||
expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); | ||
}); | ||
}); | ||
|
||
[Number.MAX_SAFE_INTEGER + 1, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 1] | ||
.map((num) => num.toString()) | ||
.forEach((numString) => { | ||
it(`returns bigint for numbers outside SAFE_INTEGER range: ${numString}`, () => { | ||
expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(BigInt(Number(numString))); | ||
}); | ||
|
||
it(`throws error for numbers outside SAFE_INTEGER range when BigInt is not defined: ${numString}`, () => { | ||
const BigIntConstructor = BigInt; | ||
(BigInt as any) = undefined; | ||
expect(() => { | ||
convertToNative({ ...emptyAttr, N: numString }); | ||
}).toThrowError(`${numString} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); | ||
BigInt = BigIntConstructor; | ||
}); | ||
|
||
it(`returns NumberValue for numbers outside SAFE_INTEGER range with options.wrapNumbers set: ${numString}`, () => { | ||
expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual({ value: numString }); | ||
}); | ||
}); | ||
|
||
[ | ||
`${Number.MAX_SAFE_INTEGER}.1`, | ||
`${Number.MIN_SAFE_INTEGER}.1`, | ||
`${Number.MIN_VALUE}1`, | ||
`-${Number.MIN_VALUE}1`, | ||
].forEach((numString) => { | ||
it(`throws if number is outside IEEE 754 Floating-Point Arithmetic: ${numString}`, () => { | ||
expect(() => { | ||
convertToNative({ ...emptyAttr, N: numString }); | ||
}).toThrowError( | ||
`Value ${numString} is outside IEEE 754 Floating-Point Arithmetic. Set options.wrapNumbers to get string value.` | ||
); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("binary", () => { | ||
it(`returns for Uint8Array`, () => { | ||
const data = new Uint8Array([...Array(64).keys()]); | ||
expect(convertToNative({ ...emptyAttr, B: data })).toEqual(data); | ||
}); | ||
}); | ||
|
||
describe("string", () => { | ||
["", "string", "'single-quote'", '"double-quote"'].forEach((str) => { | ||
it(`returns for string: ${str}`, () => { | ||
expect(convertToNative({ ...emptyAttr, S: str })).toEqual(str); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("list", () => { | ||
const uint8Arr1 = new Uint8Array([...Array(4).keys()]); | ||
const uint8Arr2 = new Uint8Array([...Array(2).keys()]); | ||
([ | ||
{ | ||
input: [{ NULL: true }, { BOOL: false }], | ||
output: [null, false], | ||
}, | ||
{ | ||
input: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }], | ||
output: ["one", 1.01, BigInt(9007199254740996)], | ||
}, | ||
{ | ||
input: [{ B: uint8Arr1 }, { B: uint8Arr2 }], | ||
output: [uint8Arr1, uint8Arr2], | ||
}, | ||
{ | ||
input: [ | ||
{ M: { nullKey: { NULL: true }, boolKey: { BOOL: false } } }, | ||
{ M: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } } }, | ||
], | ||
output: [ | ||
{ nullKey: null, boolKey: false }, | ||
{ stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, | ||
], | ||
}, | ||
{ | ||
input: [ | ||
{ NS: ["1", "2", "3"] }, | ||
{ NS: ["9007199254740996", "-9007199254740996"] }, | ||
{ BS: [uint8Arr1, uint8Arr2] }, | ||
{ SS: ["one", "two", "three"] }, | ||
], | ||
output: [ | ||
new Set([1, 2, 3]), | ||
new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]), | ||
new Set([uint8Arr1, uint8Arr2]), | ||
new Set(["one", "two", "three"]), | ||
], | ||
}, | ||
] as { input: AttributeValue[]; output: NativeAttributeValue[] }[]).forEach(({ input, output }) => { | ||
it(`testing list: ${JSON.stringify(input)}`, () => { | ||
expect(convertToNative({ ...emptyAttr, L: input })).toEqual(output); | ||
}); | ||
}); | ||
|
||
it(`testing list with options.wrapNumbers`, () => { | ||
const input = [{ N: "1.01" }, { N: "9007199254740996" }]; | ||
expect(convertToNative({ ...emptyAttr, L: input as AttributeValue[] }, { wrapNumbers: true })).toEqual( | ||
input.map((item) => ({ value: item.N })) | ||
); | ||
}); | ||
}); | ||
|
||
describe("map", () => { | ||
const uint8Arr1 = new Uint8Array([...Array(4).keys()]); | ||
const uint8Arr2 = new Uint8Array([...Array(2).keys()]); | ||
([ | ||
{ | ||
input: { nullKey: { NULL: true }, boolKey: { BOOL: false } }, | ||
output: { nullKey: null, boolKey: false }, | ||
}, | ||
{ | ||
input: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }, | ||
output: { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, | ||
}, | ||
{ | ||
input: { uint8Arr1Key: { B: uint8Arr1 }, uint8Arr2Key: { B: uint8Arr2 } }, | ||
output: { uint8Arr1Key: uint8Arr1, uint8Arr2Key: uint8Arr2 }, | ||
}, | ||
{ | ||
input: { | ||
list1: { L: [{ NULL: true }, { BOOL: false }] }, | ||
list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] }, | ||
}, | ||
output: { list1: [null, false], list2: ["one", 1.01, BigInt(9007199254740996)] }, | ||
}, | ||
{ | ||
input: { | ||
numberSet: { NS: ["1", "2", "3"] }, | ||
bigintSet: { NS: ["9007199254740996", "-9007199254740996"] }, | ||
binarySet: { BS: [uint8Arr1, uint8Arr2] }, | ||
stringSet: { SS: ["one", "two", "three"] }, | ||
}, | ||
output: { | ||
numberSet: new Set([1, 2, 3]), | ||
bigintSet: new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]), | ||
binarySet: new Set([uint8Arr1, uint8Arr2]), | ||
stringSet: new Set(["one", "two", "three"]), | ||
}, | ||
}, | ||
] as { input: { [key: string]: AttributeValue }; output: { [key: string]: NativeAttributeValue } }[]).forEach( | ||
({ input, output }) => { | ||
it(`testing map: ${input}`, () => { | ||
expect(convertToNative({ ...emptyAttr, M: input })).toEqual(output); | ||
}); | ||
} | ||
); | ||
|
||
it(`testing map with options.wrapNumbers`, () => { | ||
const input = { numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }; | ||
const output = { numberKey: { value: "1.01" }, bigintKey: { value: "9007199254740996" } }; | ||
expect(convertToNative({ ...emptyAttr, M: input }, { wrapNumbers: true })).toEqual(output); | ||
}); | ||
}); | ||
|
||
describe("set", () => { | ||
describe("number set", () => { | ||
const input = ["1", "2", "9007199254740996"]; | ||
|
||
it("without options.wrapNumbers", () => { | ||
expect(convertToNative({ ...emptyAttr, NS: input })).toEqual(new Set([1, 2, BigInt(9007199254740996)])); | ||
}); | ||
|
||
it("with options.wrapNumbers", () => { | ||
expect(convertToNative({ ...emptyAttr, NS: input }, { wrapNumbers: true })).toEqual( | ||
new Set(input.map((numString) => ({ value: numString }))) | ||
); | ||
}); | ||
}); | ||
|
||
it("binary set", () => { | ||
const uint8Arr1 = new Uint8Array([...Array(4).keys()]); | ||
const uint8Arr2 = new Uint8Array([...Array(2).keys()]); | ||
const input = [uint8Arr1, uint8Arr2]; | ||
expect(convertToNative({ ...emptyAttr, BS: input })).toEqual(new Set(input)); | ||
}); | ||
|
||
it("string set", () => { | ||
const input = ["one", "two", "three"]; | ||
expect(convertToNative({ ...emptyAttr, SS: input })).toEqual(new Set(input)); | ||
}); | ||
}); | ||
|
||
describe(`unsupported type`, () => { | ||
["A", "P", "LS"].forEach((type) => { | ||
it(`throws for unsupported type: ${type}`, () => { | ||
expect(() => { | ||
convertToNative({ ...emptyAttr, [type]: "data" }); | ||
}).toThrowError(`Unsupported type passed: ${type}`); | ||
}); | ||
}); | ||
}); | ||
|
||
it(`no value defined`, () => { | ||
expect(() => { | ||
convertToNative(emptyAttr); | ||
}).toThrowError(`No value defined: ${emptyAttr}`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { AttributeValue } from "@aws-sdk/client-dynamodb"; | ||
|
||
import { NativeAttributeValue, NumberValue } from "./models"; | ||
import { unmarshallOptions } from "./unmarshall"; | ||
|
||
/** | ||
* Convert a DynamoDB AttributeValue object to its equivalent JavaScript type. | ||
* | ||
* @param {AttributeValue} data - The DynamoDB record to convert to JavaScript type. | ||
* @param {unmarshallOptions} options - An optional configuration object for `convertToNative`. | ||
*/ | ||
export const convertToNative = (data: AttributeValue, options?: unmarshallOptions): NativeAttributeValue => { | ||
for (const [key, value] of Object.entries(data)) { | ||
if (value !== undefined) { | ||
switch (key) { | ||
case "NULL": | ||
return null; | ||
case "BOOL": | ||
return Boolean(value); | ||
case "N": | ||
return convertNumber(value as string, options); | ||
case "B": | ||
return convertBinary(value as Uint8Array); | ||
case "S": | ||
return convertString(value as string); | ||
case "L": | ||
return convertList(value as AttributeValue[], options); | ||
case "M": | ||
return convertMap(value as { [key: string]: AttributeValue }, options); | ||
case "NS": | ||
return new Set((value as string[]).map((item) => convertNumber(item, options))); | ||
case "BS": | ||
return new Set((value as Uint8Array[]).map(convertBinary)); | ||
case "SS": | ||
return new Set((value as string[]).map(convertString)); | ||
default: | ||
throw new Error(`Unsupported type passed: ${key}`); | ||
} | ||
} | ||
} | ||
throw new Error(`No value defined: ${data}`); | ||
}; | ||
|
||
const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | NumberValue => { | ||
if (options?.wrapNumbers) { | ||
return { value: numString }; | ||
} | ||
|
||
const num = Number(numString); | ||
const infinityValues = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]; | ||
if ((num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) && !infinityValues.includes(num)) { | ||
if (typeof BigInt === "function") { | ||
return BigInt(num); | ||
} else { | ||
throw new Error(`${numString} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); | ||
} | ||
} else if (num.toString() !== numString) { | ||
throw new Error( | ||
`Value ${numString} is outside IEEE 754 Floating-Point Arithmetic. Set options.wrapNumbers to get string value.` | ||
); | ||
} | ||
return num; | ||
}; | ||
|
||
// For future-proofing: Functions from scalar value as well as set value | ||
const convertString = (stringValue: string): string => stringValue; | ||
const convertBinary = (binaryValue: Uint8Array): Uint8Array => binaryValue; | ||
|
||
const convertList = (list: AttributeValue[], options?: unmarshallOptions): NativeAttributeValue[] => | ||
list.map((item) => convertToNative(item, options)); | ||
|
||
const convertMap = ( | ||
map: { [key: string]: AttributeValue }, | ||
options?: unmarshallOptions | ||
): { [key: string]: NativeAttributeValue } => | ||
Object.entries(map).reduce( | ||
(acc: { [key: string]: NativeAttributeValue }, [key, value]: [string, AttributeValue]) => ({ | ||
...acc, | ||
[key]: convertToNative(value, options), | ||
}), | ||
{} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
export * from "./convertToAttr"; | ||
export * from "./convertToNative"; | ||
export * from "./marshall"; | ||
export * from "./models"; | ||
export * from "./unmarshall"; |
Oops, something went wrong.