From a008d23fcd66f5d22ab00e14428309f7fc6f868a Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 8 Feb 2021 22:06:08 -0800 Subject: [PATCH] feat(util-dynamodb): support marshalling for Object.create (#1974) --- .../util-dynamodb/src/convertToAttr.spec.ts | 175 ++++++++++++------ packages/util-dynamodb/src/convertToAttr.ts | 28 +-- 2 files changed, 129 insertions(+), 74 deletions(-) diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index 9e9febdc5b52..1d2c1c6d9a71 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -306,80 +306,101 @@ describe("convertToAttr", () => { const uint8Arr = new Uint32Array(arr); const biguintArr = new BigUint64Array(arr.map(BigInt)); - ([ - { - 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)], + [true, false].forEach((useObjectCreate) => { + ([ + { + input: { nullKey: null, boolKey: false }, + output: { nullKey: { NULL: true }, boolKey: { BOOL: false } }, }, - output: { - list1: { L: [{ NULL: true }, { BOOL: false }] }, - list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] }, + { + input: { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) }, + output: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { 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: { uint8ArrKey: uint8Arr, biguintArrKey: biguintArr }, + output: { uint8ArrKey: { B: uint8Arr }, biguintArrKey: { B: biguintArr } }, + }, + { + 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" }] }, + }, }, - output: { - numberSet: { NS: ["1", "2", "3"] }, - bigintSet: { NS: ["9007199254740996", "-9007199254740996"] }, - binarySet: { BS: [uint8Arr, biguintArr] }, - stringSet: { SS: ["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"] }, + }, }, - }, - ] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach( - ({ input, output }) => { - it(`testing map: ${input}`, () => { - expect(convertToAttr(input)).toEqual({ M: output }); + ] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach( + ({ input, output }) => { + const inputObject = useObjectCreate ? Object.create(input) : input; + it(`testing map: ${inputObject}`, () => { + expect(convertToAttr(inputObject)).toEqual({ M: output }); + }); + } + ); + + it(`testing map 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 } }, }); - } - ); - - it(`testing map with options.convertEmptyValues=true`, () => { - 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 } }, }); - }); - - describe(`testing map with options.removeUndefinedValues`, () => { - describe("throws error", () => { - const testErrorMapWithUndefinedValues = (options?: marshallOptions) => { - expect(() => { - convertToAttr({ definedKey: "definedKey", undefinedKey: undefined }, options); - }).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`); - }; - [undefined, {}, { convertEmptyValues: false }].forEach((options) => { - it(`when options=${options}`, () => { - testErrorMapWithUndefinedValues(options); + describe(`testing map 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.`); + }; + + [undefined, {}, { convertEmptyValues: false }].forEach((options) => { + it(`when options=${options}`, () => { + testErrorMapWithUndefinedValues(useObjectCreate, options); + }); }); }); - }); - it(`returns when options.removeUndefinedValues=true`, () => { - const input = { definedKey: "definedKey", undefinedKey: undefined }; - expect(convertToAttr(input, { removeUndefinedValues: true })).toEqual({ - M: { definedKey: { S: "definedKey" } }, + it(`returns when options.removeUndefinedValues=true`, () => { + const input = { definedKey: "definedKey", undefinedKey: undefined }; + const inputObject = useObjectCreate ? Object.create(input) : input; + expect(convertToAttr(inputObject, { removeUndefinedValues: true })).toEqual({ + M: { definedKey: { S: "definedKey" } }, + }); }); }); }); + + it(`testing Object.create with function`, () => { + const person = { + isHuman: true, + printIntroduction: function () { + console.log(`Am I human? ${this.isHuman}`); + }, + }; + expect(convertToAttr(Object.create(person))).toEqual({ M: { isHuman: { BOOL: true } } }); + }); + + it(`testing Object.create(null)`, () => { + expect(convertToAttr(Object.create(null))).toEqual({ M: {} }); + }); }); describe("string", () => { @@ -438,6 +459,9 @@ describe("convertToAttr", () => { private readonly listAttr: any[], private readonly mapAttr: { [key: string]: any } ) {} + public exampleMethod() { + return "This method won't be marshalled"; + } } expect( convertToAttr( @@ -476,6 +500,35 @@ describe("convertToAttr", () => { }); }); + it("returns inherited values from parent class in map", () => { + class Person { + protected name: string; + constructor(name: string) { + this.name = name; + } + } + + class Employee extends Person { + private department: string; + + constructor(name: string, department: string) { + super(name); + this.department = department; + } + + public getElevatorPitch() { + return `Hello, my name is ${this.name} and I work in ${this.department}.`; + } + } + + expect(convertToAttr(new Employee("John", "Sales"), { convertClassInstanceToMap: true })).toEqual({ + M: { + name: { S: "John" }, + department: { S: "Sales" }, + }, + }); + }); + it("returns empty for Date object", () => { expect(convertToAttr(new Date(), { convertClassInstanceToMap: true })).toEqual({ M: {} }); }); diff --git a/packages/util-dynamodb/src/convertToAttr.ts b/packages/util-dynamodb/src/convertToAttr.ts index 11efd932b2ac..639d2f718024 100644 --- a/packages/util-dynamodb/src/convertToAttr.ts +++ b/packages/util-dynamodb/src/convertToAttr.ts @@ -18,7 +18,11 @@ 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 === "Object") { + } 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); } else if (isBinary(data)) { if (data.length === 0 && options?.convertEmptyValues) { @@ -105,18 +109,16 @@ const convertToMapAttr = ( data: { [key: string]: NativeAttributeValue }, options?: marshallOptions ): { M: { [key: string]: AttributeValue } } => ({ - M: Object.entries(data) - .filter( - ([key, value]: [string, NativeAttributeValue]) => - !options?.removeUndefinedValues || (options?.removeUndefinedValues && value !== undefined) - ) - .reduce( - (acc: { [key: string]: AttributeValue }, [key, value]: [string, NativeAttributeValue]) => ({ - ...acc, - [key]: convertToAttr(value, options), - }), - {} - ), + M: (function getMapFromEnurablePropsInPrototypeChain(data) { + const map: { [key: string]: AttributeValue } = {}; + for (const key in data) { + const value = data[key]; + if (typeof value !== "function" && (value !== undefined || !options?.removeUndefinedValues)) { + map[key] = convertToAttr(value, options); + } + } + return map; + })(data), }); // For future-proofing: this functions are called from multiple places