Skip to content

Commit

Permalink
feat(util-dynamodb): marshall JavaScript Maps (#2010)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr committed Feb 11, 2021
1 parent d1c548e commit 569b572
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 70 deletions.
176 changes: 110 additions & 66 deletions packages/util-dynamodb/src/convertToAttr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,101 +306,146 @@ 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}${useObjectCreate && " with Object.create()"}`, () => {
expect(convertToAttr(inputObject)).toEqual({ M: output });
});
}
);
});

it(`testing map with options.convertEmptyValues=true`, () => {
const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) };
const inputMap = new Map(Object.entries(input));
it(`testing map: ${inputMap}`, () => {
expect(convertToAttr(inputMap)).toEqual({ M: output });
});
}
);

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 });
});
});

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.`);
};
const inputMap = new Map(Object.entries(input));
it(`testing map`, () => {
expect(convertToAttr(inputMap, { convertEmptyValues: true })).toEqual({ M: output });
});
});

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", () => {
Expand Down Expand Up @@ -432,7 +477,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(() => {
Expand Down
25 changes: 21 additions & 4 deletions packages/util-dynamodb/src/convertToAttr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ 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 === "Map") {
return convertToMapAttrFromIterable(data as Map<string, NativeAttributeValue>, 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();
Expand All @@ -43,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.`
Expand Down Expand Up @@ -105,11 +107,26 @@ const convertToSetAttr = (
}
};

const convertToMapAttr = (
const convertToMapAttrFromIterable = (
data: Map<string, NativeAttributeValue>,
options?: marshallOptions
): { M: { [key: string]: AttributeValue } } => ({
M: ((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 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];
Expand Down

0 comments on commit 569b572

Please sign in to comment.