Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib-dynamodb): large number handling #5427

Merged
merged 6 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 105 additions & 26 deletions lib/lib-dynamodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ Responses from DynamoDB are unmarshalled into plain JavaScript objects
by the `DocumentClient`. The `DocumentClient` does not accept
`AttributeValue`s in favor of native JavaScript types.

| JavaScript Type | DynamoDB AttributeValue |
| :-------------------------------: | ----------------------- |
| String | S |
| Number / BigInt | N |
| Boolean | BOOL |
| null | NULL |
| Array | L |
| Object | M |
| Set\<Uint8Array, Blob, ...\> | BS |
| Set\<Number, BigInt\> | NS |
| Set\<String\> | SS |
| Uint8Array, Buffer, File, Blob... | B |
| JavaScript Type | DynamoDB AttributeValue |
| :--------------------------------: | ----------------------- |
| String | S |
| Number / BigInt / NumberValue | N |
| Boolean | BOOL |
| null | NULL |
| Array | L |
| Object | M |
| Set\<Uint8Array, Blob, ...\> | BS |
| Set\<Number, BigInt, NumberValue\> | NS |
| Set\<String\> | SS |
| Uint8Array, Buffer, File, Blob... | B |

### Example

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

```js
const marshallOptions = {
// Whether to automatically convert empty strings, blobs, and sets to `null`.
convertEmptyValues: false, // false, by default.
// Whether to remove undefined values while marshalling.
removeUndefinedValues: false, // false, by default.
// Whether to convert typeof object to map attribute.
convertClassInstanceToMap: false, // false, by default.
};

const unmarshallOptions = {
// Whether to return numbers as a string instead of converting them to native JavaScript numbers.
wrapNumbers: false, // false, by default.
};
```ts
export interface marshallOptions {
/**
* Whether to automatically convert empty strings, blobs, and sets to `null`
*/
convertEmptyValues?: boolean;
/**
* Whether to remove undefined values while marshalling.
*/
removeUndefinedValues?: boolean;
/**
* Whether to convert typeof object to map attribute.
*/
convertClassInstanceToMap?: boolean;
/**
* Whether to convert the top level container
* if it is a map or list.
*
* Default is true when using the DynamoDBDocumentClient,
* but false if directly using the marshall function (backwards compatibility).
*/
convertTopLevelContainer?: boolean;
}

export interface unmarshallOptions {
/**
* Whether to return numbers as a string instead of converting them to native JavaScript numbers.
* This allows for the safe round-trip transport of numbers of arbitrary size.
*/
wrapNumbers?: boolean;

/**
* When true, skip wrapping the data in `{ M: data }` before converting.
*
* Default is true when using the DynamoDBDocumentClient,
* but false if directly using the unmarshall function (backwards compatibility).
*/
convertWithoutMapWrapper?: boolean;
}

const marshallOptions: marshallOptions = {};
const unmarshallOptions: unmarshallOptions = {};

const translateConfig = { marshallOptions, unmarshallOptions };

Expand Down Expand Up @@ -160,6 +188,57 @@ await ddbDocClient.put({
});
```

### Large Numbers and `NumberValue`.

On the input or marshalling side, the class `NumberValue` can be used
anywhere to represent a DynamoDB number value, even small numbers.

```ts
import { DynamoDB } from "@aws-sdk/client-dynamodb";
import { NumberValue, DynamoDBDocument } from "@aws-sdk/lib-dynamodb";

// Note, the client will not validate the acceptability of the number
// in terms of size or format.
// It is only here to preserve your precise representation.
const client = DynamoDBDocument.from(new DynamoDB({}));

await client.put({
Item: {
id: 1,
smallNumber: NumberValue.from("123"),
bigNumber: NumberValue.from("1000000000000000000000.000000000001"),
nSet: new Set([123, NumberValue.from("456"), 789]),
},
});
```

On the output or unmarshalling side, the class `NumberValue` is used
depending on your setting for the `unmarshallOptions` flag `wrapnumbers`,
shown above.

```ts
import { DynamoDB } from "@aws-sdk/client-dynamodb";
import { NumberValue, DynamoDBDocument } from "@aws-sdk/lib-dynamodb";

const client = DynamoDBDocument.from(new DynamoDB({}));

const response = await client.get({
Key: {
id: 1,
},
});

