From 5cd5f09972236a1f137e0d49f0804b644787ece3 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 9 Feb 2021 07:59:04 +0000 Subject: [PATCH 1/4] feat(util-dynamodb): marshall JavaScript Maps --- packages/util-dynamodb/src/convertToAttr.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/util-dynamodb/src/convertToAttr.ts b/packages/util-dynamodb/src/convertToAttr.ts index 639d2f718024..b2bd10490b46 100644 --- a/packages/util-dynamodb/src/convertToAttr.ts +++ b/packages/util-dynamodb/src/convertToAttr.ts @@ -18,6 +18,8 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti return convertToListAttr(data, options); } else if (data?.constructor?.name === "Set") { return convertToSetAttr(data as Set, options); + } else if (data?.constructor?.name === "Map") { + return convertMapToMapAttr(data as Map, options); } else if ( data?.constructor?.name === "Object" || // for object which is result of Object.create(null), which doesn't have constructor defined @@ -105,6 +107,21 @@ const convertToSetAttr = ( } }; +const convertMapToMapAttr = ( + data: Map, + options?: marshallOptions +): { M: { [key: string]: AttributeValue } } => ({ + M: (function getMapFromIterable(data) { + const map: { [key: string]: AttributeValue } = {}; + for (const [key, value] of data) { + if (typeof value !== "function" && (value !== undefined || !options?.removeUndefinedValues)) { + map[key] = convertToAttr(value, options); + } + } + return map; + })(data), +}); + const convertToMapAttr = ( data: { [key: string]: NativeAttributeValue }, options?: marshallOptions From 701f9a155285899bd987aa1298ddbf7924f5a79c Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 9 Feb 2021 08:01:59 +0000 Subject: [PATCH 2/4] chore(util-dynamodb): rename internal map marshall functions --- packages/util-dynamodb/src/convertToAttr.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/util-dynamodb/src/convertToAttr.ts b/packages/util-dynamodb/src/convertToAttr.ts index b2bd10490b46..70adf8665b50 100644 --- a/packages/util-dynamodb/src/convertToAttr.ts +++ b/packages/util-dynamodb/src/convertToAttr.ts @@ -19,13 +19,13 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti } else if (data?.constructor?.name === "Set") { return convertToSetAttr(data as Set, options); } else if (data?.constructor?.name === "Map") { - return convertMapToMapAttr(data as Map, options); + return convertToMapAttrFromIterable(data as Map, options); } else if ( data?.constructor?.name === "Object" || // for object which is result of Object.create(null), which doesn't have constructor defined (!data.constructor && typeof data === "object") ) { - return convertToMapAttr(data as { [key: string]: NativeAttributeValue }, options); + return convertToMapAttrFromEnumerableProps(data as { [key: string]: NativeAttributeValue }, options); } else if (isBinary(data)) { if (data.length === 0 && options?.convertEmptyValues) { return convertToNullAttr(); @@ -45,7 +45,7 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti } return convertToStringAttr(data); } else if (options?.convertClassInstanceToMap && typeof data === "object") { - return convertToMapAttr(data as { [key: string]: NativeAttributeValue }, options); + return convertToMapAttrFromEnumerableProps(data as { [key: string]: NativeAttributeValue }, options); } throw new Error( `Unsupported type passed: ${data}. Pass options.convertClassInstanceToMap=true to marshall typeof object as map attribute.` @@ -107,11 +107,11 @@ const convertToSetAttr = ( } }; -const convertMapToMapAttr = ( +const convertToMapAttrFromIterable = ( data: Map, options?: marshallOptions ): { M: { [key: string]: AttributeValue } } => ({ - M: (function getMapFromIterable(data) { + M: ((data) => { const map: { [key: string]: AttributeValue } = {}; for (const [key, value] of data) { if (typeof value !== "function" && (value !== undefined || !options?.removeUndefinedValues)) { @@ -122,11 +122,11 @@ const convertMapToMapAttr = ( })(data), }); -const convertToMapAttr = ( +const convertToMapAttrFromEnumerableProps = ( data: { [key: string]: NativeAttributeValue }, options?: marshallOptions ): { M: { [key: string]: AttributeValue } } => ({ - M: (function getMapFromEnurablePropsInPrototypeChain(data) { + M: ((data) => { const map: { [key: string]: AttributeValue } = {}; for (const key in data) { const value = data[key]; From 35bb27f7e1b4e340377eee621edd06c74c8dcdfd Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 9 Feb 2021 08:10:09 +0000 Subject: [PATCH 3/4] test: move useObjectCreate closer to object tests --- .../util-dynamodb/src/convertToAttr.spec.ts | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index 1d2c1c6d9a71..8b86bd2d3747 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -306,62 +306,66 @@ describe("convertToAttr", () => { const uint8Arr = new Uint32Array(arr); const biguintArr = new BigUint64Array(arr.map(BigInt)); - [true, false].forEach((useObjectCreate) => { - ([ - { - input: { nullKey: null, boolKey: false }, - output: { nullKey: { NULL: true }, boolKey: { BOOL: false } }, - }, - { - input: { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, - output: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }, + ([ + { + input: { nullKey: null, boolKey: false }, + output: { nullKey: { NULL: true }, boolKey: { BOOL: false } }, + }, + { + input: { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, + output: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } }, + }, + { + input: { uint8ArrKey: uint8Arr, biguintArrKey: biguintArr }, + output: { uint8ArrKey: { B: uint8Arr }, biguintArrKey: { B: biguintArr } }, + }, + { + input: { + list1: [null, false], + list2: ["one", 1.01, BigInt(9007199254740996)], }, - { - input: { uint8ArrKey: uint8Arr, biguintArrKey: biguintArr }, - output: { uint8ArrKey: { B: uint8Arr }, biguintArrKey: { B: biguintArr } }, + output: { + list1: { L: [{ NULL: true }, { BOOL: false }] }, + list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] }, }, - { - input: { - list1: [null, false], - list2: ["one", 1.01, BigInt(9007199254740996)], - }, - output: { - list1: { L: [{ NULL: true }, { BOOL: false }] }, - list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] }, - }, + }, + { + input: { + numberSet: new Set([1, 2, 3]), + bigintSet: new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]), + binarySet: new Set([uint8Arr, biguintArr]), + stringSet: new Set(["one", "two", "three"]), }, - { - input: { - numberSet: new Set([1, 2, 3]), - bigintSet: new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]), - binarySet: new Set([uint8Arr, biguintArr]), - stringSet: new Set(["one", "two", "three"]), - }, - output: { - numberSet: { NS: ["1", "2", "3"] }, - bigintSet: { NS: ["9007199254740996", "-9007199254740996"] }, - binarySet: { BS: [uint8Arr, biguintArr] }, - stringSet: { SS: ["one", "two", "three"] }, - }, + output: { + numberSet: { NS: ["1", "2", "3"] }, + bigintSet: { NS: ["9007199254740996", "-9007199254740996"] }, + binarySet: { BS: [uint8Arr, biguintArr] }, + stringSet: { SS: ["one", "two", "three"] }, }, - ] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach( - ({ input, output }) => { + }, + ] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach( + ({ input, output }) => { + [true, false].forEach((useObjectCreate) => { const inputObject = useObjectCreate ? Object.create(input) : input; - it(`testing map: ${inputObject}`, () => { + it(`testing object: ${inputObject}`, () => { expect(convertToAttr(inputObject)).toEqual({ M: output }); }); - } - ); + }); + } + ); - it(`testing map with options.convertEmptyValues=true`, () => { + [true, false].forEach((useObjectCreate) => { + it(`testing object with options.convertEmptyValues=true`, () => { const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) }; const inputObject = useObjectCreate ? Object.create(input) : input; expect(convertToAttr(inputObject, { convertEmptyValues: true })).toEqual({ M: { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } }, }); }); + }); - describe(`testing map with options.removeUndefinedValues`, () => { + [true, false].forEach((useObjectCreate) => { + describe(`testing object with options.removeUndefinedValues`, () => { describe("throws error", () => { const testErrorMapWithUndefinedValues = (useObjectCreate: boolean, options?: marshallOptions) => { const input = { definedKey: "definedKey", undefinedKey: undefined }; @@ -432,7 +436,6 @@ describe("convertToAttr", () => { }).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`); }); - // ToDo: Serialize ES6 class objects as string https://github.com/aws/aws-sdk-js-v3/issues/1535 [new Date(), new FooClass("foo")].forEach((data) => { it(`throws for: ${String(data)}`, () => { expect(() => { From 6161f05633e6cd0d07003abb8e81b192c5e0ec12 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 9 Feb 2021 08:37:42 +0000 Subject: [PATCH 4/4] test: marshalling JavaScript maps --- .../util-dynamodb/src/convertToAttr.spec.ts | 99 +++++++++++++------ 1 file changed, 70 insertions(+), 29 deletions(-) diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index 8b86bd2d3747..dbc1e7b628ae 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -347,64 +347,105 @@ describe("convertToAttr", () => { ({ input, output }) => { [true, false].forEach((useObjectCreate) => { const inputObject = useObjectCreate ? Object.create(input) : input; - it(`testing object: ${inputObject}`, () => { + it(`testing object: ${inputObject}${useObjectCreate && " with Object.create()"}`, () => { expect(convertToAttr(inputObject)).toEqual({ M: output }); }); }); + + const inputMap = new Map(Object.entries(input)); + it(`testing map: ${inputMap}`, () => { + expect(convertToAttr(inputMap)).toEqual({ M: output }); + }); } ); - [true, false].forEach((useObjectCreate) => { - it(`testing object with options.convertEmptyValues=true`, () => { - const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) }; + describe(`with options.convertEmptyValues=true`, () => { + const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) }; + const output = { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } }; + + [true, false].forEach((useObjectCreate) => { const inputObject = useObjectCreate ? Object.create(input) : input; - expect(convertToAttr(inputObject, { convertEmptyValues: true })).toEqual({ - M: { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } }, + it(`testing object${useObjectCreate && " with Object.create()"}`, () => { + expect(convertToAttr(inputObject, { convertEmptyValues: true })).toEqual({ M: output }); }); }); + + const inputMap = new Map(Object.entries(input)); + it(`testing map`, () => { + expect(convertToAttr(inputMap, { convertEmptyValues: true })).toEqual({ M: output }); + }); }); - [true, false].forEach((useObjectCreate) => { - describe(`testing object with options.removeUndefinedValues`, () => { - describe("throws error", () => { - const testErrorMapWithUndefinedValues = (useObjectCreate: boolean, options?: marshallOptions) => { - const input = { definedKey: "definedKey", undefinedKey: undefined }; - const inputObject = useObjectCreate ? Object.create(input) : input; - expect(() => { - convertToAttr(inputObject, options); - }).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`); - }; + describe(`with options.removeUndefinedValues=true`, () => { + describe("throws error", () => { + const testErrorMapWithUndefinedValues = (input: any, options?: marshallOptions) => { + expect(() => { + convertToAttr(input, options); + }).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`); + }; - [undefined, {}, { convertEmptyValues: false }].forEach((options) => { - it(`when options=${options}`, () => { - testErrorMapWithUndefinedValues(useObjectCreate, options); + [undefined, {}, { convertEmptyValues: false }].forEach((options) => { + const input = { definedKey: "definedKey", undefinedKey: undefined }; + [true, false].forEach((useObjectCreate) => { + const inputObject = useObjectCreate ? Object.create(input) : input; + it(`testing object${useObjectCreate && " with Object.create()"} when options=${options}`, () => { + testErrorMapWithUndefinedValues(inputObject, options); }); }); + + const inputMap = new Map(Object.entries(input)); + it(`testing map when options=${options}`, () => { + testErrorMapWithUndefinedValues(inputMap, options); + }); }); + }); - it(`returns when options.removeUndefinedValues=true`, () => { - const input = { definedKey: "definedKey", undefinedKey: undefined }; + describe(`returns when options.removeUndefinedValues=true`, () => { + const input = { definedKey: "definedKey", undefinedKey: undefined }; + const output = { definedKey: { S: "definedKey" } }; + [true, false].forEach((useObjectCreate) => { const inputObject = useObjectCreate ? Object.create(input) : input; - expect(convertToAttr(inputObject, { removeUndefinedValues: true })).toEqual({ - M: { definedKey: { S: "definedKey" } }, + it(`testing object${useObjectCreate && " with Object.create()"}`, () => { + expect(convertToAttr(inputObject, { removeUndefinedValues: true })).toEqual({ M: output }); }); }); + + const inputMap = new Map(Object.entries(input)); + it(`testing map`, () => { + expect(convertToAttr(inputMap, { removeUndefinedValues: true })).toEqual({ M: output }); + }); }); }); - it(`testing Object.create with function`, () => { - const person = { - isHuman: true, - printIntroduction: function () { - console.log(`Am I human? ${this.isHuman}`); + describe(`testing with function`, () => { + const input = { + bool: true, + func: function () { + console.log(`bool: ${this.bool}`); }, }; - expect(convertToAttr(Object.create(person))).toEqual({ M: { isHuman: { BOOL: true } } }); + const output = { bool: { BOOL: true } }; + + [true, false].forEach((useObjectCreate) => { + const inputObject = useObjectCreate ? Object.create(input) : input; + it(`testing object${useObjectCreate && " with Object.create()"}`, () => { + expect(convertToAttr(inputObject)).toEqual({ M: output }); + }); + }); + + const inputMap = new Map(Object.entries(input)); + it(`testing map`, () => { + expect(convertToAttr(inputMap)).toEqual({ M: output }); + }); }); it(`testing Object.create(null)`, () => { expect(convertToAttr(Object.create(null))).toEqual({ M: {} }); }); + + it(`testing empty Map`, () => { + expect(convertToAttr(new Map())).toEqual({ M: {} }); + }); }); describe("string", () => {