Skip to content

Commit a008d23

Browse files
authored
feat(util-dynamodb): support marshalling for Object.create (#1974)
1 parent 96c1b99 commit a008d23

File tree

2 files changed

+129
-74
lines changed

2 files changed

+129
-74
lines changed

Diff for: packages/util-dynamodb/src/convertToAttr.spec.ts

+114-61
Original file line numberDiff line numberDiff line change
@@ -306,80 +306,101 @@ describe("convertToAttr", () => {
306306
const uint8Arr = new Uint32Array(arr);
307307
const biguintArr = new BigUint64Array(arr.map(BigInt));
308308

309-
([
310-
{
311-
input: { nullKey: null, boolKey: false },
312-
output: { nullKey: { NULL: true }, boolKey: { BOOL: false } },
313-
},
314-
{
315-
input: { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) },
316-
output: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } },
317-
},
318-
{
319-
input: { uint8ArrKey: uint8Arr, biguintArrKey: biguintArr },
320-
output: { uint8ArrKey: { B: uint8Arr }, biguintArrKey: { B: biguintArr } },
321-
},
322-
{
323-
input: {
324-
list1: [null, false],
325-
list2: ["one", 1.01, BigInt(9007199254740996)],
309+
[true, false].forEach((useObjectCreate) => {
310+
([
311+
{
312+
input: { nullKey: null, boolKey: false },
313+
output: { nullKey: { NULL: true }, boolKey: { BOOL: false } },
326314
},
327-
output: {
328-
list1: { L: [{ NULL: true }, { BOOL: false }] },
329-
list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] },
315+
{
316+
input: { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) },
317+
output: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } },
330318
},
331-
},
332-
{
333-
input: {
334-
numberSet: new Set([1, 2, 3]),
335-
bigintSet: new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]),
336-
binarySet: new Set([uint8Arr, biguintArr]),
337-
stringSet: new Set(["one", "two", "three"]),
319+
{
320+
input: { uint8ArrKey: uint8Arr, biguintArrKey: biguintArr },
321+
output: { uint8ArrKey: { B: uint8Arr }, biguintArrKey: { B: biguintArr } },
322+
},
323+
{
324+
input: {
325+
list1: [null, false],
326+
list2: ["one", 1.01, BigInt(9007199254740996)],
327+
},
328+
output: {
329+
list1: { L: [{ NULL: true }, { BOOL: false }] },
330+
list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] },
331+
},
338332
},
339-
output: {
340-
numberSet: { NS: ["1", "2", "3"] },
341-
bigintSet: { NS: ["9007199254740996", "-9007199254740996"] },
342-
binarySet: { BS: [uint8Arr, biguintArr] },
343-
stringSet: { SS: ["one", "two", "three"] },
333+
{
334+
input: {
335+
numberSet: new Set([1, 2, 3]),
336+
bigintSet: new Set([BigInt(9007199254740996), BigInt(-9007199254740996)]),
337+
binarySet: new Set([uint8Arr, biguintArr]),
338+
stringSet: new Set(["one", "two", "three"]),
339+
},
340+
output: {
341+
numberSet: { NS: ["1", "2", "3"] },
342+
bigintSet: { NS: ["9007199254740996", "-9007199254740996"] },
343+
binarySet: { BS: [uint8Arr, biguintArr] },
344+
stringSet: { SS: ["one", "two", "three"] },
345+
},
344346
},
345-
},
346-
] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach(
347-
({ input, output }) => {
348-
it(`testing map: ${input}`, () => {
349-
expect(convertToAttr(input)).toEqual({ M: output });
347+
] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach(
348+
({ input, output }) => {
349+
const inputObject = useObjectCreate ? Object.create(input) : input;
350+
it(`testing map: ${inputObject}`, () => {
351+
expect(convertToAttr(inputObject)).toEqual({ M: output });
352+
});
353+
}
354+
);
355+
356+
it(`testing map with options.convertEmptyValues=true`, () => {
357+
const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) };
358+
const inputObject = useObjectCreate ? Object.create(input) : input;
359+
expect(convertToAttr(inputObject, { convertEmptyValues: true })).toEqual({
360+
M: { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } },
350361
});
351-
}
352-
);
353-
354-
it(`testing map with options.convertEmptyValues=true`, () => {
355-
const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) };
356-
expect(convertToAttr(input, { convertEmptyValues: true })).toEqual({
357-
M: { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } },
358362
});
359-
});
360-
361-
describe(`testing map with options.removeUndefinedValues`, () => {
362-
describe("throws error", () => {
363-
const testErrorMapWithUndefinedValues = (options?: marshallOptions) => {
364-
expect(() => {
365-
convertToAttr({ definedKey: "definedKey", undefinedKey: undefined }, options);
366-
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
367-
};
368363

369-
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
370-
it(`when options=${options}`, () => {
371-
testErrorMapWithUndefinedValues(options);
364+
describe(`testing map with options.removeUndefinedValues`, () => {
365+
describe("throws error", () => {
366+
const testErrorMapWithUndefinedValues = (useObjectCreate: boolean, options?: marshallOptions) => {
367+
const input = { definedKey: "definedKey", undefinedKey: undefined };
368+
const inputObject = useObjectCreate ? Object.create(input) : input;
369+
expect(() => {
370+
convertToAttr(inputObject, options);
371+
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
372+
};
373+
374+
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
375+
it(`when options=${options}`, () => {
376+
testErrorMapWithUndefinedValues(useObjectCreate, options);
377+
});
372378
});
373379
});
374-
});
375380

376-
it(`returns when options.removeUndefinedValues=true`, () => {
377-
const input = { definedKey: "definedKey", undefinedKey: undefined };
378-
expect(convertToAttr(input, { removeUndefinedValues: true })).toEqual({
379-
M: { definedKey: { S: "definedKey" } },
381+
it(`returns when options.removeUndefinedValues=true`, () => {
382+
const input = { definedKey: "definedKey", undefinedKey: undefined };
383+
const inputObject = useObjectCreate ? Object.create(input) : input;
384+
expect(convertToAttr(inputObject, { removeUndefinedValues: true })).toEqual({
385+
M: { definedKey: { S: "definedKey" } },
386+
});
380387
});
381388
});
382389
});
390+
391+
it(`testing Object.create with function`, () => {
392+
const person = {
393+
isHuman: true,
394+
printIntroduction: function () {
395+
console.log(`Am I human? ${this.isHuman}`);
396+
},
397+
};
398+
expect(convertToAttr(Object.create(person))).toEqual({ M: { isHuman: { BOOL: true } } });
399+
});
400+
401+
it(`testing Object.create(null)`, () => {
402+
expect(convertToAttr(Object.create(null))).toEqual({ M: {} });
403+
});
383404
});
384405

385406
describe("string", () => {
@@ -438,6 +459,9 @@ describe("convertToAttr", () => {
438459
private readonly listAttr: any[],
439460
private readonly mapAttr: { [key: string]: any }
440461
) {}
462+
public exampleMethod() {
463+
return "This method won't be marshalled";
464+
}
441465
}
442466
expect(
443467
convertToAttr(
@@ -476,6 +500,35 @@ describe("convertToAttr", () => {
476500
});
477501
});
478502

503+
it("returns inherited values from parent class in map", () => {
504+
class Person {
505+
protected name: string;
506+
constructor(name: string) {
507+
this.name = name;
508+
}
509+
}
510+
511+
class Employee extends Person {
512+
private department: string;
513+
514+
constructor(name: string, department: string) {
515+
super(name);
516+
this.department = department;
517+
}
518+
519+
public getElevatorPitch() {
520+
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
521+
}
522+
}
523+
524+
expect(convertToAttr(new Employee("John", "Sales"), { convertClassInstanceToMap: true })).toEqual({
525+
M: {
526+
name: { S: "John" },
527+
department: { S: "Sales" },
528+
},
529+
});
530+
});
531+
479532
it("returns empty for Date object", () => {
480533
expect(convertToAttr(new Date(), { convertClassInstanceToMap: true })).toEqual({ M: {} });
481534
});

Diff for: packages/util-dynamodb/src/convertToAttr.ts

+15-13
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti
1818
return convertToListAttr(data, options);
1919
} else if (data?.constructor?.name === "Set") {
2020
return convertToSetAttr(data as Set<any>, options);
21-
} else if (data?.constructor?.name === "Object") {
21+
} else if (
22+
data?.constructor?.name === "Object" ||
23+
// for object which is result of Object.create(null), which doesn't have constructor defined
24+
(!data.constructor && typeof data === "object")
25+
) {
2226
return convertToMapAttr(data as { [key: string]: NativeAttributeValue }, options);
2327
} else if (isBinary(data)) {
2428
if (data.length === 0 && options?.convertEmptyValues) {
@@ -105,18 +109,16 @@ const convertToMapAttr = (
105109
data: { [key: string]: NativeAttributeValue },
106110
options?: marshallOptions
107111
): { M: { [key: string]: AttributeValue } } => ({
108-
M: Object.entries(data)
109-
.filter(
110-
([key, value]: [string, NativeAttributeValue]) =>
111-
!options?.removeUndefinedValues || (options?.removeUndefinedValues && value !== undefined)
112-
)
113-
.reduce(
114-
(acc: { [key: string]: AttributeValue }, [key, value]: [string, NativeAttributeValue]) => ({
115-
...acc,
116-
[key]: convertToAttr(value, options),
117-
}),
118-
{}
119-
),
112+
M: (function getMapFromEnurablePropsInPrototypeChain(data) {
113+
const map: { [key: string]: AttributeValue } = {};
114+
for (const key in data) {
115+
const value = data[key];
116+
if (typeof value !== "function" && (value !== undefined || !options?.removeUndefinedValues)) {
117+
map[key] = convertToAttr(value, options);
118+
}
119+
}
120+
return map;
121+
})(data),
120122
});
121123

122124
// For future-proofing: this functions are called from multiple places

0 commit comments

Comments
 (0)