/**
* Numbers in the response may be a number, a BigInt, or a NumberValue depending
* on how you set `wrapNumbers`.
*/
const value = response.Item.bigNumber;
```

`NumberValue` does not provide a way to do mathematical operations on itself.
To do mathematical operations, take the string value of `NumberValue` by calling
`.toString()` and supply it to your chosen big number implementation.

### Client and Command middleware stacks

As with other AWS SDK for JavaScript v3 clients, you can apply middleware functions
Expand Down
2 changes: 2 additions & 0 deletions lib/lib-dynamodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export * from "./DynamoDBDocumentClient";
// smithy-typescript generated code
export * from "./commands";
export * from "./pagination";

export { NumberValueImpl as NumberValue } from "@aws-sdk/util-dynamodb";
57 changes: 41 additions & 16 deletions lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ExecuteStatementCommandOutput,
ExecuteTransactionCommandOutput,
GetCommandOutput,
NumberValue,
PutCommandOutput,
QueryCommandOutput,
ScanCommandOutput,
Expand All @@ -32,6 +33,9 @@ describe(DynamoDBDocument.name, () => {
marshallOptions: {
convertTopLevelContainer: true,
},
unmarshallOptions: {
wrapNumbers: true,
},
});

function throwIfError(e: unknown) {
Expand Down Expand Up @@ -76,30 +80,39 @@ describe(DynamoDBDocument.name, () => {
const data = {
null: null,
string: "myString",
number: 1,
number: NumberValue.from(1),
bigInt: NumberValue.from(
"10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
),
bigNumber: NumberValue.from("3210000000000000000.0000000000000123"),
boolean: true,
sSet: new Set(["my", "string", "set"]),
nSet: new Set([2, 3, 4]),
nSet: new Set([2, 3, 4].map(NumberValue.from)),
list: [
null,
"myString",
1,
NumberValue.from(1),
true,
new Set(["my", "string", "set"]),
new Set([2, 3, 4]),
["listInList", 1, null],
new Set([NumberValue.from(2), NumberValue.from(3), NumberValue.from(4)]),
new Set([
NumberValue.from("3210000000000000000.0000000000000123"),
NumberValue.from("3210000000000000001.0000000000000123"),
NumberValue.from("3210000000000000002.0000000000000123"),
]),
["listInList", NumberValue.from(1), null],
{
mapInList: "mapInList",
},
],
map: {
null: null,
string: "myString",
number: 1,
number: NumberValue.from(1),
boolean: true,
sSet: new Set(["my", "string", "set"]),
nSet: new Set([2, 3, 4]),
listInMap: ["listInMap", 1, null],
nSet: new Set([2, 3, 4].map(NumberValue.from)),
listInMap: ["listInMap", NumberValue.from(1), null],
mapInMap: { mapInMap: "mapInMap" },
},
};
Expand All @@ -116,6 +129,9 @@ describe(DynamoDBDocument.name, () => {
if (input instanceof Set) {
return new Set([...input].map(updateTransform)) as T;
}
if (input instanceof NumberValue) {
return NumberValue.from(input.toString()) as T;
}
return Object.entries(input).reduce((acc, [k, v]) => {
acc[updateTransform(k)] = updateTransform(v);
return acc;
Expand Down Expand Up @@ -436,28 +452,37 @@ describe(DynamoDBDocument.name, () => {
expect(updateTransform(data)).toEqual({
"null-x": null,
"string-x": "myString-x",
"number-x": 2,
"number-x": NumberValue.from(1),
"bigInt-x": NumberValue.from(
"10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
),
"bigNumber-x": NumberValue.from("3210000000000000000.0000000000000123"),
"boolean-x": false,
"sSet-x": new Set(["my-x", "string-x", "set-x"]),
"nSet-x": new Set([3, 4, 5]),
"nSet-x": new Set([2, 3, 4].map(NumberValue.from)),
"list-x": [
null,
"myString-x",
2,
NumberValue.from(1),
false,
new Set(["my-x", "string-x", "set-x"]),
new Set([3, 4, 5]),
["listInList-x", 2, null],
new Set([2, 3, 4].map(NumberValue.from)),
new Set([
NumberValue.from("3210000000000000000.0000000000000123"),
NumberValue.from("3210000000000000001.0000000000000123"),
NumberValue.from("3210000000000000002.0000000000000123"),
]),
["listInList-x", NumberValue.from(1), null],
{ "mapInList-x": "mapInList-x" },
],
"map-x": {
"null-x": null,
"string-x": "myString-x",
"number-x": 2,
"number-x": NumberValue.from(1),
"boolean-x": false,
"sSet-x": new Set(["my-x", "string-x", "set-x"]),
"nSet-x": new Set([3, 4, 5]),
"listInMap-x": ["listInMap-x", 2, null],
"nSet-x": new Set([2, 3, 4].map(NumberValue.from)),
"listInMap-x": ["listInMap-x", NumberValue.from(1), null],
"mapInMap-x": { "mapInMap-x": "mapInMap-x" },
},
});
Expand Down
38 changes: 38 additions & 0 deletions packages/util-dynamodb/src/NumberValue.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NumberValue } from "./NumberValue";

const BIG_DECIMAL =
"123456789012345678901234567890123456789012345678901234567890.123456789012345678901234567890123456789012345678901234567890";
const BIG_INT = "123456789012345678901234567890123456789012345678901234567890";

describe(NumberValue.name, () => {
it("can be statically constructed from numbers", () => {
expect(NumberValue.from(123.123).toString()).toEqual("123.123");

expect(() => NumberValue.from(1.23e100)).toThrow();
expect(() => NumberValue.from(Infinity)).toThrow();
expect(() => NumberValue.from(-Infinity)).toThrow();
expect(() => NumberValue.from(NaN)).toThrow();
});

it("can be statically constructed from strings", () => {
expect(NumberValue.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL);
});

it("can be statically constructed from BigInts", () => {
expect(NumberValue.from(BigInt(BIG_INT)).toString()).toEqual(BIG_INT);
});

it("can convert to AttributeValue", () => {
expect(NumberValue.from(BIG_DECIMAL).toAttributeValue()).toEqual({
N: BIG_DECIMAL,
});
});

it("can convert to string", () => {
expect(NumberValue.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL);
});

it("can convert to BigInt", () => {
expect(NumberValue.from(BIG_INT).toBigInt()).toEqual(BigInt(BIG_INT));
});
});
Loading