Skip to content

Commit

Permalink
feat(util-dynamodb): support marshalling for Object.create (#1974)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr authored Feb 9, 2021
1 parent 96c1b99 commit a008d23
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 74 deletions.
175 changes: 114 additions & 61 deletions packages/util-dynamodb/src/convertToAttr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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: {} });
});
Expand Down
28 changes: 15 additions & 13 deletions packages/util-dynamodb/src/convertToAttr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>, 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) {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a008d23

Please sign in to comment.