Skip to content

Commit 4c7fe9c

Browse files
authored
feat(lib-dynamodb): large number handling (#5427)
* feat(lib-dynamodb): large number handling * feat(lib-dynamodb): large number handling docs and set test case * feat(lib-dynamodb): set release tag * feat(lib-dynamodb): remove unsafe conversion feature * feat(lib-dynamodb): add 1e100 number test case * feat(lib-dynamodb): large number handling, remove extra unmarshall option
1 parent a7f619a commit 4c7fe9c

File tree

10 files changed

+314
-46
lines changed

10 files changed

+314
-46
lines changed

Diff for: lib/lib-dynamodb/README.md

+105-26
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@ Responses from DynamoDB are unmarshalled into plain JavaScript objects
2020
by the `DocumentClient`. The `DocumentClient` does not accept
2121
`AttributeValue`s in favor of native JavaScript types.
2222

23-
| JavaScript Type | DynamoDB AttributeValue |
24-
| :-------------------------------: | ----------------------- |
25-
| String | S |
26-
| Number / BigInt | N |
27-
| Boolean | BOOL |
28-
| null | NULL |
29-
| Array | L |
30-
| Object | M |
31-
| Set\<Uint8Array, Blob, ...\> | BS |
32-
| Set\<Number, BigInt\> | NS |
33-
| Set\<String\> | SS |
34-
| Uint8Array, Buffer, File, Blob... | B |
23+
| JavaScript Type | DynamoDB AttributeValue |
24+
| :--------------------------------: | ----------------------- |
25+
| String | S |
26+
| Number / BigInt / NumberValue | N |
27+
| Boolean | BOOL |
28+
| null | NULL |
29+
| Array | L |
30+
| Object | M |
31+
| Set\<Uint8Array, Blob, ...\> | BS |
32+
| Set\<Number, BigInt, NumberValue\> | NS |
33+
| Set\<String\> | SS |
34+
| Uint8Array, Buffer, File, Blob... | B |
3535

3636
### Example
3737

@@ -98,20 +98,48 @@ const ddbDocClient = DynamoDBDocument.from(client); // client is DynamoDB client
9898
The configuration for marshalling and unmarshalling can be sent as an optional
9999
second parameter during creation of document client as follows:
100100

101-
```js
102-
const marshallOptions = {
103-
// Whether to automatically convert empty strings, blobs, and sets to `null`.
104-
convertEmptyValues: false, // false, by default.
105-
// Whether to remove undefined values while marshalling.
106-
removeUndefinedValues: false, // false, by default.
107-
// Whether to convert typeof object to map attribute.
108-
convertClassInstanceToMap: false, // false, by default.
109-
};
110-
111-
const unmarshallOptions = {
112-
// Whether to return numbers as a string instead of converting them to native JavaScript numbers.
113-
wrapNumbers: false, // false, by default.
114-
};
101+
```ts
102+
export interface marshallOptions {
103+
/**
104+
* Whether to automatically convert empty strings, blobs, and sets to `null`
105+
*/
106+
convertEmptyValues?: boolean;
107+
/**
108+
* Whether to remove undefined values while marshalling.
109+
*/
110+
removeUndefinedValues?: boolean;
111+
/**
112+
* Whether to convert typeof object to map attribute.
113+
*/
114+
convertClassInstanceToMap?: boolean;
115+
/**
116+
* Whether to convert the top level container
117+
* if it is a map or list.
118+
*
119+
* Default is true when using the DynamoDBDocumentClient,
120+
* but false if directly using the marshall function (backwards compatibility).
121+
*/
122+
convertTopLevelContainer?: boolean;
123+
}
124+
125+
export interface unmarshallOptions {
126+
/**
127+
* Whether to return numbers as a string instead of converting them to native JavaScript numbers.
128+
* This allows for the safe round-trip transport of numbers of arbitrary size.
129+
*/
130+
wrapNumbers?: boolean;
131+
132+
/**
133+
* When true, skip wrapping the data in `{ M: data }` before converting.
134+
*
135+
* Default is true when using the DynamoDBDocumentClient,
136+
* but false if directly using the unmarshall function (backwards compatibility).
137+
*/
138+
convertWithoutMapWrapper?: boolean;
139+
}
140+
141+
const marshallOptions: marshallOptions = {};
142+
const unmarshallOptions: unmarshallOptions = {};
115143

116144
const translateConfig = { marshallOptions, unmarshallOptions };
117145

@@ -160,6 +188,57 @@ await ddbDocClient.put({
160188
});
161189
```
162190

191+
### Large Numbers and `NumberValue`.
192+
193+
On the input or marshalling side, the class `NumberValue` can be used
194+
anywhere to represent a DynamoDB number value, even small numbers.
195+
196+
```ts
197+
import { DynamoDB } from "@aws-sdk/client-dynamodb";
198+
import { NumberValue, DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
199+
200+
// Note, the client will not validate the acceptability of the number
201+
// in terms of size or format.
202+
// It is only here to preserve your precise representation.
203+
const client = DynamoDBDocument.from(new DynamoDB({}));
204+
205+
await client.put({
206+
Item: {
207+
id: 1,
208+
smallNumber: NumberValue.from("123"),
209+
bigNumber: NumberValue.from("1000000000000000000000.000000000001"),
210+
nSet: new Set([123, NumberValue.from("456"), 789]),
211+
},
212+
});
213+
```
214+
215+
On the output or unmarshalling side, the class `NumberValue` is used
216+
depending on your setting for the `unmarshallOptions` flag `wrapnumbers`,
217+
shown above.
218+
219+
```ts
220+
import { DynamoDB } from "@aws-sdk/client-dynamodb";
221+
import { NumberValue, DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
222+
223+
const client = DynamoDBDocument.from(new DynamoDB({}));
224+
225+
const response = await client.get({
226+
Key: {
227+
id: 1,
228+
},
229+
});
230+
231+
/**
232+
* Numbers in the response may be a number, a BigInt, or a NumberValue depending
233+
* on how you set `wrapNumbers`.
234+
*/
235+
const value = response.Item.bigNumber;
236+
```
237+
238+
`NumberValue` does not provide a way to do mathematical operations on itself.
239+
To do mathematical operations, take the string value of `NumberValue` by calling
240+
`.toString()` and supply it to your chosen big number implementation.
241+
163242
### Client and Command middleware stacks
164243

165244
As with other AWS SDK for JavaScript v3 clients, you can apply middleware functions

Diff for: lib/lib-dynamodb/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export * from "./DynamoDBDocumentClient";
44
// smithy-typescript generated code
55
export * from "./commands";
66
export * from "./pagination";
7+
8+
export { NumberValueImpl as NumberValue } from "@aws-sdk/util-dynamodb";

Diff for: lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts

+41-16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ExecuteStatementCommandOutput,
1616
ExecuteTransactionCommandOutput,
1717
GetCommandOutput,
18+
NumberValue,
1819
PutCommandOutput,
1920
QueryCommandOutput,
2021
ScanCommandOutput,
@@ -32,6 +33,9 @@ describe(DynamoDBDocument.name, () => {
3233
marshallOptions: {
3334
convertTopLevelContainer: true,
3435
},
36+
unmarshallOptions: {
37+
wrapNumbers: true,
38+
},
3539
});
3640

3741
function throwIfError(e: unknown) {
@@ -76,30 +80,39 @@ describe(DynamoDBDocument.name, () => {
7680
const data = {
7781
null: null,
7882
string: "myString",
79-
number: 1,
83+
number: NumberValue.from(1),
84+
bigInt: NumberValue.from(
85+
"10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
86+
),
87+
bigNumber: NumberValue.from("3210000000000000000.0000000000000123"),
8088
boolean: true,
8189
sSet: new Set(["my", "string", "set"]),
82-
nSet: new Set([2, 3, 4]),
90+
nSet: new Set([2, 3, 4].map(NumberValue.from)),
8391
list: [
8492
null,
8593
"myString",
86-
1,
94+
NumberValue.from(1),
8795
true,
8896
new Set(["my", "string", "set"]),
89-
new Set([2, 3, 4]),
90-
["listInList", 1, null],
97+
new Set([NumberValue.from(2), NumberValue.from(3), NumberValue.from(4)]),
98+
new Set([
99+
NumberValue.from("3210000000000000000.0000000000000123"),
100+
NumberValue.from("3210000000000000001.0000000000000123"),
101+
NumberValue.from("3210000000000000002.0000000000000123"),
102+
]),
103+
["listInList", NumberValue.from(1), null],
91104
{
92105
mapInList: "mapInList",
93106
},
94107
],
95108
map: {
96109
null: null,
97110
string: "myString",
98-
number: 1,
111+
number: NumberValue.from(1),
99112
boolean: true,
100113
sSet: new Set(["my", "string", "set"]),
101-
nSet: new Set([2, 3, 4]),
102-
listInMap: ["listInMap", 1, null],
114+
nSet: new Set([2, 3, 4].map(NumberValue.from)),
115+
listInMap: ["listInMap", NumberValue.from(1), null],
103116
mapInMap: { mapInMap: "mapInMap" },
104117
},
105118
};
@@ -116,6 +129,9 @@ describe(DynamoDBDocument.name, () => {
116129
if (input instanceof Set) {
117130
return new Set([...input].map(updateTransform)) as T;
118131
}
132+
if (input instanceof NumberValue) {
133+
return NumberValue.from(input.toString()) as T;
134+
}
119135
return Object.entries(input).reduce((acc, [k, v]) => {
120136
acc[updateTransform(k)] = updateTransform(v);
121137
return acc;
@@ -436,28 +452,37 @@ describe(DynamoDBDocument.name, () => {
436452
expect(updateTransform(data)).toEqual({
437453
"null-x": null,
438454
"string-x": "myString-x",
439-
"number-x": 2,
455+
"number-x": NumberValue.from(1),
456+
"bigInt-x": NumberValue.from(
457+
"10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
458+
),
459+
"bigNumber-x": NumberValue.from("3210000000000000000.0000000000000123"),
440460
"boolean-x": false,
441461
"sSet-x": new Set(["my-x", "string-x", "set-x"]),
442-
"nSet-x": new Set([3, 4, 5]),
462+
"nSet-x": new Set([2, 3, 4].map(NumberValue.from)),
443463
"list-x": [
444464
null,
445465
"myString-x",
446-
2,
466+
NumberValue.from(1),
447467
false,
448468
new Set(["my-x", "string-x", "set-x"]),
449-
new Set([3, 4, 5]),
450-
["listInList-x", 2, null],
469+
new Set([2, 3, 4].map(NumberValue.from)),
470+
new Set([
471+
NumberValue.from("3210000000000000000.0000000000000123"),
472+
NumberValue.from("3210000000000000001.0000000000000123"),
473+
NumberValue.from("3210000000000000002.0000000000000123"),
474+
]),
475+
["listInList-x", NumberValue.from(1), null],
451476
{ "mapInList-x": "mapInList-x" },
452477
],
453478
"map-x": {
454479
"null-x": null,
455480
"string-x": "myString-x",
456-
"number-x": 2,
481+
"number-x": NumberValue.from(1),
457482
"boolean-x": false,
458483
"sSet-x": new Set(["my-x", "string-x", "set-x"]),
459-
"nSet-x": new Set([3, 4, 5]),
460-
"listInMap-x": ["listInMap-x", 2, null],
484+
"nSet-x": new Set([2, 3, 4].map(NumberValue.from)),
485+
"listInMap-x": ["listInMap-x", NumberValue.from(1), null],
461486
"mapInMap-x": { "mapInMap-x": "mapInMap-x" },
462487
},
463488
});

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

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NumberValue } from "./NumberValue";
2+
3+
const BIG_DECIMAL =
4+
"123456789012345678901234567890123456789012345678901234567890.123456789012345678901234567890123456789012345678901234567890";
5+
const BIG_INT = "123456789012345678901234567890123456789012345678901234567890";
6+
7+
describe(NumberValue.name, () => {
8+
it("can be statically constructed from numbers", () => {
9+
expect(NumberValue.from(123.123).toString()).toEqual("123.123");
10+
11+
expect(() => NumberValue.from(1.23e100)).toThrow();
12+
expect(() => NumberValue.from(Infinity)).toThrow();
13+
expect(() => NumberValue.from(-Infinity)).toThrow();
14+
expect(() => NumberValue.from(NaN)).toThrow();
15+
});
16+
17+
it("can be statically constructed from strings", () => {
18+
expect(NumberValue.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL);
19+
});
20+
21+
it("can be statically constructed from BigInts", () => {
22+
expect(NumberValue.from(BigInt(BIG_INT)).toString()).toEqual(BIG_INT);
23+
});
24+
25+
it("can convert to AttributeValue", () => {
26+
expect(NumberValue.from(BIG_DECIMAL).toAttributeValue()).toEqual({
27+
N: BIG_DECIMAL,
28+
});
29+
});
30+
31+
it("can convert to string", () => {
32+
expect(NumberValue.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL);
33+
});
34+
35+
it("can convert to BigInt", () => {
36+
expect(NumberValue.from(BIG_INT).toBigInt()).toEqual(BigInt(BIG_INT));
37+
});
38+
});

0 commit comments

Comments
 (0)