From 212516772f8eac726b8e5818deb1d10438a95648 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Fri, 18 Sep 2020 23:14:41 +0000 Subject: [PATCH 01/32] chore: temporary commit for future util-dynamodb PRs --- .../util-dynamodb/src/convertToAttr.spec.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index 501eeeadfda9..46194529c3bc 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -41,6 +41,7 @@ describe("convertToAttr", () => { [Number.MAX_SAFE_INTEGER + 1, Number.MAX_VALUE].forEach((num) => { it(`throws for number greater than Number.MAX_SAFE_INTEGER: ${num}`, () => { +<<<<<<< HEAD const errorPrefix = `Number ${num} is greater than Number.MAX_SAFE_INTEGER.`; expect(() => { @@ -53,11 +54,17 @@ describe("convertToAttr", () => { convertToAttr(num); }).toThrowError(`${errorPrefix} Pass string value instead.`); BigInt = BigIntConstructor; +======= + expect(() => { + convertToAttr(num); + }).toThrowError(`Number ${num} is greater than Number.MAX_SAFE_INTEGER. Use BigInt.`); +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); }); [Number.MIN_SAFE_INTEGER - 1].forEach((num) => { it(`throws for number lesser than Number.MIN_SAFE_INTEGER: ${num}`, () => { +<<<<<<< HEAD const errorPrefix = `Number ${num} is lesser than Number.MIN_SAFE_INTEGER.`; expect(() => { @@ -70,6 +77,11 @@ describe("convertToAttr", () => { convertToAttr(num); }).toThrowError(`${errorPrefix} Pass string value instead.`); BigInt = BigIntConstructor; +======= + expect(() => { + convertToAttr(num); + }).toThrowError(`Number ${num} is lesser than Number.MIN_SAFE_INTEGER. Use BigInt.`); +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); }); }); @@ -97,7 +109,10 @@ describe("convertToAttr", () => { const buffer = new ArrayBuffer(64); const arr = [...Array(64).keys()]; const addPointOne = (num: number) => num + 0.1; +<<<<<<< HEAD +======= +>>>>>>> chore: temporary commit for future util-dynamodb PRs [ buffer, new Blob([new Uint8Array(buffer)]), @@ -119,16 +134,20 @@ describe("convertToAttr", () => { expect(convertToAttr(data)).toEqual({ B: data }); }); }); +<<<<<<< HEAD it("returns null for Binary when options.convertEmptyValues=true", () => { expect(convertToAttr(new Uint8Array(), { convertEmptyValues: true })).toEqual({ NULL: true }); }); +======= +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); describe("list", () => { const arr = [...Array(4).keys()]; const uint8Arr = new Uint32Array(arr); const biguintArr = new BigUint64Array(arr.map(BigInt)); +<<<<<<< HEAD ([ { @@ -179,6 +198,27 @@ describe("convertToAttr", () => { L: [{ NULL: true }, { NULL: true }, { NULL: true }], }); }); +======= + [ + [ + [null, false], + [{ NULL: true }, { BOOL: false }], + ], + [ + [1.01, BigInt(1), "one"], + [{ N: "1.01" }, { N: "1" }, { S: "one" }], + ], + [ + [uint8Arr, biguintArr], + [{ B: uint8Arr }, { B: biguintArr }], + ], + ].forEach(([input, output]) => { + it(`testing list: ${input}`, () => { + // @ts-ignore + expect(convertToAttr(input)).toEqual({ L: output }); + }); + }); +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); describe("set", () => { @@ -189,8 +229,12 @@ describe("convertToAttr", () => { it("bigint set", () => { // @ts-expect-error BigInt literals are not available when targeting lower than ES2020. +<<<<<<< HEAD const bigNum = BigInt(Number.MAX_SAFE_INTEGER) + 2n; const set = new Set([bigNum, -bigNum]); +======= + const set = new Set([1n, 2n, 3n]); +>>>>>>> chore: temporary commit for future util-dynamodb PRs expect(convertToAttr(set)).toEqual({ NS: Array.from(set).map((num) => num.toString()) }); }); @@ -204,6 +248,7 @@ describe("convertToAttr", () => { expect(convertToAttr(set)).toEqual({ SS: Array.from(set) }); }); +<<<<<<< HEAD it("returns null for empty set for options.convertEmptyValues=true", () => { expect(convertToAttr(new Set([]), { convertEmptyValues: true })).toEqual({ NULL: true }); }); @@ -212,11 +257,20 @@ describe("convertToAttr", () => { expect(() => { convertToAttr(new Set([])); }).toThrowError(`Please pass a non-empty set, or set convertEmptyValues to true.`); +======= + it("throws error for empty set", () => { + expect(() => { + convertToAttr(new Set()); + }).toThrowError(`Please pass a non-empty set`); +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); it("thows error for unallowed set", () => { expect(() => { +<<<<<<< HEAD // @ts-expect-error Type 'Set' is not assignable +======= +>>>>>>> chore: temporary commit for future util-dynamodb PRs convertToAttr(new Set([true, false])); }).toThrowError(`Only Number Set (NS), Binary Set (BS) or String Set (SS) are allowed.`); }); @@ -226,6 +280,7 @@ describe("convertToAttr", () => { const arr = [...Array(4).keys()]; const uint8Arr = new Uint32Array(arr); const biguintArr = new BigUint64Array(arr.map(BigInt)); +<<<<<<< HEAD ([ { @@ -276,6 +331,24 @@ describe("convertToAttr", () => { const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) }; expect(convertToAttr(input, { convertEmptyValues: true })).toEqual({ M: { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } }, +======= + [ + [ + { a: null, b: false }, + { a: { NULL: true }, b: { BOOL: false } }, + ], + [ + { a: 1.01, b: BigInt(1), c: "one" }, + { a: { N: "1.01" }, b: { N: "1" }, c: { S: "one" } }, + ], + [ + { a: uint8Arr, b: biguintArr }, + { a: { B: uint8Arr }, b: { B: biguintArr } }, + ], + ].forEach(([input, output]) => { + it(`testing map: ${input}`, () => { + expect(convertToAttr(input)).toEqual({ M: output }); +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); }); }); @@ -286,14 +359,18 @@ describe("convertToAttr", () => { expect(convertToAttr(str)).toEqual({ S: str }); }); }); +<<<<<<< HEAD it("returns null for string when options.convertEmptyValues=true", () => { expect(convertToAttr("", { convertEmptyValues: true })).toEqual({ NULL: true }); }); +======= +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); describe(`unsupported type`, () => { class FooObj { +<<<<<<< HEAD constructor(private readonly foo: string) {} } @@ -302,9 +379,41 @@ describe("convertToAttr", () => { it(`throws for: ${String(data)}`, () => { expect(() => { // @ts-expect-error Argument is not assignable to parameter of type 'NativeAttributeValue' +======= + constructor() { + // @ts-ignore + this.foo = "foo"; + } + } + + // ToDo: Serialize ES6 class objects as string https://github.com/aws/aws-sdk-js-v3/issues/1535 + [undefined, new Date(), new FooObj()].forEach((data) => { + it(`throws for: ${String(data)}`, () => { + expect(() => { + // @ts-ignore Argument is not assignable to parameter of type 'NativeAttributeValue' +>>>>>>> chore: temporary commit for future util-dynamodb PRs convertToAttr(data); }).toThrowError(`Unsupported type passed: ${String(data)}`); }); }); }); +<<<<<<< HEAD +======= + + describe("convertEmptyValues set to true", () => { + const convertEmptyValues = true; + + it(`returns null for Set`, () => { + expect(convertToAttr(new Set(), { convertEmptyValues })).toEqual({ NULL: true }); + }); + + it(`returns null for String`, () => { + expect(convertToAttr("", { convertEmptyValues })).toEqual({ NULL: true }); + }); + + it(`returns null for Binary`, () => { + expect(convertToAttr(new Uint8Array(), { convertEmptyValues })).toEqual({ NULL: true }); + }); + }); +>>>>>>> chore: temporary commit for future util-dynamodb PRs }); From f3599409cc83ee00b7d89a904cd0f1ec0caa3230 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 16:47:35 +0000 Subject: [PATCH 02/32] feat: basic convertToNative with support for string and number --- .../util-dynamodb/src/convertToNative.spec.ts | 40 +++++++++++++++++++ packages/util-dynamodb/src/convertToNative.ts | 22 ++++++++++ 2 files changed, 62 insertions(+) create mode 100644 packages/util-dynamodb/src/convertToNative.spec.ts create mode 100644 packages/util-dynamodb/src/convertToNative.ts diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts new file mode 100644 index 000000000000..15cb74efac00 --- /dev/null +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -0,0 +1,40 @@ +import { convertToNative } from "./convertToNative"; + +describe("convertToNative", () => { + const input = { + B: undefined, + BOOL: undefined, + BS: undefined, + L: undefined, + M: undefined, + N: undefined, + NS: undefined, + NULL: undefined, + S: undefined, + SS: undefined, + }; + + describe("number", () => { + ["1", Number.MAX_SAFE_INTEGER.toString(), Number.MIN_SAFE_INTEGER.toString()].forEach((num) => { + it(`returns for number (integer): ${num}`, () => { + expect(convertToNative({ ...input, N: num })).toEqual(Number(num)); + }); + }); + + ["1.01", Math.PI.toString(), Math.E.toString(), Number.MIN_VALUE.toString(), Number.EPSILON.toString()].forEach( + (num) => { + it(`returns for number (floating point): ${num}`, () => { + expect(convertToNative({ ...input, N: num })).toEqual(Number(num)); + }); + } + ); + }); + + describe("string", () => { + ["", "string", "'single-quote'", '"double-quote"'].forEach((str) => { + it(`returns for string: ${str}`, () => { + expect(convertToNative({ ...input, S: str })).toEqual(str); + }); + }); + }); +}); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts new file mode 100644 index 000000000000..3f6f2355c5de --- /dev/null +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -0,0 +1,22 @@ +import { AttributeValue } from "@aws-sdk/client-dynamodb"; + +import { NativeAttributeValue } from "./models"; + +/** + * Convert a DynamoDB AttributeValue object to its equivalent JavaScript type. + * + */ +export const convertToNative = (data: AttributeValue): NativeAttributeValue => { + for (const type in data) { + // @ts-expect-error Element implicitly has an 'any' type + if (data[type] !== undefined) { + if (type === "N") { + return Number(data[type]); + } else if (type === "S") { + return data[type] as string; + } + throw new Error(`Unsupported type passed: ${type}`); + } + } + throw new Error(`No value defined in convertToNative: ${data}`); +}; From 1b3c3cbfde83a79100e9e9044e5963f008175089 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 16:55:43 +0000 Subject: [PATCH 03/32] test: convertToNative unsupported type and no value defined --- .../util-dynamodb/src/convertToNative.spec.ts | 16 ++++++++++++++++ packages/util-dynamodb/src/convertToNative.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 15cb74efac00..5d8f32e21914 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -37,4 +37,20 @@ describe("convertToNative", () => { }); }); }); + + describe(`unsupported type`, () => { + ["A", "M", "LS"].forEach((type) => { + it(`throws for unsupported type: ${type}`, () => { + expect(() => { + convertToNative({ ...input, [type]: "data" }); + }).toThrowError(`Unsupported type passed: ${type}`); + }); + }); + }); + + it(`no value defined`, () => { + expect(() => { + convertToNative(input); + }).toThrowError(`No value defined: ${input}`); + }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 3f6f2355c5de..4eb0a4a4719d 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -18,5 +18,5 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { throw new Error(`Unsupported type passed: ${type}`); } } - throw new Error(`No value defined in convertToNative: ${data}`); + throw new Error(`No value defined: ${data}`); }; From 4b7f44e2ea48ff55b05d1c124f338600cc1a8428 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 17:36:22 +0000 Subject: [PATCH 04/32] feat: add null to convertToNative --- packages/util-dynamodb/src/convertToNative.spec.ts | 6 ++++++ packages/util-dynamodb/src/convertToNative.ts | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 5d8f32e21914..f415483d1df7 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -14,6 +14,12 @@ describe("convertToNative", () => { SS: undefined, }; + describe("null", () => { + it(`returns for null`, () => { + expect(convertToNative({ ...input, NULL: true })).toEqual(null); + }); + }); + describe("number", () => { ["1", Number.MAX_SAFE_INTEGER.toString(), Number.MIN_SAFE_INTEGER.toString()].forEach((num) => { it(`returns for number (integer): ${num}`, () => { diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 4eb0a4a4719d..b381c6b311c0 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -10,7 +10,9 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { for (const type in data) { // @ts-expect-error Element implicitly has an 'any' type if (data[type] !== undefined) { - if (type === "N") { + if (type === "NULL") { + return null; + } else if (type === "N") { return Number(data[type]); } else if (type === "S") { return data[type] as string; From b5508f1971850401d39858f255070aeede36c78f Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 17:38:23 +0000 Subject: [PATCH 05/32] feat: add boolean to convertToNative --- packages/util-dynamodb/src/convertToNative.spec.ts | 8 ++++++++ packages/util-dynamodb/src/convertToNative.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index f415483d1df7..2e781c04ce7c 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -20,6 +20,14 @@ describe("convertToNative", () => { }); }); + describe("boolean", () => { + [true, false].forEach((bool) => { + it(`returns for boolean: ${bool}`, () => { + expect(convertToNative({ ...input, BOOL: bool })).toEqual(bool); + }); + }); + }); + describe("number", () => { ["1", Number.MAX_SAFE_INTEGER.toString(), Number.MIN_SAFE_INTEGER.toString()].forEach((num) => { it(`returns for number (integer): ${num}`, () => { diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index b381c6b311c0..6f5f9ea575e1 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -12,6 +12,8 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { if (data[type] !== undefined) { if (type === "NULL") { return null; + } else if (type === "BOOL") { + return Boolean(data[type]); } else if (type === "N") { return Number(data[type]); } else if (type === "S") { From 4251c67763362ae37ad39a8ae1f41af9d0bd03a3 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 18:47:25 +0000 Subject: [PATCH 06/32] feat: add bigint to convertToNative --- .../util-dynamodb/src/convertToNative.spec.ts | 40 ++++++++++++++----- packages/util-dynamodb/src/convertToNative.ts | 16 +++++++- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 2e781c04ce7c..a904acc92728 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -29,19 +29,39 @@ describe("convertToNative", () => { }); describe("number", () => { - ["1", Number.MAX_SAFE_INTEGER.toString(), Number.MIN_SAFE_INTEGER.toString()].forEach((num) => { - it(`returns for number (integer): ${num}`, () => { - expect(convertToNative({ ...input, N: num })).toEqual(Number(num)); + [1, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER] + .map((num) => num.toString()) + .forEach((numString) => { + it(`returns for number (integer): ${numString}`, () => { + expect(convertToNative({ ...input, N: numString })).toEqual(Number(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({ ...input, N: numString })).toEqual(Number(numString)); + }); }); - }); - ["1.01", Math.PI.toString(), Math.E.toString(), Number.MIN_VALUE.toString(), Number.EPSILON.toString()].forEach( - (num) => { - it(`returns for number (floating point): ${num}`, () => { - expect(convertToNative({ ...input, N: num })).toEqual(Number(num)); + [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] + .map((num) => num.toString()) + .forEach((numString) => { + it(`throws for number (special numeric value): ${numString}`, () => { + expect(() => { + convertToNative({ ...input, N: numString }); + }).toThrowError(`Special numeric value ${numString} is not allowed`); }); - } - ); + }); + + [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 MAX_SAFE_INTEGER and MIN_SAFE_INTEGER range: ${numString}`, () => { + expect(convertToNative({ ...input, N: numString })).toEqual(BigInt(Number(numString))); + }); + }); }); describe("string", () => { diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 6f5f9ea575e1..946a0dbc9d46 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -15,7 +15,7 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { } else if (type === "BOOL") { return Boolean(data[type]); } else if (type === "N") { - return Number(data[type]); + return convertNumber(data[type] as string); } else if (type === "S") { return data[type] as string; } @@ -24,3 +24,17 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { } throw new Error(`No value defined: ${data}`); }; + +const convertNumber = (numString: string): number | bigint => { + if ( + [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY].map((num) => num.toString()).includes(numString) + ) { + throw new Error(`Special numeric value ${numString} is not allowed`); + } + + const num = Number(numString); + if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { + return BigInt(num); + } + return num; +}; From 4bfb175850c5f9ed778cf692d4489ed0a6f134d5 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 19:41:56 +0000 Subject: [PATCH 07/32] feat: add binary in convertToNative --- packages/util-dynamodb/src/convertToNative.spec.ts | 7 +++++++ packages/util-dynamodb/src/convertToNative.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index a904acc92728..b2be8e2559e3 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -64,6 +64,13 @@ describe("convertToNative", () => { }); }); + describe("binary", () => { + it(`returns for Uint8Array`, () => { + const data = new Uint8Array([...Array(64).keys()]); + expect(convertToNative({ ...input, B: data })).toEqual(data); + }); + }); + describe("string", () => { ["", "string", "'single-quote'", '"double-quote"'].forEach((str) => { it(`returns for string: ${str}`, () => { diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 946a0dbc9d46..b6f1dad9cdd5 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -16,6 +16,8 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { return Boolean(data[type]); } else if (type === "N") { return convertNumber(data[type] as string); + } else if (type === "B") { + return data[type] as Uint8Array; } else if (type === "S") { return data[type] as string; } From aa73ac212938507df548887d0b775414b77e1711 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 21:00:58 +0000 Subject: [PATCH 08/32] feat: add list to convertToNative --- .../util-dynamodb/src/convertToNative.spec.ts | 49 ++++++++++++++----- packages/util-dynamodb/src/convertToNative.ts | 2 + 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index b2be8e2559e3..25671a08e436 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -1,7 +1,9 @@ +import { AttributeValue } from "@aws-sdk/client-dynamodb"; + import { convertToNative } from "./convertToNative"; describe("convertToNative", () => { - const input = { + const emptyAttr = { B: undefined, BOOL: undefined, BS: undefined, @@ -16,14 +18,14 @@ describe("convertToNative", () => { describe("null", () => { it(`returns for null`, () => { - expect(convertToNative({ ...input, NULL: true })).toEqual(null); + expect(convertToNative({ ...emptyAttr, NULL: true })).toEqual(null); }); }); describe("boolean", () => { [true, false].forEach((bool) => { it(`returns for boolean: ${bool}`, () => { - expect(convertToNative({ ...input, BOOL: bool })).toEqual(bool); + expect(convertToNative({ ...emptyAttr, BOOL: bool })).toEqual(bool); }); }); }); @@ -33,7 +35,7 @@ describe("convertToNative", () => { .map((num) => num.toString()) .forEach((numString) => { it(`returns for number (integer): ${numString}`, () => { - expect(convertToNative({ ...input, N: numString })).toEqual(Number(numString)); + expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); }); }); @@ -41,7 +43,7 @@ describe("convertToNative", () => { .map((num) => num.toString()) .forEach((numString) => { it(`returns for number (floating point): ${numString}`, () => { - expect(convertToNative({ ...input, N: numString })).toEqual(Number(numString)); + expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); }); }); @@ -50,7 +52,7 @@ describe("convertToNative", () => { .forEach((numString) => { it(`throws for number (special numeric value): ${numString}`, () => { expect(() => { - convertToNative({ ...input, N: numString }); + convertToNative({ ...emptyAttr, N: numString }); }).toThrowError(`Special numeric value ${numString} is not allowed`); }); }); @@ -59,7 +61,7 @@ describe("convertToNative", () => { .map((num) => num.toString()) .forEach((numString) => { it(`returns bigint for numbers outside MAX_SAFE_INTEGER and MIN_SAFE_INTEGER range: ${numString}`, () => { - expect(convertToNative({ ...input, N: numString })).toEqual(BigInt(Number(numString))); + expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(BigInt(Number(numString))); }); }); }); @@ -67,14 +69,37 @@ describe("convertToNative", () => { describe("binary", () => { it(`returns for Uint8Array`, () => { const data = new Uint8Array([...Array(64).keys()]); - expect(convertToNative({ ...input, B: data })).toEqual(data); + expect(convertToNative({ ...emptyAttr, B: data })).toEqual(data); }); }); describe("string", () => { ["", "string", "'single-quote'", '"double-quote"'].forEach((str) => { it(`returns for string: ${str}`, () => { - expect(convertToNative({ ...input, S: str })).toEqual(str); + expect(convertToNative({ ...emptyAttr, S: str })).toEqual(str); + }); + }); + }); + + describe("list", () => { + const uint8Arr1 = new Uint8Array([...Array(4).keys()]); + const uint8Arr2 = new Uint8Array([...Array(2).keys()]); + [ + [ + [{ NULL: true }, { BOOL: false }], + [null, false], + ], + [ + [{ N: "1.01" }, { N: "9007199254740996" }, { S: "one" }], + [1.01, BigInt(9007199254740996), "one"], + ], + [ + [{ B: uint8Arr1 }, { B: uint8Arr2 }], + [uint8Arr1, uint8Arr2], + ], + ].forEach(([input, output]) => { + it(`testing list: ${JSON.stringify(input)}`, () => { + expect(convertToNative({ ...emptyAttr, L: input as AttributeValue[] })).toEqual(output); }); }); }); @@ -83,7 +108,7 @@ describe("convertToNative", () => { ["A", "M", "LS"].forEach((type) => { it(`throws for unsupported type: ${type}`, () => { expect(() => { - convertToNative({ ...input, [type]: "data" }); + convertToNative({ ...emptyAttr, [type]: "data" }); }).toThrowError(`Unsupported type passed: ${type}`); }); }); @@ -91,7 +116,7 @@ describe("convertToNative", () => { it(`no value defined`, () => { expect(() => { - convertToNative(input); - }).toThrowError(`No value defined: ${input}`); + convertToNative(emptyAttr); + }).toThrowError(`No value defined: ${emptyAttr}`); }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index b6f1dad9cdd5..aa33d96ce4c6 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -20,6 +20,8 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { return data[type] as Uint8Array; } else if (type === "S") { return data[type] as string; + } else if (type === "L") { + return (data[type] as AttributeValue[]).map(convertToNative); } throw new Error(`Unsupported type passed: ${type}`); } From e5282bbf3a24f29a4f2b6d1a438b6e4f221cc65f Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 21:08:05 +0000 Subject: [PATCH 09/32] feat: add map to convertToNative --- .../util-dynamodb/src/convertToNative.spec.ts | 25 ++++++++++++++++++- packages/util-dynamodb/src/convertToNative.ts | 8 ++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 25671a08e436..2d0e5be3ab3d 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -104,8 +104,31 @@ describe("convertToNative", () => { }); }); + describe("map", () => { + const uint8Arr1 = new Uint8Array([...Array(4).keys()]); + const uint8Arr2 = new Uint8Array([...Array(2).keys()]); + [ + [ + { a: { NULL: true }, b: { BOOL: false } }, + { a: null, b: false }, + ], + [ + { a: { N: "1.01" }, b: { N: "9007199254740996" }, c: { S: "one" } }, + { a: 1.01, b: BigInt(9007199254740996), c: "one" }, + ], + [ + { a: { B: uint8Arr1 }, b: { B: uint8Arr2 } }, + { a: uint8Arr1, b: uint8Arr2 }, + ], + ].forEach(([input, output]) => { + it(`testing map: ${input}`, () => { + expect(convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } })).toEqual(output); + }); + }); + }); + describe(`unsupported type`, () => { - ["A", "M", "LS"].forEach((type) => { + ["A", "P", "LS"].forEach((type) => { it(`throws for unsupported type: ${type}`, () => { expect(() => { convertToNative({ ...emptyAttr, [type]: "data" }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index aa33d96ce4c6..7fd8d01d11a8 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -22,6 +22,14 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { return data[type] as string; } else if (type === "L") { return (data[type] as AttributeValue[]).map(convertToNative); + } else if (type === "M") { + return Object.entries(data[type] as { [key: string]: AttributeValue }).reduce( + (acc: { [key: string]: NativeAttributeValue }, [key, value]: [string, AttributeValue]) => ({ + ...acc, + [key]: convertToNative(value), + }), + {} + ); } throw new Error(`Unsupported type passed: ${type}`); } From 93c48867320aa9ac661f9e13d9b5e917a77e3f56 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 21:23:21 +0000 Subject: [PATCH 10/32] feat: add set to convertToNative --- .../util-dynamodb/src/convertToNative.spec.ts | 19 +++++++++++++++++++ packages/util-dynamodb/src/convertToNative.ts | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 2d0e5be3ab3d..e45ea2899ae1 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -127,6 +127,25 @@ describe("convertToNative", () => { }); }); + describe("set", () => { + it("number set", () => { + const input = ["1", "2", "9007199254740996"]; + expect(convertToNative({ ...emptyAttr, NS: input })).toEqual(new Set([1, 2, BigInt(9007199254740996)])); + }); + + 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}`, () => { diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 7fd8d01d11a8..5412b5cbb3c1 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -30,6 +30,12 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { }), {} ); + } else if (type === "NS") { + return new Set((data[type] as string[]).map(convertNumber)); + } else if (type === "BS") { + return new Set(data[type]); + } else if (type === "SS") { + return new Set(data[type]); } throw new Error(`Unsupported type passed: ${type}`); } From f6d540a395f91125a398ca68353d1e3f654d588d Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 21:32:22 +0000 Subject: [PATCH 11/32] chore: added utility functions for converting string and binary --- packages/util-dynamodb/src/convertToNative.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 5412b5cbb3c1..70627660d817 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -17,9 +17,9 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { } else if (type === "N") { return convertNumber(data[type] as string); } else if (type === "B") { - return data[type] as Uint8Array; + return convertBinary(data[type] as Uint8Array); } else if (type === "S") { - return data[type] as string; + return convertString(data[type] as string); } else if (type === "L") { return (data[type] as AttributeValue[]).map(convertToNative); } else if (type === "M") { @@ -33,9 +33,9 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { } else if (type === "NS") { return new Set((data[type] as string[]).map(convertNumber)); } else if (type === "BS") { - return new Set(data[type]); + return new Set((data[type] as Uint8Array[]).map(convertBinary)); } else if (type === "SS") { - return new Set(data[type]); + return new Set((data[type] as string[]).map(convertString)); } throw new Error(`Unsupported type passed: ${type}`); } @@ -56,3 +56,7 @@ const convertNumber = (numString: string): number | bigint => { } 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; From 1278c28aa13fc3a86dcd534c2d8f8840a3c9fdbe Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 21:44:53 +0000 Subject: [PATCH 12/32] chore: add convert utility methods for list and map --- packages/util-dynamodb/src/convertToNative.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 70627660d817..7ad3052436b6 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -21,15 +21,9 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { } else if (type === "S") { return convertString(data[type] as string); } else if (type === "L") { - return (data[type] as AttributeValue[]).map(convertToNative); + return convertList(data[type] as AttributeValue[]); } else if (type === "M") { - return Object.entries(data[type] as { [key: string]: AttributeValue }).reduce( - (acc: { [key: string]: NativeAttributeValue }, [key, value]: [string, AttributeValue]) => ({ - ...acc, - [key]: convertToNative(value), - }), - {} - ); + return convertMap(data[type] as { [key: string]: AttributeValue }); } else if (type === "NS") { return new Set((data[type] as string[]).map(convertNumber)); } else if (type === "BS") { @@ -60,3 +54,14 @@ const convertNumber = (numString: string): number | bigint => { // 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[]): NativeAttributeValue[] => list.map(convertToNative); + +const convertMap = (map: { [key: string]: AttributeValue }): { [key: string]: NativeAttributeValue } => + Object.entries(map).reduce( + (acc: { [key: string]: NativeAttributeValue }, [key, value]: [string, AttributeValue]) => ({ + ...acc, + [key]: convertToNative(value), + }), + {} + ); From 7488d554018f3f865eaa53e84eec9fb97c6da0ff Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 21:46:24 +0000 Subject: [PATCH 13/32] chore: use Object.entries while iterating through data --- packages/util-dynamodb/src/convertToNative.ts | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 7ad3052436b6..c4d79ef153b0 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -7,31 +7,30 @@ import { NativeAttributeValue } from "./models"; * */ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { - for (const type in data) { - // @ts-expect-error Element implicitly has an 'any' type - if (data[type] !== undefined) { - if (type === "NULL") { + for (const [key, value] of Object.entries(data)) { + if (value !== undefined) { + if (key === "NULL") { return null; - } else if (type === "BOOL") { - return Boolean(data[type]); - } else if (type === "N") { - return convertNumber(data[type] as string); - } else if (type === "B") { - return convertBinary(data[type] as Uint8Array); - } else if (type === "S") { - return convertString(data[type] as string); - } else if (type === "L") { - return convertList(data[type] as AttributeValue[]); - } else if (type === "M") { - return convertMap(data[type] as { [key: string]: AttributeValue }); - } else if (type === "NS") { - return new Set((data[type] as string[]).map(convertNumber)); - } else if (type === "BS") { - return new Set((data[type] as Uint8Array[]).map(convertBinary)); - } else if (type === "SS") { - return new Set((data[type] as string[]).map(convertString)); + } else if (key === "BOOL") { + return Boolean(value); + } else if (key === "N") { + return convertNumber(value as string); + } else if (key === "B") { + return convertBinary(value as Uint8Array); + } else if (key === "S") { + return convertString(value as string); + } else if (key === "L") { + return convertList(value as AttributeValue[]); + } else if (key === "M") { + return convertMap(value as { [key: string]: AttributeValue }); + } else if (key === "NS") { + return new Set((value as string[]).map(convertNumber)); + } else if (key === "BS") { + return new Set((value as Uint8Array[]).map(convertBinary)); + } else if (key === "SS") { + return new Set((value as string[]).map(convertString)); } - throw new Error(`Unsupported type passed: ${type}`); + throw new Error(`Unsupported type passed: ${key}`); } } throw new Error(`No value defined: ${data}`); From ba266f454049e1f12757b5302ff0e3744433cef5 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 21 Sep 2020 21:49:38 +0000 Subject: [PATCH 14/32] chore: use switch instead of if-else --- packages/util-dynamodb/src/convertToNative.ts | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index c4d79ef153b0..51515b040e22 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -9,28 +9,30 @@ import { NativeAttributeValue } from "./models"; export const convertToNative = (data: AttributeValue): NativeAttributeValue => { for (const [key, value] of Object.entries(data)) { if (value !== undefined) { - if (key === "NULL") { - return null; - } else if (key === "BOOL") { - return Boolean(value); - } else if (key === "N") { - return convertNumber(value as string); - } else if (key === "B") { - return convertBinary(value as Uint8Array); - } else if (key === "S") { - return convertString(value as string); - } else if (key === "L") { - return convertList(value as AttributeValue[]); - } else if (key === "M") { - return convertMap(value as { [key: string]: AttributeValue }); - } else if (key === "NS") { - return new Set((value as string[]).map(convertNumber)); - } else if (key === "BS") { - return new Set((value as Uint8Array[]).map(convertBinary)); - } else if (key === "SS") { - return new Set((value as string[]).map(convertString)); + switch (key) { + case "NULL": + return null; + case "BOOL": + return Boolean(value); + case "N": + return convertNumber(value as string); + case "B": + return convertBinary(value as Uint8Array); + case "S": + return convertString(value as string); + case "L": + return convertList(value as AttributeValue[]); + case "M": + return convertMap(value as { [key: string]: AttributeValue }); + case "NS": + return new Set((value as string[]).map(convertNumber)); + 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(`Unsupported type passed: ${key}`); } } throw new Error(`No value defined: ${data}`); From 0a00e4c403b47f7e908b858c08aae056d077dc4d Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 22 Sep 2020 02:35:03 +0000 Subject: [PATCH 15/32] feat: add options.wrapNumbers to returns numbers as string --- .../util-dynamodb/src/convertToNative.spec.ts | 37 +++++++++++++++++- packages/util-dynamodb/src/convertToNative.ts | 39 ++++++++++++++----- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index e45ea2899ae1..ef5aa757ad84 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -31,12 +31,17 @@ describe("convertToNative", () => { }); 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 string for number (integer) with options.wrapNumbers set: ${numString}`, () => { + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(numString); + }); }); [1.01, Math.PI, Math.E, Number.MIN_VALUE, Number.EPSILON] @@ -45,6 +50,9 @@ describe("convertToNative", () => { it(`returns for number (floating point): ${numString}`, () => { expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); }); + it(`returns string for number (floating point) with options.wrapNumbers set: ${numString}`, () => { + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(numString); + }); }); [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] @@ -63,6 +71,9 @@ describe("convertToNative", () => { it(`returns bigint for numbers outside MAX_SAFE_INTEGER and MIN_SAFE_INTEGER range: ${numString}`, () => { expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(BigInt(Number(numString))); }); + it(`returns string for numbers outside MAX_SAFE_INTEGER and MIN_SAFE_INTEGER range with options.wrapNumbers set: ${numString}`, () => { + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(numString); + }); }); }); @@ -102,6 +113,13 @@ describe("convertToNative", () => { expect(convertToNative({ ...emptyAttr, L: input as AttributeValue[] })).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) => item.N) + ); + }); }); describe("map", () => { @@ -125,12 +143,27 @@ describe("convertToNative", () => { expect(convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } })).toEqual(output); }); }); + + it(`testing map with options.wrapNumbers`, () => { + const input = { a: { N: "1.01" }, b: { N: "9007199254740996" } }; + const output = { a: "1.01", b: "9007199254740996" }; + expect( + convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } }, { wrapNumbers: true }) + ).toEqual(output); + }); }); describe("set", () => { - it("number set", () => { + describe("number set", () => { const input = ["1", "2", "9007199254740996"]; - expect(convertToNative({ ...emptyAttr, NS: input })).toEqual(new Set([1, 2, BigInt(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)); + }); }); it("binary set", () => { diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 51515b040e22..1449d735d085 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -2,11 +2,24 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { NativeAttributeValue } from "./models"; +/** + * An optional configuration object for `convertToNative` + */ +export interface convertToNativeOptions { + /** + * Whether to return numbers as a string instead of converting them to native JavaScript numbers. + * This allows for the safe round-trip transport of numbers of arbitrary size. + */ + wrapNumbers?: boolean; +} + /** * Convert a DynamoDB AttributeValue object to its equivalent JavaScript type. * + * @param {AttributeValue} data - The DynamoDB record to convert to JavaScript type. + * @param {convertToNativeOptions} options - An optional configuration object for `convertToNative`. */ -export const convertToNative = (data: AttributeValue): NativeAttributeValue => { +export const convertToNative = (data: AttributeValue, options?: convertToNativeOptions): NativeAttributeValue => { for (const [key, value] of Object.entries(data)) { if (value !== undefined) { switch (key) { @@ -15,17 +28,17 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { case "BOOL": return Boolean(value); case "N": - return convertNumber(value as string); + 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[]); + return convertList(value as AttributeValue[], options); case "M": - return convertMap(value as { [key: string]: AttributeValue }); + return convertMap(value as { [key: string]: AttributeValue }, options); case "NS": - return new Set((value as string[]).map(convertNumber)); + return new Set((value as string[]).map((item) => convertNumber(item, options))); case "BS": return new Set((value as Uint8Array[]).map(convertBinary)); case "SS": @@ -38,13 +51,17 @@ export const convertToNative = (data: AttributeValue): NativeAttributeValue => { throw new Error(`No value defined: ${data}`); }; -const convertNumber = (numString: string): number | bigint => { +const convertNumber = (numString: string, options?: convertToNativeOptions): number | bigint | string => { if ( [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY].map((num) => num.toString()).includes(numString) ) { throw new Error(`Special numeric value ${numString} is not allowed`); } + if (options?.wrapNumbers) { + return numString; + } + const num = Number(numString); if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { return BigInt(num); @@ -56,13 +73,17 @@ const convertNumber = (numString: string): number | bigint => { const convertString = (stringValue: string): string => stringValue; const convertBinary = (binaryValue: Uint8Array): Uint8Array => binaryValue; -const convertList = (list: AttributeValue[]): NativeAttributeValue[] => list.map(convertToNative); +const convertList = (list: AttributeValue[], options?: convertToNativeOptions): NativeAttributeValue[] => + list.map((item) => convertToNative(item, options)); -const convertMap = (map: { [key: string]: AttributeValue }): { [key: string]: NativeAttributeValue } => +const convertMap = ( + map: { [key: string]: AttributeValue }, + options?: convertToNativeOptions +): { [key: string]: NativeAttributeValue } => Object.entries(map).reduce( (acc: { [key: string]: NativeAttributeValue }, [key, value]: [string, AttributeValue]) => ({ ...acc, - [key]: convertToNative(value), + [key]: convertToNative(value, options), }), {} ); From b1824bf413ca05c49fbc2a285acdc45576432fe8 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 22 Sep 2020 02:37:31 +0000 Subject: [PATCH 16/32] chore: store special numeric values in an array --- packages/util-dynamodb/src/convertToNative.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 1449d735d085..5fb636630c2a 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -52,9 +52,8 @@ export const convertToNative = (data: AttributeValue, options?: convertToNativeO }; const convertNumber = (numString: string, options?: convertToNativeOptions): number | bigint | string => { - if ( - [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY].map((num) => num.toString()).includes(numString) - ) { + const specialNumericValues = [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]; + if (specialNumericValues.map((num) => num.toString()).includes(numString)) { throw new Error(`Special numeric value ${numString} is not allowed`); } From 4b70463fe925a337f1f9d047a04e92777c71b255 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 22 Sep 2020 02:45:42 +0000 Subject: [PATCH 17/32] test: add test for map of lists and list of maps --- .../util-dynamodb/src/convertToNative.spec.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index ef5aa757ad84..e21f3adc46cf 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -101,13 +101,23 @@ describe("convertToNative", () => { [null, false], ], [ - [{ N: "1.01" }, { N: "9007199254740996" }, { S: "one" }], - [1.01, BigInt(9007199254740996), "one"], + [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }], + ["one", 1.01, BigInt(9007199254740996)], ], [ [{ B: uint8Arr1 }, { B: uint8Arr2 }], [uint8Arr1, uint8Arr2], ], + [ + [ + { M: { a: { NULL: true }, b: { BOOL: false } } }, + { M: { a: { S: "one" }, b: { N: "1.01" }, c: { N: "9007199254740996" } } }, + ], + [ + { a: null, b: false }, + { a: "one", b: 1.01, c: BigInt(9007199254740996) }, + ], + ], ].forEach(([input, output]) => { it(`testing list: ${JSON.stringify(input)}`, () => { expect(convertToNative({ ...emptyAttr, L: input as AttributeValue[] })).toEqual(output); @@ -131,13 +141,20 @@ describe("convertToNative", () => { { a: null, b: false }, ], [ - { a: { N: "1.01" }, b: { N: "9007199254740996" }, c: { S: "one" } }, - { a: 1.01, b: BigInt(9007199254740996), c: "one" }, + { a: { S: "one" }, b: { N: "1.01" }, c: { N: "9007199254740996" } }, + { a: "one", b: 1.01, c: BigInt(9007199254740996) }, ], [ { a: { B: uint8Arr1 }, b: { B: uint8Arr2 } }, { a: uint8Arr1, b: uint8Arr2 }, ], + [ + { + a: { L: [{ NULL: true }, { BOOL: false }] }, + b: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] }, + }, + { a: [null, false], b: ["one", 1.01, BigInt(9007199254740996)] }, + ], ].forEach(([input, output]) => { it(`testing map: ${input}`, () => { expect(convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } })).toEqual(output); From 2b3e03772a40648a3dab8d20c94e6981a9d3dc4e Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 22 Sep 2020 02:53:28 +0000 Subject: [PATCH 18/32] test: name the keys of map --- .../util-dynamodb/src/convertToNative.spec.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index e21f3adc46cf..b96e95091310 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -110,12 +110,12 @@ describe("convertToNative", () => { ], [ [ - { M: { a: { NULL: true }, b: { BOOL: false } } }, - { M: { a: { S: "one" }, b: { N: "1.01" }, c: { N: "9007199254740996" } } }, + { M: { nullKey: { NULL: true }, boolKey: { BOOL: false } } }, + { M: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } } }, ], [ - { a: null, b: false }, - { a: "one", b: 1.01, c: BigInt(9007199254740996) }, + { nullKey: null, boolKey: false }, + { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, ], ], ].forEach(([input, output]) => { @@ -137,23 +137,23 @@ describe("convertToNative", () => { const uint8Arr2 = new Uint8Array([...Array(2).keys()]); [ [ - { a: { NULL: true }, b: { BOOL: false } }, - { a: null, b: false }, + { nullKey: { NULL: true }, boolKey: { BOOL: false } }, + { nullKey: null, boolKey: false }, ], [ - { a: { S: "one" }, b: { N: "1.01" }, c: { N: "9007199254740996" } }, - { a: "one", b: 1.01, c: BigInt(9007199254740996) }, + { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }, + { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, ], [ - { a: { B: uint8Arr1 }, b: { B: uint8Arr2 } }, - { a: uint8Arr1, b: uint8Arr2 }, + { uint8Arr1Key: { B: uint8Arr1 }, uint8Arr2Key: { B: uint8Arr2 } }, + { uint8Arr1Key: uint8Arr1, uint8Arr2Key: uint8Arr2 }, ], [ { - a: { L: [{ NULL: true }, { BOOL: false }] }, - b: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] }, + list1: { L: [{ NULL: true }, { BOOL: false }] }, + list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] }, }, - { a: [null, false], b: ["one", 1.01, BigInt(9007199254740996)] }, + { list1: [null, false], list2: ["one", 1.01, BigInt(9007199254740996)] }, ], ].forEach(([input, output]) => { it(`testing map: ${input}`, () => { @@ -162,8 +162,8 @@ describe("convertToNative", () => { }); it(`testing map with options.wrapNumbers`, () => { - const input = { a: { N: "1.01" }, b: { N: "9007199254740996" } }; - const output = { a: "1.01", b: "9007199254740996" }; + const input = { numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }; + const output = { numberKey: "1.01", bigintKey: "9007199254740996" }; expect( convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } }, { wrapNumbers: true }) ).toEqual(output); From 1a371b9bfaefa49b2e310392c9f1d9c284f7ee74 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 22 Sep 2020 04:07:57 +0000 Subject: [PATCH 19/32] chore: export convertToNative from index --- packages/util-dynamodb/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/util-dynamodb/src/index.ts b/packages/util-dynamodb/src/index.ts index cf1b70121978..271e1400b42b 100644 --- a/packages/util-dynamodb/src/index.ts +++ b/packages/util-dynamodb/src/index.ts @@ -1,3 +1,4 @@ export * from "./convertToAttr"; +export * from "./convertToNative"; export * from "./marshall"; export * from "./models"; From 08dff454a01ec3ca1b34a093dbeb96fcae3716d4 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 22 Sep 2020 05:52:30 +0000 Subject: [PATCH 20/32] test: add test for list/map of sets --- .../util-dynamodb/src/convertToNative.spec.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index b96e95091310..5f652a95a6bb 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -118,6 +118,20 @@ describe("convertToNative", () => { { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, ], ], + [ + [ + { NS: ["1", "2", "3"] }, + { NS: ["9007199254740996", "-9007199254740996"] }, + { BS: [uint8Arr1, uint8Arr2] }, + { SS: ["one", "two", "three"] }, + ], + [ + new Set([1, 2, 3]), + new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]), + new Set([uint8Arr1, uint8Arr2]), + new Set(["one", "two", "three"]), + ], + ], ].forEach(([input, output]) => { it(`testing list: ${JSON.stringify(input)}`, () => { expect(convertToNative({ ...emptyAttr, L: input as AttributeValue[] })).toEqual(output); @@ -155,6 +169,20 @@ describe("convertToNative", () => { }, { list1: [null, false], list2: ["one", 1.01, BigInt(9007199254740996)] }, ], + [ + { + numberSet: { NS: ["1", "2", "3"] }, + bigintSet: { NS: ["9007199254740996", "-9007199254740996"] }, + binarySet: { BS: [uint8Arr1, uint8Arr2] }, + stringSet: { SS: ["one", "two", "three"] }, + }, + { + numberSet: new Set([1, 2, 3]), + bigintSet: new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]), + binarySet: new Set([uint8Arr1, uint8Arr2]), + stringSet: new Set(["one", "two", "three"]), + }, + ], ].forEach(([input, output]) => { it(`testing map: ${input}`, () => { expect(convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } })).toEqual(output); From f7dfe1751687eccc704fc083e51b14efdcbdb691 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Wed, 23 Sep 2020 18:40:34 +0000 Subject: [PATCH 21/32] fix: throw error if BigInt is not supported --- packages/util-dynamodb/src/convertToNative.spec.ts | 14 ++++++++++++-- packages/util-dynamodb/src/convertToNative.ts | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 5f652a95a6bb..f6c6bc44d782 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -68,10 +68,20 @@ describe("convertToNative", () => { [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 MAX_SAFE_INTEGER and MIN_SAFE_INTEGER range: ${numString}`, () => { + it(`returns bigint for numbers outside SAFE_INTEGER range: ${numString}`, () => { expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(BigInt(Number(numString))); }); - it(`returns string for numbers outside MAX_SAFE_INTEGER and MIN_SAFE_INTEGER range with options.wrapNumbers set: ${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 string for numbers outside SAFE_INTEGER range with options.wrapNumbers set: ${numString}`, () => { expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(numString); }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 5fb636630c2a..fe209c1d7727 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -63,7 +63,11 @@ const convertNumber = (numString: string, options?: convertToNativeOptions): num const num = Number(numString); if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { - return BigInt(num); + if (typeof BigInt === "function") { + return BigInt(num); + } else { + throw new Error(`${num} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); + } } return num; }; From 2aa6db6bc41c486de58fb24267569278ec976e00 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Wed, 23 Sep 2020 18:56:20 +0000 Subject: [PATCH 22/32] fix: throw errors for numbers outside IEEE 754 Floating-Point Arithmetic --- .../util-dynamodb/src/convertToNative.spec.ts | 15 +++++++++++++++ packages/util-dynamodb/src/convertToNative.ts | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index f6c6bc44d782..c366405360f7 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -85,6 +85,21 @@ describe("convertToNative", () => { expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(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", () => { diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index fe209c1d7727..c012fc7c12ae 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -66,8 +66,12 @@ const convertNumber = (numString: string, options?: convertToNativeOptions): num if (typeof BigInt === "function") { return BigInt(num); } else { - throw new Error(`${num} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); + 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; }; From 07ae807bfa8e86750133e7ac3960147552efb0c9 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Wed, 23 Sep 2020 21:57:42 +0000 Subject: [PATCH 23/32] feat(util-dynamodb): unmarshall to convert DynamoDB record to JavaScript object --- packages/util-dynamodb/src/convertToNative.ts | 22 ++++--------- packages/util-dynamodb/src/index.ts | 1 + packages/util-dynamodb/src/unmarshall.spec.ts | 32 +++++++++++++++++++ packages/util-dynamodb/src/unmarshall.ts | 27 ++++++++++++++++ 4 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 packages/util-dynamodb/src/unmarshall.spec.ts create mode 100644 packages/util-dynamodb/src/unmarshall.ts diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index c012fc7c12ae..28889e23fdd1 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -1,25 +1,15 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { NativeAttributeValue } from "./models"; - -/** - * An optional configuration object for `convertToNative` - */ -export interface convertToNativeOptions { - /** - * Whether to return numbers as a string instead of converting them to native JavaScript numbers. - * This allows for the safe round-trip transport of numbers of arbitrary size. - */ - wrapNumbers?: boolean; -} +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 {convertToNativeOptions} options - An optional configuration object for `convertToNative`. + * @param {unmarshallOptions} options - An optional configuration object for `convertToNative`. */ -export const convertToNative = (data: AttributeValue, options?: convertToNativeOptions): NativeAttributeValue => { +export const convertToNative = (data: AttributeValue, options?: unmarshallOptions): NativeAttributeValue => { for (const [key, value] of Object.entries(data)) { if (value !== undefined) { switch (key) { @@ -51,7 +41,7 @@ export const convertToNative = (data: AttributeValue, options?: convertToNativeO throw new Error(`No value defined: ${data}`); }; -const convertNumber = (numString: string, options?: convertToNativeOptions): number | bigint | string => { +const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | string => { const specialNumericValues = [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]; if (specialNumericValues.map((num) => num.toString()).includes(numString)) { throw new Error(`Special numeric value ${numString} is not allowed`); @@ -80,12 +70,12 @@ const convertNumber = (numString: string, options?: convertToNativeOptions): num const convertString = (stringValue: string): string => stringValue; const convertBinary = (binaryValue: Uint8Array): Uint8Array => binaryValue; -const convertList = (list: AttributeValue[], options?: convertToNativeOptions): NativeAttributeValue[] => +const convertList = (list: AttributeValue[], options?: unmarshallOptions): NativeAttributeValue[] => list.map((item) => convertToNative(item, options)); const convertMap = ( map: { [key: string]: AttributeValue }, - options?: convertToNativeOptions + options?: unmarshallOptions ): { [key: string]: NativeAttributeValue } => Object.entries(map).reduce( (acc: { [key: string]: NativeAttributeValue }, [key, value]: [string, AttributeValue]) => ({ diff --git a/packages/util-dynamodb/src/index.ts b/packages/util-dynamodb/src/index.ts index 271e1400b42b..0d6f5d7852bd 100644 --- a/packages/util-dynamodb/src/index.ts +++ b/packages/util-dynamodb/src/index.ts @@ -2,3 +2,4 @@ export * from "./convertToAttr"; export * from "./convertToNative"; export * from "./marshall"; export * from "./models"; +export * from "./unmarshall"; diff --git a/packages/util-dynamodb/src/unmarshall.spec.ts b/packages/util-dynamodb/src/unmarshall.spec.ts new file mode 100644 index 000000000000..7c74df3e9afe --- /dev/null +++ b/packages/util-dynamodb/src/unmarshall.spec.ts @@ -0,0 +1,32 @@ +import { convertToNative } from "./convertToNative"; +import { unmarshall } from "./unmarshall"; + +jest.mock("./convertToNative"); + +describe("marshall", () => { + const input = { a: "A" }; + + beforeEach(() => { + (convertToNative as jest.Mock).mockReturnValue(input); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("calls convertToNative", () => { + // @ts-ignore output mocked for testing + expect(unmarshall(input)).toEqual(input); + expect(convertToNative).toHaveBeenCalledTimes(1); + expect(convertToNative).toHaveBeenCalledWith({ M: input }, undefined); + }); + + [false, true].forEach((wrapNumbers) => { + it(`passes wrapNumbers=${wrapNumbers} to convertToNative`, () => { + // @ts-ignore output mocked for testing + expect(unmarshall(input, { wrapNumbers })).toEqual(input); + expect(convertToNative).toHaveBeenCalledTimes(1); + expect(convertToNative).toHaveBeenCalledWith({ M: input }, { wrapNumbers }); + }); + }); +}); diff --git a/packages/util-dynamodb/src/unmarshall.ts b/packages/util-dynamodb/src/unmarshall.ts new file mode 100644 index 000000000000..6b13e0da08c4 --- /dev/null +++ b/packages/util-dynamodb/src/unmarshall.ts @@ -0,0 +1,27 @@ +import { AttributeValue } from "@aws-sdk/client-dynamodb"; + +import { convertToNative } from "./convertToNative"; +import { NativeAttributeValue } from "./models"; + +/** + * An optional configuration object for `convertToNative` + */ +export interface unmarshallOptions { + /** + * Whether to return numbers as a string instead of converting them to native JavaScript numbers. + * This allows for the safe round-trip transport of numbers of arbitrary size. + */ + wrapNumbers?: boolean; +} + +/** + * Convert a DynamoDB record into a JavaScript object. + * + * @param {any} data - The DynamoDB record + * @param {unmarshallOptions} options - An optional configuration object for `unmarshall` + */ +export const unmarshall = ( + data: { [key: string]: AttributeValue }, + options?: unmarshallOptions +): { [key: string]: NativeAttributeValue } => + convertToNative({ M: data }, options) as { [key: string]: NativeAttributeValue }; From 27557e879577df5feed5b22254f4139f458f9895 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 24 Sep 2020 15:03:49 +0000 Subject: [PATCH 24/32] fix: do not throw for special numberic values like NaN/Infinity reason: user may only have read permission on DynamoDB values, and can't modify already stored values. --- packages/util-dynamodb/src/convertToNative.spec.ts | 6 ++---- packages/util-dynamodb/src/convertToNative.ts | 8 ++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index c366405360f7..5fd7fce9b80d 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -58,10 +58,8 @@ describe("convertToNative", () => { [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] .map((num) => num.toString()) .forEach((numString) => { - it(`throws for number (special numeric value): ${numString}`, () => { - expect(() => { - convertToNative({ ...emptyAttr, N: numString }); - }).toThrowError(`Special numeric value ${numString} is not allowed`); + it(`returns for number (special numeric value): ${numString}`, () => { + expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 28889e23fdd1..8622862e2e03 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -42,17 +42,13 @@ export const convertToNative = (data: AttributeValue, options?: unmarshallOption }; const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | string => { - const specialNumericValues = [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]; - if (specialNumericValues.map((num) => num.toString()).includes(numString)) { - throw new Error(`Special numeric value ${numString} is not allowed`); - } - if (options?.wrapNumbers) { return numString; } const num = Number(numString); - if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { + 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 { From 0d2dcb8dd3ec31f2dac3b84829e094d9ba93b7ac Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 24 Sep 2020 15:21:59 +0000 Subject: [PATCH 25/32] fix: use wrapper NumberValue instead of string reason: users will be able to differentiate between string and NumberValue while processing the returned value --- .../util-dynamodb/src/convertToNative.spec.ts | 21 +++++++----- packages/util-dynamodb/src/convertToNative.ts | 5 +-- packages/util-dynamodb/src/index.ts | 1 + packages/util-dynamodb/src/models.ts | 11 ++++++- packages/util-dynamodb/src/numberValue.ts | 33 +++++++++++++++++++ 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 packages/util-dynamodb/src/numberValue.ts diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 5fd7fce9b80d..4021068617a7 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -1,6 +1,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToNative } from "./convertToNative"; +import { NumberValue } from "./numberValue"; describe("convertToNative", () => { const emptyAttr = { @@ -39,8 +40,8 @@ describe("convertToNative", () => { it(`returns for number (integer): ${numString}`, () => { expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); }); - it(`returns string for number (integer) with options.wrapNumbers set: ${numString}`, () => { - expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(numString); + it(`returns NumberValue for number (integer) with options.wrapNumbers set: ${numString}`, () => { + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(new NumberValue(numString)); }); }); @@ -50,8 +51,8 @@ describe("convertToNative", () => { it(`returns for number (floating point): ${numString}`, () => { expect(convertToNative({ ...emptyAttr, N: numString })).toEqual(Number(numString)); }); - it(`returns string for number (floating point) with options.wrapNumbers set: ${numString}`, () => { - expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(numString); + it(`returns NumberValue for number (floating point) with options.wrapNumbers set: ${numString}`, () => { + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(new NumberValue(numString)); }); }); @@ -79,8 +80,8 @@ describe("convertToNative", () => { BigInt = BigIntConstructor; }); - it(`returns string for numbers outside SAFE_INTEGER range with options.wrapNumbers set: ${numString}`, () => { - expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(numString); + it(`returns NumberValue for numbers outside SAFE_INTEGER range with options.wrapNumbers set: ${numString}`, () => { + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(new NumberValue(numString)); }); }); @@ -164,7 +165,7 @@ describe("convertToNative", () => { 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) => item.N) + input.map((item) => new NumberValue(item.N)) ); }); }); @@ -214,7 +215,7 @@ describe("convertToNative", () => { it(`testing map with options.wrapNumbers`, () => { const input = { numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }; - const output = { numberKey: "1.01", bigintKey: "9007199254740996" }; + const output = { numberKey: new NumberValue("1.01"), bigintKey: new NumberValue("9007199254740996") }; expect( convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } }, { wrapNumbers: true }) ).toEqual(output); @@ -230,7 +231,9 @@ describe("convertToNative", () => { }); it("with options.wrapNumbers", () => { - expect(convertToNative({ ...emptyAttr, NS: input }, { wrapNumbers: true })).toEqual(new Set(input)); + expect(convertToNative({ ...emptyAttr, NS: input }, { wrapNumbers: true })).toEqual( + new Set(input.map((numString) => new NumberValue(numString))) + ); }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 8622862e2e03..27a0180cdfa1 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -1,6 +1,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { NativeAttributeValue } from "./models"; +import { NumberValue } from "./numberValue"; import { unmarshallOptions } from "./unmarshall"; /** @@ -41,9 +42,9 @@ export const convertToNative = (data: AttributeValue, options?: unmarshallOption throw new Error(`No value defined: ${data}`); }; -const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | string => { +const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | NumberValue => { if (options?.wrapNumbers) { - return numString; + return new NumberValue(numString); } const num = Number(numString); diff --git a/packages/util-dynamodb/src/index.ts b/packages/util-dynamodb/src/index.ts index 0d6f5d7852bd..c52cc3847181 100644 --- a/packages/util-dynamodb/src/index.ts +++ b/packages/util-dynamodb/src/index.ts @@ -2,4 +2,5 @@ export * from "./convertToAttr"; export * from "./convertToNative"; export * from "./marshall"; export * from "./models"; +export * from "./numberValue"; export * from "./unmarshall"; diff --git a/packages/util-dynamodb/src/models.ts b/packages/util-dynamodb/src/models.ts index 51bd8e40a61d..faa217d9c4f0 100644 --- a/packages/util-dynamodb/src/models.ts +++ b/packages/util-dynamodb/src/models.ts @@ -1,10 +1,19 @@ +import { NumberValue } from "./numberValue"; + export type NativeAttributeValue = | NativeScalarAttributeValue | { [key: string]: NativeAttributeValue } | NativeAttributeValue[] | Set; -export type NativeScalarAttributeValue = null | boolean | number | bigint | NativeAttributeBinary | string; +export type NativeScalarAttributeValue = + | null + | boolean + | number + | NumberValue + | bigint + | NativeAttributeBinary + | string; export type NativeAttributeBinary = | ArrayBuffer diff --git a/packages/util-dynamodb/src/numberValue.ts b/packages/util-dynamodb/src/numberValue.ts new file mode 100644 index 000000000000..f35d0f0abf25 --- /dev/null +++ b/packages/util-dynamodb/src/numberValue.ts @@ -0,0 +1,33 @@ +/** + * A class recognizable as a numeric value that stores the underlying number + * as a string. + * + * Intended to be a deserialization target for the DynamoDB Document Client when + * the `wrapNumbers` flag is set. This allows for numeric values that lose + * precision when converted to JavaScript's `number` type. + */ +export class NumberValue { + constructor(private readonly value: string) {} + + /** + * Render the underlying value as a number when converting to JSON. + */ + public toJSON() { + return this.toNumber(); + } + + /** + * Convert the underlying value to a JavaScript number. + */ + public toNumber() { + return Number(this.value); + } + + /** + * Return a string representing the unaltered value provided to the + * constructor. + */ + public toString() { + return this.value; + } +} From f4da61fc44bfa31d88a5427b2bfa7fa7e84fe05d Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 24 Sep 2020 15:45:48 +0000 Subject: [PATCH 26/32] fix: use simple interface NumberValue instead of class wrapper --- .../util-dynamodb/src/convertToNative.spec.ts | 13 ++++---- packages/util-dynamodb/src/convertToNative.ts | 5 ++- packages/util-dynamodb/src/index.ts | 1 - packages/util-dynamodb/src/models.ts | 12 ++++++- packages/util-dynamodb/src/numberValue.ts | 33 ------------------- 5 files changed, 19 insertions(+), 45 deletions(-) delete mode 100644 packages/util-dynamodb/src/numberValue.ts diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 4021068617a7..015bf50c03e3 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -1,7 +1,6 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToNative } from "./convertToNative"; -import { NumberValue } from "./numberValue"; describe("convertToNative", () => { const emptyAttr = { @@ -41,7 +40,7 @@ describe("convertToNative", () => { 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(new NumberValue(numString)); + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual({ value: numString }); }); }); @@ -52,7 +51,7 @@ describe("convertToNative", () => { 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(new NumberValue(numString)); + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual({ value: numString }); }); }); @@ -81,7 +80,7 @@ describe("convertToNative", () => { }); it(`returns NumberValue for numbers outside SAFE_INTEGER range with options.wrapNumbers set: ${numString}`, () => { - expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual(new NumberValue(numString)); + expect(convertToNative({ ...emptyAttr, N: numString }, { wrapNumbers })).toEqual({ value: numString }); }); }); @@ -165,7 +164,7 @@ describe("convertToNative", () => { 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) => new NumberValue(item.N)) + input.map((item) => ({ value: item.N })) ); }); }); @@ -215,7 +214,7 @@ describe("convertToNative", () => { it(`testing map with options.wrapNumbers`, () => { const input = { numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }; - const output = { numberKey: new NumberValue("1.01"), bigintKey: new NumberValue("9007199254740996") }; + const output = { numberKey: { value: "1.01" }, bigintKey: { value: "9007199254740996" } }; expect( convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } }, { wrapNumbers: true }) ).toEqual(output); @@ -232,7 +231,7 @@ describe("convertToNative", () => { it("with options.wrapNumbers", () => { expect(convertToNative({ ...emptyAttr, NS: input }, { wrapNumbers: true })).toEqual( - new Set(input.map((numString) => new NumberValue(numString))) + new Set(input.map((numString) => ({ value: numString }))) ); }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 27a0180cdfa1..dc8c15b0fbf4 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -1,7 +1,6 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; -import { NativeAttributeValue } from "./models"; -import { NumberValue } from "./numberValue"; +import { NativeAttributeValue, NumberValue } from "./models"; import { unmarshallOptions } from "./unmarshall"; /** @@ -44,7 +43,7 @@ export const convertToNative = (data: AttributeValue, options?: unmarshallOption const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | NumberValue => { if (options?.wrapNumbers) { - return new NumberValue(numString); + return { value: numString }; } const num = Number(numString); diff --git a/packages/util-dynamodb/src/index.ts b/packages/util-dynamodb/src/index.ts index c52cc3847181..0d6f5d7852bd 100644 --- a/packages/util-dynamodb/src/index.ts +++ b/packages/util-dynamodb/src/index.ts @@ -2,5 +2,4 @@ export * from "./convertToAttr"; export * from "./convertToNative"; export * from "./marshall"; export * from "./models"; -export * from "./numberValue"; export * from "./unmarshall"; diff --git a/packages/util-dynamodb/src/models.ts b/packages/util-dynamodb/src/models.ts index faa217d9c4f0..7c853a932ff7 100644 --- a/packages/util-dynamodb/src/models.ts +++ b/packages/util-dynamodb/src/models.ts @@ -1,4 +1,14 @@ -import { NumberValue } from "./numberValue"; +/** + * A interface recognizable as a numeric value that stores the underlying number + * as a string. + * + * Intended to be a deserialization target for the DynamoDB Document Client when + * the `wrapNumbers` flag is set. This allows for numeric values that lose + * precision when converted to JavaScript's `number` type. + */ +export interface NumberValue { + readonly value: string; +} export type NativeAttributeValue = | NativeScalarAttributeValue diff --git a/packages/util-dynamodb/src/numberValue.ts b/packages/util-dynamodb/src/numberValue.ts deleted file mode 100644 index f35d0f0abf25..000000000000 --- a/packages/util-dynamodb/src/numberValue.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * A class recognizable as a numeric value that stores the underlying number - * as a string. - * - * Intended to be a deserialization target for the DynamoDB Document Client when - * the `wrapNumbers` flag is set. This allows for numeric values that lose - * precision when converted to JavaScript's `number` type. - */ -export class NumberValue { - constructor(private readonly value: string) {} - - /** - * Render the underlying value as a number when converting to JSON. - */ - public toJSON() { - return this.toNumber(); - } - - /** - * Convert the underlying value to a JavaScript number. - */ - public toNumber() { - return Number(this.value); - } - - /** - * Return a string representing the unaltered value provided to the - * constructor. - */ - public toString() { - return this.value; - } -} From e5990548d5f21748fd97c5d095eab09c01fb7b3d Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Fri, 25 Sep 2020 16:18:50 +0000 Subject: [PATCH 27/32] fix: ts-ignore to ts-expect-error in tests --- .../util-dynamodb/src/convertToNative.spec.ts | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 015bf50c03e3..9810456ef838 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -119,45 +119,46 @@ describe("convertToNative", () => { const uint8Arr1 = new Uint8Array([...Array(4).keys()]); const uint8Arr2 = new Uint8Array([...Array(2).keys()]); [ - [ - [{ NULL: true }, { BOOL: false }], - [null, false], - ], - [ - [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }], - ["one", 1.01, BigInt(9007199254740996)], - ], - [ - [{ B: uint8Arr1 }, { B: uint8Arr2 }], - [uint8Arr1, uint8Arr2], - ], - [ - [ + { + 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"]), ], - ], - ].forEach(([input, output]) => { + }, + ].forEach(({ input, output }) => { it(`testing list: ${JSON.stringify(input)}`, () => { - expect(convertToNative({ ...emptyAttr, L: input as AttributeValue[] })).toEqual(output); + // @ts-expect-error Bug with complex types in TS https://github.com/microsoft/TypeScript/issues/40770 + expect(convertToNative({ ...emptyAttr, L: input })).toEqual(output); }); }); @@ -173,51 +174,50 @@ describe("convertToNative", () => { const uint8Arr1 = new Uint8Array([...Array(4).keys()]); const uint8Arr2 = new Uint8Array([...Array(2).keys()]); [ - [ - { nullKey: { NULL: true }, boolKey: { BOOL: false } }, - { nullKey: null, boolKey: false }, - ], - [ - { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }, - { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, - ], - [ - { uint8Arr1Key: { B: uint8Arr1 }, uint8Arr2Key: { B: uint8Arr2 } }, - { uint8Arr1Key: uint8Arr1, uint8Arr2Key: uint8Arr2 }, - ], - [ - { + { + 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" }] }, }, - { list1: [null, false], list2: ["one", 1.01, BigInt(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"]), }, - ], - ].forEach(([input, output]) => { + }, + ].forEach(({ input, output }) => { it(`testing map: ${input}`, () => { - expect(convertToNative({ ...emptyAttr, M: input as { [key: string]: AttributeValue } })).toEqual(output); + // @ts-expect-error Bug with complex types in TS https://github.com/microsoft/TypeScript/issues/40770 + 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 as { [key: string]: AttributeValue } }, { wrapNumbers: true }) - ).toEqual(output); + expect(convertToNative({ ...emptyAttr, M: input }, { wrapNumbers: true })).toEqual(output); }); }); From 5872c650d78b0219f1adfa8584e637b22894b047 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Fri, 25 Sep 2020 23:20:30 +0000 Subject: [PATCH 28/32] chore: remove usage of ts-expect-error --- .../util-dynamodb/src/convertToNative.spec.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 9810456ef838..38689276ae69 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -1,6 +1,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToNative } from "./convertToNative"; +import { NativeAttributeValue } from "./models"; describe("convertToNative", () => { const emptyAttr = { @@ -118,7 +119,7 @@ describe("convertToNative", () => { describe("list", () => { const uint8Arr1 = new Uint8Array([...Array(4).keys()]); const uint8Arr2 = new Uint8Array([...Array(2).keys()]); - [ + ([ { input: [{ NULL: true }, { BOOL: false }], output: [null, false], @@ -155,9 +156,8 @@ describe("convertToNative", () => { new Set(["one", "two", "three"]), ], }, - ].forEach(({ input, output }) => { + ] as { input: AttributeValue[]; output: NativeAttributeValue[] }[]).forEach(({ input, output }) => { it(`testing list: ${JSON.stringify(input)}`, () => { - // @ts-expect-error Bug with complex types in TS https://github.com/microsoft/TypeScript/issues/40770 expect(convertToNative({ ...emptyAttr, L: input })).toEqual(output); }); }); @@ -173,7 +173,7 @@ describe("convertToNative", () => { 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 }, @@ -207,12 +207,13 @@ describe("convertToNative", () => { stringSet: new Set(["one", "two", "three"]), }, }, - ].forEach(({ input, output }) => { - it(`testing map: ${input}`, () => { - // @ts-expect-error Bug with complex types in TS https://github.com/microsoft/TypeScript/issues/40770 - expect(convertToNative({ ...emptyAttr, M: input })).toEqual(output); - }); - }); + ] 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" } }; From 69756b304756a9e57ed6a72e9de79d591e67e29d Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 28 Sep 2020 18:17:08 +0000 Subject: [PATCH 29/32] fix: unresolved conflicts in convertToAttr.spec.ts --- .../util-dynamodb/src/convertToAttr.spec.ts | 110 ------------------ 1 file changed, 110 deletions(-) diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index 46194529c3bc..5090ddad22c5 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -41,7 +41,6 @@ describe("convertToAttr", () => { [Number.MAX_SAFE_INTEGER + 1, Number.MAX_VALUE].forEach((num) => { it(`throws for number greater than Number.MAX_SAFE_INTEGER: ${num}`, () => { -<<<<<<< HEAD const errorPrefix = `Number ${num} is greater than Number.MAX_SAFE_INTEGER.`; expect(() => { @@ -54,17 +53,11 @@ describe("convertToAttr", () => { convertToAttr(num); }).toThrowError(`${errorPrefix} Pass string value instead.`); BigInt = BigIntConstructor; -======= - expect(() => { - convertToAttr(num); - }).toThrowError(`Number ${num} is greater than Number.MAX_SAFE_INTEGER. Use BigInt.`); ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); }); [Number.MIN_SAFE_INTEGER - 1].forEach((num) => { it(`throws for number lesser than Number.MIN_SAFE_INTEGER: ${num}`, () => { -<<<<<<< HEAD const errorPrefix = `Number ${num} is lesser than Number.MIN_SAFE_INTEGER.`; expect(() => { @@ -77,11 +70,6 @@ describe("convertToAttr", () => { convertToAttr(num); }).toThrowError(`${errorPrefix} Pass string value instead.`); BigInt = BigIntConstructor; -======= - expect(() => { - convertToAttr(num); - }).toThrowError(`Number ${num} is lesser than Number.MIN_SAFE_INTEGER. Use BigInt.`); ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); }); }); @@ -109,10 +97,6 @@ describe("convertToAttr", () => { const buffer = new ArrayBuffer(64); const arr = [...Array(64).keys()]; const addPointOne = (num: number) => num + 0.1; -<<<<<<< HEAD - -======= ->>>>>>> chore: temporary commit for future util-dynamodb PRs [ buffer, new Blob([new Uint8Array(buffer)]), @@ -134,20 +118,16 @@ describe("convertToAttr", () => { expect(convertToAttr(data)).toEqual({ B: data }); }); }); -<<<<<<< HEAD it("returns null for Binary when options.convertEmptyValues=true", () => { expect(convertToAttr(new Uint8Array(), { convertEmptyValues: true })).toEqual({ NULL: true }); }); -======= ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); describe("list", () => { const arr = [...Array(4).keys()]; const uint8Arr = new Uint32Array(arr); const biguintArr = new BigUint64Array(arr.map(BigInt)); -<<<<<<< HEAD ([ { @@ -198,27 +178,6 @@ describe("convertToAttr", () => { L: [{ NULL: true }, { NULL: true }, { NULL: true }], }); }); -======= - [ - [ - [null, false], - [{ NULL: true }, { BOOL: false }], - ], - [ - [1.01, BigInt(1), "one"], - [{ N: "1.01" }, { N: "1" }, { S: "one" }], - ], - [ - [uint8Arr, biguintArr], - [{ B: uint8Arr }, { B: biguintArr }], - ], - ].forEach(([input, output]) => { - it(`testing list: ${input}`, () => { - // @ts-ignore - expect(convertToAttr(input)).toEqual({ L: output }); - }); - }); ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); describe("set", () => { @@ -229,12 +188,8 @@ describe("convertToAttr", () => { it("bigint set", () => { // @ts-expect-error BigInt literals are not available when targeting lower than ES2020. -<<<<<<< HEAD const bigNum = BigInt(Number.MAX_SAFE_INTEGER) + 2n; const set = new Set([bigNum, -bigNum]); -======= - const set = new Set([1n, 2n, 3n]); ->>>>>>> chore: temporary commit for future util-dynamodb PRs expect(convertToAttr(set)).toEqual({ NS: Array.from(set).map((num) => num.toString()) }); }); @@ -248,7 +203,6 @@ describe("convertToAttr", () => { expect(convertToAttr(set)).toEqual({ SS: Array.from(set) }); }); -<<<<<<< HEAD it("returns null for empty set for options.convertEmptyValues=true", () => { expect(convertToAttr(new Set([]), { convertEmptyValues: true })).toEqual({ NULL: true }); }); @@ -257,20 +211,11 @@ describe("convertToAttr", () => { expect(() => { convertToAttr(new Set([])); }).toThrowError(`Please pass a non-empty set, or set convertEmptyValues to true.`); -======= - it("throws error for empty set", () => { - expect(() => { - convertToAttr(new Set()); - }).toThrowError(`Please pass a non-empty set`); ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); it("thows error for unallowed set", () => { expect(() => { -<<<<<<< HEAD // @ts-expect-error Type 'Set' is not assignable -======= ->>>>>>> chore: temporary commit for future util-dynamodb PRs convertToAttr(new Set([true, false])); }).toThrowError(`Only Number Set (NS), Binary Set (BS) or String Set (SS) are allowed.`); }); @@ -280,7 +225,6 @@ describe("convertToAttr", () => { const arr = [...Array(4).keys()]; const uint8Arr = new Uint32Array(arr); const biguintArr = new BigUint64Array(arr.map(BigInt)); -<<<<<<< HEAD ([ { @@ -331,24 +275,6 @@ describe("convertToAttr", () => { const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) }; expect(convertToAttr(input, { convertEmptyValues: true })).toEqual({ M: { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } }, -======= - [ - [ - { a: null, b: false }, - { a: { NULL: true }, b: { BOOL: false } }, - ], - [ - { a: 1.01, b: BigInt(1), c: "one" }, - { a: { N: "1.01" }, b: { N: "1" }, c: { S: "one" } }, - ], - [ - { a: uint8Arr, b: biguintArr }, - { a: { B: uint8Arr }, b: { B: biguintArr } }, - ], - ].forEach(([input, output]) => { - it(`testing map: ${input}`, () => { - expect(convertToAttr(input)).toEqual({ M: output }); ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); }); }); @@ -359,18 +285,14 @@ describe("convertToAttr", () => { expect(convertToAttr(str)).toEqual({ S: str }); }); }); -<<<<<<< HEAD it("returns null for string when options.convertEmptyValues=true", () => { expect(convertToAttr("", { convertEmptyValues: true })).toEqual({ NULL: true }); }); -======= ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); describe(`unsupported type`, () => { class FooObj { -<<<<<<< HEAD constructor(private readonly foo: string) {} } @@ -379,41 +301,9 @@ describe("convertToAttr", () => { it(`throws for: ${String(data)}`, () => { expect(() => { // @ts-expect-error Argument is not assignable to parameter of type 'NativeAttributeValue' -======= - constructor() { - // @ts-ignore - this.foo = "foo"; - } - } - - // ToDo: Serialize ES6 class objects as string https://github.com/aws/aws-sdk-js-v3/issues/1535 - [undefined, new Date(), new FooObj()].forEach((data) => { - it(`throws for: ${String(data)}`, () => { - expect(() => { - // @ts-ignore Argument is not assignable to parameter of type 'NativeAttributeValue' ->>>>>>> chore: temporary commit for future util-dynamodb PRs convertToAttr(data); }).toThrowError(`Unsupported type passed: ${String(data)}`); }); }); }); -<<<<<<< HEAD -======= - - describe("convertEmptyValues set to true", () => { - const convertEmptyValues = true; - - it(`returns null for Set`, () => { - expect(convertToAttr(new Set(), { convertEmptyValues })).toEqual({ NULL: true }); - }); - - it(`returns null for String`, () => { - expect(convertToAttr("", { convertEmptyValues })).toEqual({ NULL: true }); - }); - - it(`returns null for Binary`, () => { - expect(convertToAttr(new Uint8Array(), { convertEmptyValues })).toEqual({ NULL: true }); - }); - }); ->>>>>>> chore: temporary commit for future util-dynamodb PRs }); From 288d1789510ca76fb5329f8eeba389d9abd9ed47 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 28 Sep 2020 18:24:29 +0000 Subject: [PATCH 30/32] fix: add NumberValue in Set type --- packages/util-dynamodb/src/models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/util-dynamodb/src/models.ts b/packages/util-dynamodb/src/models.ts index 7c853a932ff7..831488c6bb44 100644 --- a/packages/util-dynamodb/src/models.ts +++ b/packages/util-dynamodb/src/models.ts @@ -14,7 +14,7 @@ export type NativeAttributeValue = | NativeScalarAttributeValue | { [key: string]: NativeAttributeValue } | NativeAttributeValue[] - | Set; + | Set; export type NativeScalarAttributeValue = | null From a1a79bebf705c56a5d7b94bf51d7513fc259105a Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 28 Sep 2020 18:25:56 +0000 Subject: [PATCH 31/32] chore: add removed newline from convertToAttr.spec.ts while resolving conflicts --- packages/util-dynamodb/src/convertToAttr.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index 5090ddad22c5..501eeeadfda9 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -97,6 +97,7 @@ describe("convertToAttr", () => { const buffer = new ArrayBuffer(64); const arr = [...Array(64).keys()]; const addPointOne = (num: number) => num + 0.1; + [ buffer, new Blob([new Uint8Array(buffer)]), From 1c8a9d6008725990aee5ac6da5134c04ff03dfaf Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 28 Sep 2020 20:12:15 +0000 Subject: [PATCH 32/32] docs: add unmarshall to README.md --- packages/util-dynamodb/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/util-dynamodb/README.md b/packages/util-dynamodb/README.md index 8f0798fc4d87..634c1853a82f 100644 --- a/packages/util-dynamodb/README.md +++ b/packages/util-dynamodb/README.md @@ -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); +```