Skip to content

Commit 1783c69

Browse files
authored
feat(util-dynamodb): add option to convert class instance to map (#1969)
1 parent 9c21f14 commit 1783c69

File tree

5 files changed

+275
-152
lines changed

5 files changed

+275
-152
lines changed

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

+190-104
Original file line numberDiff line numberDiff line change
@@ -6,123 +6,145 @@ import { NativeAttributeValue } from "./models";
66

77
describe("convertToAttr", () => {
88
describe("null", () => {
9-
it(`returns for null`, () => {
10-
expect(convertToAttr(null)).toEqual({ NULL: true });
9+
[true, false].forEach((convertClassInstanceToMap) => {
10+
it(`returns for null`, () => {
11+
expect(convertToAttr(null, { convertClassInstanceToMap })).toEqual({ NULL: true });
12+
});
1113
});
1214
});
1315

1416
describe("boolean", () => {
15-
[true, false].forEach((bool) => {
16-
it(`returns for boolean: ${bool}`, () => {
17-
expect(convertToAttr(bool)).toEqual({ BOOL: bool });
17+
[true, false].forEach((convertClassInstanceToMap) => {
18+
[true, false].forEach((isClassInstance) => {
19+
[true, false].forEach((boolValue) => {
20+
const bool = isClassInstance ? new Boolean(boolValue) : boolValue;
21+
it(`returns for boolean: ${bool}`, () => {
22+
expect(convertToAttr(bool, { convertClassInstanceToMap })).toEqual({ BOOL: bool.valueOf() });
23+
});
24+
});
1825
});
1926
});
2027
});
2128

2229
describe("number", () => {
23-
[1, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER].forEach((num) => {
24-
it(`returns for number (integer): ${num}`, () => {
25-
expect(convertToAttr(num)).toEqual({ N: num.toString() });
26-
});
27-
});
28-
29-
[1.01, Math.PI, Math.E, Number.MIN_VALUE, Number.EPSILON].forEach((num) => {
30-
it(`returns for number (floating point): ${num}`, () => {
31-
expect(convertToAttr(num)).toEqual({ N: num.toString() });
32-
});
33-
});
34-
35-
[Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY].forEach((num) => {
36-
it(`throws for number (special numeric value): ${num}`, () => {
37-
expect(() => {
38-
convertToAttr(num);
39-
}).toThrowError(`Special numeric value ${num} is not allowed`);
40-
});
41-
});
42-
43-
[Number.MAX_SAFE_INTEGER + 1, Number.MAX_VALUE].forEach((num) => {
44-
it(`throws for number greater than Number.MAX_SAFE_INTEGER: ${num}`, () => {
45-
const errorPrefix = `Number ${num} is greater than Number.MAX_SAFE_INTEGER.`;
46-
47-
expect(() => {
48-
convertToAttr(num);
49-
}).toThrowError(`${errorPrefix} Use BigInt.`);
30+
[true, false].forEach((convertClassInstanceToMap) => {
31+
[true, false].forEach((isClassInstance) => {
32+
[1, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER].forEach((numValue) => {
33+
const num = isClassInstance ? new Number(numValue) : numValue;
34+
it(`returns for number (integer): ${num}`, () => {
35+
expect(convertToAttr(num, { convertClassInstanceToMap })).toEqual({ N: num.toString() });
36+
});
37+
});
5038

51-
const BigIntConstructor = BigInt;
52-
(BigInt as any) = undefined;
53-
expect(() => {
54-
convertToAttr(num);
55-
}).toThrowError(`${errorPrefix} Pass string value instead.`);
56-
BigInt = BigIntConstructor;
57-
});
58-
});
39+
[1.01, Math.PI, Math.E, Number.MIN_VALUE, Number.EPSILON].forEach((numValue) => {
40+
const num = isClassInstance ? new Number(numValue) : numValue;
41+
it(`returns for number (floating point): ${num}`, () => {
42+
expect(convertToAttr(num, { convertClassInstanceToMap })).toEqual({ N: num.toString() });
43+
});
44+
});
5945

60-
[Number.MIN_SAFE_INTEGER - 1].forEach((num) => {
61-
it(`throws for number lesser than Number.MIN_SAFE_INTEGER: ${num}`, () => {
62-
const errorPrefix = `Number ${num} is lesser than Number.MIN_SAFE_INTEGER.`;
46+
[Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY].forEach((numValue) => {
47+
const num = isClassInstance ? new Number(numValue) : numValue;
48+
it(`throws for number (special numeric value): ${num}`, () => {
49+
expect(() => {
50+
convertToAttr(num, { convertClassInstanceToMap });
51+
}).toThrowError(`Special numeric value ${num.toString()} is not allowed`);
52+
});
53+
});
6354

64-
expect(() => {
65-
convertToAttr(num);
66-
}).toThrowError(`${errorPrefix} Use BigInt.`);
55+
[Number.MAX_SAFE_INTEGER + 1, Number.MAX_VALUE].forEach((numValue) => {
56+
const num = isClassInstance ? new Number(numValue) : numValue;
57+
it(`throws for number greater than Number.MAX_SAFE_INTEGER: ${num}`, () => {
58+
const errorPrefix = `Number ${num.toString()} is greater than Number.MAX_SAFE_INTEGER.`;
59+
60+
expect(() => {
61+
convertToAttr(num, { convertClassInstanceToMap });
62+
}).toThrowError(`${errorPrefix} Use BigInt.`);
63+
64+
const BigIntConstructor = BigInt;
65+
(BigInt as any) = undefined;
66+
expect(() => {
67+
convertToAttr(num, { convertClassInstanceToMap });
68+
}).toThrowError(`${errorPrefix} Pass string value instead.`);
69+
BigInt = BigIntConstructor;
70+
});
71+
});
6772

68-
const BigIntConstructor = BigInt;
69-
(BigInt as any) = undefined;
70-
expect(() => {
71-
convertToAttr(num);
72-
}).toThrowError(`${errorPrefix} Pass string value instead.`);
73-
BigInt = BigIntConstructor;
73+
[Number.MIN_SAFE_INTEGER - 1].forEach((numValue) => {
74+
const num = isClassInstance ? new Number(numValue) : numValue;
75+
it(`throws for number lesser than Number.MIN_SAFE_INTEGER: ${num}`, () => {
76+
const errorPrefix = `Number ${num.toString()} is lesser than Number.MIN_SAFE_INTEGER.`;
77+
78+
expect(() => {
79+
convertToAttr(num, { convertClassInstanceToMap });
80+
}).toThrowError(`${errorPrefix} Use BigInt.`);
81+
82+
const BigIntConstructor = BigInt;
83+
(BigInt as any) = undefined;
84+
expect(() => {
85+
convertToAttr(num, { convertClassInstanceToMap });
86+
}).toThrowError(`${errorPrefix} Pass string value instead.`);
87+
BigInt = BigIntConstructor;
88+
});
89+
});
7490
});
7591
});
7692
});
7793

7894
describe("bigint", () => {
79-
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
80-
[
81-
// @ts-expect-error BigInt literals are not available when targeting lower than ES2020.
82-
1n,
83-
// @ts-expect-error BigInt literals are not available when targeting lower than ES2020.
84-
maxSafe * 2n,
85-
// @ts-expect-error BigInt literals are not available when targeting lower than ES2020.
86-
maxSafe * -2n,
87-
BigInt(Number.MAX_VALUE),
88-
BigInt("0x1fffffffffffff"),
89-
BigInt("0b11111111111111111111111111111111111111111111111111111"),
90-
].forEach((num) => {
91-
it(`returns for bigint: ${num}`, () => {
92-
expect(convertToAttr(num)).toEqual({ N: num.toString() });
95+
[true, false].forEach((convertClassInstanceToMap) => {
96+
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
97+
[
98+
// @ts-expect-error BigInt literals are not available when targeting lower than ES2020.
99+
1n,
100+
// @ts-expect-error BigInt literals are not available when targeting lower than ES2020.
101+
maxSafe * 2n,
102+
// @ts-expect-error BigInt literals are not available when targeting lower than ES2020.
103+
maxSafe * -2n,
104+
BigInt(Number.MAX_VALUE),
105+
BigInt("0x1fffffffffffff"),
106+
BigInt("0b11111111111111111111111111111111111111111111111111111"),
107+
].forEach((num) => {
108+
it(`returns for bigint: ${num}`, () => {
109+
expect(convertToAttr(num, { convertClassInstanceToMap })).toEqual({ N: num.toString() });
110+
});
93111
});
94112
});
95113
});
96114

97115
describe("binary", () => {
98-
const buffer = new ArrayBuffer(64);
99-
const arr = [...Array(64).keys()];
100-
const addPointOne = (num: number) => num + 0.1;
101-
102-
[
103-
buffer,
104-
new Blob([new Uint8Array(buffer)]),
105-
Buffer.from(buffer),
106-
new DataView(buffer),
107-
new Int8Array(arr),
108-
new Uint8Array(arr),
109-
new Uint8ClampedArray(arr),
110-
new Int16Array(arr),
111-
new Uint16Array(arr),
112-
new Int32Array(arr),
113-
new Uint32Array(arr),
114-
new Float32Array(arr.map(addPointOne)),
115-
new Float64Array(arr.map(addPointOne)),
116-
new BigInt64Array(arr.map(BigInt)),
117-
new BigUint64Array(arr.map(BigInt)),
118-
].forEach((data) => {
119-
it(`returns for binary: ${data.constructor.name}`, () => {
120-
expect(convertToAttr(data)).toEqual({ B: data });
116+
[true, false].forEach((convertClassInstanceToMap) => {
117+
const buffer = new ArrayBuffer(64);
118+
const arr = [...Array(64).keys()];
119+
const addPointOne = (num: number) => num + 0.1;
120+
121+
[
122+
buffer,
123+
new Blob([new Uint8Array(buffer)]),
124+
Buffer.from(buffer),
125+
new DataView(buffer),
126+
new Int8Array(arr),
127+
new Uint8Array(arr),
128+
new Uint8ClampedArray(arr),
129+
new Int16Array(arr),
130+
new Uint16Array(arr),
131+
new Int32Array(arr),
132+
new Uint32Array(arr),
133+
new Float32Array(arr.map(addPointOne)),
134+
new Float64Array(arr.map(addPointOne)),
135+
new BigInt64Array(arr.map(BigInt)),
136+
new BigUint64Array(arr.map(BigInt)),
137+
].forEach((data) => {
138+
it(`returns for binary: ${data.constructor.name}`, () => {
139+
expect(convertToAttr(data, { convertClassInstanceToMap })).toEqual({ B: data });
140+
});
121141
});
122-
});
123142

124-
it("returns null for Binary when options.convertEmptyValues=true", () => {
125-
expect(convertToAttr(new Uint8Array(), { convertEmptyValues: true })).toEqual({ NULL: true });
143+
it("returns null for Binary when options.convertEmptyValues=true", () => {
144+
expect(convertToAttr(new Uint8Array(), { convertClassInstanceToMap, convertEmptyValues: true })).toEqual({
145+
NULL: true,
146+
});
147+
});
126148
});
127149
});
128150

@@ -273,7 +295,6 @@ describe("convertToAttr", () => {
273295
describe("unallowed set", () => {
274296
it("throws error", () => {
275297
expect(() => {
276-
// @ts-expect-error Type 'Set<boolean>' is not assignable
277298
convertToAttr(new Set([true, false]));
278299
}).toThrowError(`Only Number Set (NS), Binary Set (BS) or String Set (SS) are allowed.`);
279300
});
@@ -362,19 +383,25 @@ describe("convertToAttr", () => {
362383
});
363384

364385
describe("string", () => {
365-
["", "string", "'single-quote'", '"double-quote"'].forEach((str) => {
366-
it(`returns for string: ${str}`, () => {
367-
expect(convertToAttr(str)).toEqual({ S: str });
368-
});
369-
});
386+
[true, false].forEach((convertClassInstanceToMap) => {
387+
[true, false].forEach((isClassInstance) => {
388+
["", "string", "'single-quote'", '"double-quote"'].forEach((strValue) => {
389+
const str = isClassInstance ? new String(strValue) : strValue;
390+
it(`returns for string: ${str}`, () => {
391+
expect(convertToAttr(str, { convertClassInstanceToMap })).toEqual({ S: str.toString() });
392+
});
393+
});
370394

371-
it("returns null for string when options.convertEmptyValues=true", () => {
372-
expect(convertToAttr("", { convertEmptyValues: true })).toEqual({ NULL: true });
395+
it("returns null for string when options.convertEmptyValues=true", () => {
396+
const str = isClassInstance ? new String("") : "";
397+
expect(convertToAttr(str, { convertClassInstanceToMap, convertEmptyValues: true })).toEqual({ NULL: true });
398+
});
399+
});
373400
});
374401
});
375402

376403
describe(`unsupported type`, () => {
377-
class FooObj {
404+
class FooClass {
378405
constructor(private readonly foo: string) {}
379406
}
380407

@@ -385,13 +412,72 @@ describe("convertToAttr", () => {
385412
});
386413

387414
// ToDo: Serialize ES6 class objects as string https://github.com/aws/aws-sdk-js-v3/issues/1535
388-
[new Date(), new FooObj("foo")].forEach((data) => {
415+
[new Date(), new FooClass("foo")].forEach((data) => {
389416
it(`throws for: ${String(data)}`, () => {
390417
expect(() => {
391-
// @ts-expect-error Argument is not assignable to parameter of type 'NativeAttributeValue'
392418
convertToAttr(data);
393-
}).toThrowError(`Unsupported type passed: ${String(data)}`);
419+
}).toThrowError(
420+
`Unsupported type passed: ${String(
421+
data
422+
)}. Pass options.convertClassInstanceToMap=true to marshall typeof object as map attribute.`
423+
);
394424
});
395425
});
396426
});
427+
428+
describe("typeof object with options.convertClassInstanceToMap=true", () => {
429+
it("returns map for class", () => {
430+
class FooClass {
431+
constructor(
432+
private readonly nullAttr: null,
433+
private readonly boolAttr: boolean,
434+
private readonly strAttr: string,
435+
private readonly numAttr: number,
436+
private readonly bigintAttr: bigint,
437+
private readonly binaryAttr: Uint8Array,
438+
private readonly listAttr: any[],
439+
private readonly mapAttr: { [key: string]: any }
440+
) {}
441+
}
442+
expect(
443+
convertToAttr(
444+
new FooClass(
445+
null,
446+
true,
447+
"string",
448+
1,
449+
BigInt(Number.MAX_VALUE),
450+
new Uint8Array([...Array(64).keys()]),
451+
[null, 1, "two", true],
452+
{
453+
nullKey: null,
454+
numKey: 1,
455+
strKey: "string",
456+
boolKey: true,
457+
}
458+
),
459+
{
460+
convertClassInstanceToMap: true,
461+
}
462+
)
463+
).toEqual({
464+
M: {
465+
nullAttr: { NULL: true },
466+
boolAttr: { BOOL: true },
467+
strAttr: { S: "string" },
468+
numAttr: { N: "1" },
469+
bigintAttr: { N: BigInt(Number.MAX_VALUE).toString() },
470+
binaryAttr: { B: new Uint8Array([...Array(64).keys()]) },
471+
listAttr: { L: [{ NULL: true }, { N: "1" }, { S: "two" }, { BOOL: true }] },
472+
mapAttr: {
473+
M: { nullKey: { NULL: true }, numKey: { N: "1" }, strKey: { S: "string" }, boolKey: { BOOL: true } },
474+
},
475+
},
476+
});
477+
});
478+
479+
it("returns empty for Date object", () => {
480+
expect(convertToAttr(new Date(), { convertClassInstanceToMap: true })).toEqual({ M: {} });
481+
});
482+
});
397483
});

0 commit comments

Comments
 (0)