diff --git a/lib/lib-dynamodb/src/commands/marshallInput.spec.ts b/lib/lib-dynamodb/src/commands/marshallInput.spec.ts index c6df6f96e4e43..f002bad71276b 100644 --- a/lib/lib-dynamodb/src/commands/marshallInput.spec.ts +++ b/lib/lib-dynamodb/src/commands/marshallInput.spec.ts @@ -10,6 +10,15 @@ describe("marshallInput and processObj", () => { } ); }); + + it("marshallInput should ignore function properties", () => { + const input = { Items: [() => {}, 1, "test"] }; + const inputKeyNodes = { Items: null }; + const output = { Items: { L: [{ N: "1" }, { S: "test" }] } }; + expect( + marshallInput(input, inputKeyNodes, { convertTopLevelContainer: true, convertClassInstanceToMap: true }) + ).toEqual(output); + }); }); describe("marshallInput for commands", () => { diff --git a/lib/lib-dynamodb/src/commands/utils.spec.ts b/lib/lib-dynamodb/src/commands/utils.spec.ts index dad1cfa939f8d..f9a50a2ff8c5e 100644 --- a/lib/lib-dynamodb/src/commands/utils.spec.ts +++ b/lib/lib-dynamodb/src/commands/utils.spec.ts @@ -146,3 +146,94 @@ describe("utils", () => { }); }); }); + +describe("object with function property", () => { + const notAttrValue = { NotAttrValue: "NotAttrValue" }; + const keyNodes = { Item: {} }; + const nativeAttrObj = { Item: { id: 1, func: () => {} }, ...notAttrValue }; + const attrObj = { Item: { id: { N: "1" } }, ...notAttrValue }; + it("should remove functions", () => { + expect( + marshallInput(nativeAttrObj, keyNodes, { convertTopLevelContainer: true, convertClassInstanceToMap: true }) + ).toEqual(attrObj); + }); + + // List of functions + const listOfFunctions = { Item: { id: 1, funcs: [() => {}, () => {}] }, ...notAttrValue }; + it("should remove functions from lists", () => { + expect( + marshallInput(listOfFunctions, keyNodes, { convertTopLevelContainer: true, convertClassInstanceToMap: true }) + ).toEqual({ Item: { id: { N: "1" }, funcs: { L: [] } }, ...notAttrValue }); + }); + + // Nested list of functions + const nestedListOfFunctions = { + Item: { + id: 1, + funcs: [ + [() => {}, () => {}], + [() => {}, () => {}], + ], + }, + ...notAttrValue, + }; + it("should remove functions from nested lists", () => { + expect( + marshallInput(nestedListOfFunctions, keyNodes, { + convertTopLevelContainer: true, + convertClassInstanceToMap: true, + }) + ).toEqual({ Item: { id: { N: "1" }, funcs: { L: [{ L: [] }, { L: [] }] } }, ...notAttrValue }); + }); + + // Nested list of functions 3 levels down + const nestedListOfFunctions3Levels = { + Item: { + id: 1, + funcs: [ + [ + [() => {}, () => {}], + [() => {}, () => {}], + ], + [ + [() => {}, () => {}], + [() => {}, () => {}], + ], + ], + }, + ...notAttrValue, + }; + + it("should remove functions from a nested list of depth 3", () => { + expect( + marshallInput(nestedListOfFunctions3Levels, keyNodes, { + convertTopLevelContainer: true, + convertClassInstanceToMap: true, + }) + ).toEqual({ + Item: { + id: { N: "1" }, + funcs: { + L: [ + { + L: [{ L: [] }, { L: [] }], + }, + { + L: [{ L: [] }, { L: [] }], + }, + ], + }, + }, + ...notAttrValue, + }); + }); + it("should throw when recursion depth has exceeded", () => { + const obj = {} as any; + obj.SELF = obj; + expect(() => marshallInput(obj, {}, { convertClassInstanceToMap: true })).toThrow( + new Error( + "Recursive copy depth exceeded 1000. Please set options.convertClassInstanceToMap to false and manually remove functions from your data object." + ) + ); + }); +}); diff --git a/lib/lib-dynamodb/src/commands/utils.ts b/lib/lib-dynamodb/src/commands/utils.ts index 2fef7a161150a..8f15fdc572390 100644 --- a/lib/lib-dynamodb/src/commands/utils.ts +++ b/lib/lib-dynamodb/src/commands/utils.ts @@ -90,12 +90,41 @@ const processAllKeysInObj = (obj: any, processFunc: Function, keyNodes: KeyNodes }, {} as any); }; +function copyWithoutFunctions(o: any, depth = 0): any { + if (depth > 1000) { + throw new Error( + "Recursive copy depth exceeded 1000. Please set options.convertClassInstanceToMap to false and manually remove functions from your data object." + ); + } + if (typeof o === "object" || typeof o === "function") { + if (Array.isArray(o)) { + return o.filter((item) => typeof item !== "function").map((item) => copyWithoutFunctions(item, depth + 1)); + } + if (o === null) { + return null; + } + const copy = {} as any; + for (const [key, value] of Object.entries(o)) { + if (typeof value !== "function") { + copy[key] = copyWithoutFunctions(value, depth + 1); + } + } + return copy; + } else { + return o; + } +} + /** * @internal */ export const marshallInput = (obj: any, keyNodes: KeyNodeChildren, options?: marshallOptions) => { + let _obj = obj; + if (options?.convertClassInstanceToMap) { + _obj = copyWithoutFunctions(obj); + } const marshallFunc = (toMarshall: any) => marshall(toMarshall, options); - return processKeysInObj(obj, marshallFunc, keyNodes); + return processKeysInObj(_obj, marshallFunc, keyNodes); }; /**