Skip to content

Commit 569b572

Browse files
authored
feat(util-dynamodb): marshall JavaScript Maps (#2010)
1 parent d1c548e commit 569b572

File tree

2 files changed

+131
-70
lines changed

2 files changed

+131
-70
lines changed

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

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

309-
[true, false].forEach((useObjectCreate) => {
310-
([
311-
{
312-
input: { nullKey: null, boolKey: false },
313-
output: { nullKey: { NULL: true }, boolKey: { BOOL: false } },
314-
},
315-
{
316-
input: { stringKey: "one", numberKey: 1.01, bigintKey: BigInt(9007199254740996) },
317-
output: { stringKey: { S: "one" }, numberKey: { N: "1.01" }, bigintKey: { N: "9007199254740996" } },
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)],
318326
},
319-
{
320-
input: { uint8ArrKey: uint8Arr, biguintArrKey: biguintArr },
321-
output: { uint8ArrKey: { B: uint8Arr }, biguintArrKey: { B: biguintArr } },
327+
output: {
328+
list1: { L: [{ NULL: true }, { BOOL: false }] },
329+
list2: { L: [{ S: "one" }, { N: "1.01" }, { N: "9007199254740996" }] },
322330
},
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-
},
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"]),
332338
},
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-
},
339+
output: {
340+
numberSet: { NS: ["1", "2", "3"] },
341+
bigintSet: { NS: ["9007199254740996", "-9007199254740996"] },
342+
binarySet: { BS: [uint8Arr, biguintArr] },
343+
stringSet: { SS: ["one", "two", "three"] },
346344
},
347-
] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach(
348-
({ input, output }) => {
345+
},
346+
] as { input: { [key: string]: NativeAttributeValue }; output: { [key: string]: AttributeValue } }[]).forEach(
347+
({ input, output }) => {
348+
[true, false].forEach((useObjectCreate) => {
349349
const inputObject = useObjectCreate ? Object.create(input) : input;
350-
it(`testing map: ${inputObject}`, () => {
350+
it(`testing object: ${inputObject}${useObjectCreate && " with Object.create()"}`, () => {
351351
expect(convertToAttr(inputObject)).toEqual({ M: output });
352352
});
353-
}
354-
);
353+
});
355354

356-
it(`testing map with options.convertEmptyValues=true`, () => {
357-
const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) };
355+
const inputMap = new Map(Object.entries(input));
356+
it(`testing map: ${inputMap}`, () => {
357+
expect(convertToAttr(inputMap)).toEqual({ M: output });
358+
});
359+
}
360+
);
361+
362+
describe(`with options.convertEmptyValues=true`, () => {
363+
const input = { stringKey: "", binaryKey: new Uint8Array(), setKey: new Set([]) };
364+
const output = { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } };
365+
366+
[true, false].forEach((useObjectCreate) => {
358367
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 } },
368+
it(`testing object${useObjectCreate && " with Object.create()"}`, () => {
369+
expect(convertToAttr(inputObject, { convertEmptyValues: true })).toEqual({ M: output });
361370
});
362371
});
363372

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+
const inputMap = new Map(Object.entries(input));
374+
it(`testing map`, () => {
375+
expect(convertToAttr(inputMap, { convertEmptyValues: true })).toEqual({ M: output });
376+
});
377+
});
378+
379+
describe(`with options.removeUndefinedValues=true`, () => {
380+
describe("throws error", () => {
381+
const testErrorMapWithUndefinedValues = (input: any, options?: marshallOptions) => {
382+
expect(() => {
383+
convertToAttr(input, options);
384+
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
385+
};
373386

374-
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
375-
it(`when options=${options}`, () => {
376-
testErrorMapWithUndefinedValues(useObjectCreate, options);
387+
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
388+
const input = { definedKey: "definedKey", undefinedKey: undefined };
389+
[true, false].forEach((useObjectCreate) => {
390+
const inputObject = useObjectCreate ? Object.create(input) : input;
391+
it(`testing object${useObjectCreate && " with Object.create()"} when options=${options}`, () => {
392+
testErrorMapWithUndefinedValues(inputObject, options);
377393
});
378394
});
395+
396+
const inputMap = new Map(Object.entries(input));
397+
it(`testing map when options=${options}`, () => {
398+
testErrorMapWithUndefinedValues(inputMap, options);
399+
});
379400
});
401+
});
380402

381-
it(`returns when options.removeUndefinedValues=true`, () => {
382-
const input = { definedKey: "definedKey", undefinedKey: undefined };
403+
describe(`returns when options.removeUndefinedValues=true`, () => {
404+
const input = { definedKey: "definedKey", undefinedKey: undefined };
405+
const output = { definedKey: { S: "definedKey" } };
406+
[true, false].forEach((useObjectCreate) => {
383407
const inputObject = useObjectCreate ? Object.create(input) : input;
384-
expect(convertToAttr(inputObject, { removeUndefinedValues: true })).toEqual({
385-
M: { definedKey: { S: "definedKey" } },
408+
it(`testing object${useObjectCreate && " with Object.create()"}`, () => {
409+
expect(convertToAttr(inputObject, { removeUndefinedValues: true })).toEqual({ M: output });
386410
});
387411
});
412+
413+
const inputMap = new Map(Object.entries(input));
414+
it(`testing map`, () => {
415+
expect(convertToAttr(inputMap, { removeUndefinedValues: true })).toEqual({ M: output });
416+
});
388417
});
389418
});
390419

391-
it(`testing Object.create with function`, () => {
392-
const person = {
393-
isHuman: true,
394-
printIntroduction: function () {
395-
console.log(`Am I human? ${this.isHuman}`);
420+
describe(`testing with function`, () => {
421+
const input = {
422+
bool: true,
423+
func: function () {
424+
console.log(`bool: ${this.bool}`);
396425
},
397426
};
398-
expect(convertToAttr(Object.create(person))).toEqual({ M: { isHuman: { BOOL: true } } });
427+
const output = { bool: { BOOL: true } };
428+
429+
[true, false].forEach((useObjectCreate) => {
430+
const inputObject = useObjectCreate ? Object.create(input) : input;
431+
it(`testing object${useObjectCreate && " with Object.create()"}`, () => {
432+
expect(convertToAttr(inputObject)).toEqual({ M: output });
433+
});
434+
});
435+
436+
const inputMap = new Map(Object.entries(input));
437+
it(`testing map`, () => {
438+
expect(convertToAttr(inputMap)).toEqual({ M: output });
439+
});
399440
});
400441

401442
it(`testing Object.create(null)`, () => {
402443
expect(convertToAttr(Object.create(null))).toEqual({ M: {} });
403444
});
445+
446+
it(`testing empty Map`, () => {
447+
expect(convertToAttr(new Map())).toEqual({ M: {} });
448+
});
404449
});
405450

406451
describe("string", () => {
@@ -432,7 +477,6 @@ describe("convertToAttr", () => {
432477
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
433478
});
434479

435-
// ToDo: Serialize ES6 class objects as string https://github.com/aws/aws-sdk-js-v3/issues/1535
436480
[new Date(), new FooClass("foo")].forEach((data) => {
437481
it(`throws for: ${String(data)}`, () => {
438482
expect(() => {

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

+21-4
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ 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 === "Map") {
22+
return convertToMapAttrFromIterable(data as Map<string, NativeAttributeValue>, options);
2123
} else if (
2224
data?.constructor?.name === "Object" ||
2325
// for object which is result of Object.create(null), which doesn't have constructor defined
2426
(!data.constructor && typeof data === "object")
2527
) {
26-
return convertToMapAttr(data as { [key: string]: NativeAttributeValue }, options);
28+
return convertToMapAttrFromEnumerableProps(data as { [key: string]: NativeAttributeValue }, options);
2729
} else if (isBinary(data)) {
2830
if (data.length === 0 && options?.convertEmptyValues) {
2931
return convertToNullAttr();
@@ -43,7 +45,7 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti
4345
}
4446
return convertToStringAttr(data);
4547
} else if (options?.convertClassInstanceToMap && typeof data === "object") {
46-
return convertToMapAttr(data as { [key: string]: NativeAttributeValue }, options);
48+
return convertToMapAttrFromEnumerableProps(data as { [key: string]: NativeAttributeValue }, options);
4749
}
4850
throw new Error(
4951
`Unsupported type passed: ${data}. Pass options.convertClassInstanceToMap=true to marshall typeof object as map attribute.`
@@ -105,11 +107,26 @@ const convertToSetAttr = (
105107
}
106108
};
107109

108-
const convertToMapAttr = (
110+
const convertToMapAttrFromIterable = (
111+
data: Map<string, NativeAttributeValue>,
112+
options?: marshallOptions
113+
): { M: { [key: string]: AttributeValue } } => ({
114+
M: ((data) => {
115+
const map: { [key: string]: AttributeValue } = {};
116+
for (const [key, value] of data) {
117+
if (typeof value !== "function" && (value !== undefined || !options?.removeUndefinedValues)) {
118+
map[key] = convertToAttr(value, options);
119+
}
120+
}
121+
return map;
122+
})(data),
123+
});
124+
125+
const convertToMapAttrFromEnumerableProps = (
109126
data: { [key: string]: NativeAttributeValue },
110127
options?: marshallOptions
111128
): { M: { [key: string]: AttributeValue } } => ({
112-
M: (function getMapFromEnurablePropsInPrototypeChain(data) {
129+
M: ((data) => {
113130
const map: { [key: string]: AttributeValue } = {};
114131
for (const key in data) {
115132
const value = data[key];

0 commit comments

Comments
 (0)