Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(util-dynamodb): unmarshall to convert DynamoDB record to JavaScript Object #1537

Merged
merged 32 commits into from
Sep 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2125167
chore: temporary commit for future util-dynamodb PRs
trivikr Sep 18, 2020
f359940
feat: basic convertToNative with support for string and number
trivikr Sep 21, 2020
1b3c3cb
test: convertToNative unsupported type and no value defined
trivikr Sep 21, 2020
4b7f44e
feat: add null to convertToNative
trivikr Sep 21, 2020
b5508f1
feat: add boolean to convertToNative
trivikr Sep 21, 2020
4251c67
feat: add bigint to convertToNative
trivikr Sep 21, 2020
4bfb175
feat: add binary in convertToNative
trivikr Sep 21, 2020
aa73ac2
feat: add list to convertToNative
trivikr Sep 21, 2020
e5282bb
feat: add map to convertToNative
trivikr Sep 21, 2020
93c4886
feat: add set to convertToNative
trivikr Sep 21, 2020
f6d540a
chore: added utility functions for converting string and binary
trivikr Sep 21, 2020
1278c28
chore: add convert utility methods for list and map
trivikr Sep 21, 2020
7488d55
chore: use Object.entries while iterating through data
trivikr Sep 21, 2020
ba266f4
chore: use switch instead of if-else
trivikr Sep 21, 2020
0a00e4c
feat: add options.wrapNumbers to returns numbers as string
trivikr Sep 22, 2020
b1824bf
chore: store special numeric values in an array
trivikr Sep 22, 2020
4b70463
test: add test for map of lists and list of maps
trivikr Sep 22, 2020
2b3e037
test: name the keys of map
trivikr Sep 22, 2020
1a371b9
chore: export convertToNative from index
trivikr Sep 22, 2020
08dff45
test: add test for list/map of sets
trivikr Sep 22, 2020
f7dfe17
fix: throw error if BigInt is not supported
trivikr Sep 23, 2020
2aa6db6
fix: throw errors for numbers outside IEEE 754 Floating-Point Arithmetic
trivikr Sep 23, 2020
07ae807
feat(util-dynamodb): unmarshall to convert DynamoDB record to JavaScr…
trivikr Sep 23, 2020
27557e8
fix: do not throw for special numberic values like NaN/Infinity
trivikr Sep 24, 2020
0d2dcb8
fix: use wrapper NumberValue instead of string
trivikr Sep 24, 2020
f4da61f
fix: use simple interface NumberValue instead of class wrapper
trivikr Sep 24, 2020
e599054
fix: ts-ignore to ts-expect-error in tests
trivikr Sep 25, 2020
5872c65
chore: remove usage of ts-expect-error
trivikr Sep 25, 2020
69756b3
fix: unresolved conflicts in convertToAttr.spec.ts
trivikr Sep 28, 2020
288d178
fix: add NumberValue in Set type
trivikr Sep 28, 2020
a1a79be
chore: add removed newline from convertToAttr.spec.ts while resolving…
trivikr Sep 28, 2020
1c8a9d6
docs: add unmarshall to README.md
trivikr Sep 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/util-dynamodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,21 @@ const params = {

await client.putItem(params);
```

## Convert DynamoDB Record into JavaScript object

```js
const { DynamoDB } = require("@aws-sdk/client-dynamodb");
const { marshall, unmarshall } = require("@aws-sdk/util-dynamodb");

const client = new DynamoDB(clientParams);
const params = {
TableName: "Table",
Key: marshall({
HashKey: "hashKey",
}),
};

const { Item } = await client.getItem(params);
unmarshall(Item);
```
268 changes: 268 additions & 0 deletions packages/util-dynamodb/src/convertToNative.spec.ts
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}`);
});
});
82 changes: 82 additions & 0 deletions packages/util-dynamodb/src/convertToNative.ts
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),
}),
{}
);
2 changes: 2 additions & 0 deletions packages/util-dynamodb/src/index.ts
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";
Loading