Skip to content

Commit

Permalink
feat(lib-dynamodb): large number handling, remove extra unmarshall op…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
kuhe committed Oct 31, 2023
1 parent 6df2ebc commit 2ca27ed
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 205 deletions.
84 changes: 28 additions & 56 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 / DynamoDBNumber | N |
| Boolean | BOOL |
| null | NULL |
| Array | L |
| Object | M |
| Set\<Uint8Array, Blob, ...\> | BS |
|Set\<Number, BigInt, DynamoDBNumber\>| 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 @@ -126,9 +126,6 @@ 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.
*
* @deprecated set useDynamoDBNumberWrapper option instead.
* If useDynamoDBNumberWrapper option is set, the value of wrapNumbers will be ignored.
*/
wrapNumbers?: boolean;

Expand All @@ -139,18 +136,6 @@ export interface unmarshallOptions {
* but false if directly using the unmarshall function (backwards compatibility).
*/
convertWithoutMapWrapper?: boolean;

/**
* When to use the DynamoDBNumber wrapper class for numbers.
* Default="never". An error will be thrown for large numbers.
*
* "bigNumbersOnly": only numbers exceeding MAX_SAFE_INTEGER in absolute terms
* will be wrapped. Your application code must handle the divergent result
* types of `number | DynamoDBNumber`.
*
* "allNumbers": all numbers will be wrapped with the DynamoDBNumber class.
*/
useDynamoDBNumberWrapper?: "never" | "bigNumbersOnly" | "allNumbers";
}

const marshallOptions: marshallOptions = {};
Expand Down Expand Up @@ -203,14 +188,14 @@ await ddbDocClient.put({
});
```

### Large Numbers and `DynamoDBNumber`.
### Large Numbers and `NumberValue`.

On the input or marshalling side, the class `DynamoDBNumber` can be used
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 { DynamoDBNumber, DynamoDBDocument } from "@aws-sdk/lib-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.
Expand All @@ -220,53 +205,40 @@ const client = DynamoDBDocument.from(new DynamoDB({}));
await client.put({
Item: {
id: 1,
smallNumber: DynamoDBNumber.from("123"),
bigNumber: DynamoDBNumber.from("1000000000000000000000.000000000001"),
nSet: new Set([
DynamoDBNumber.from("123"),
DynamoDBNumber.from("456")
])
}
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 `DynamoDBNumber` is used
depending on your setting for the `unmarshallOptions` flag `useDynamoDBNumberWrapper`,
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 { DynamoDBNumber, DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import { NumberValue, DynamoDBDocument } from "@aws-sdk/lib-dynamodb";

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

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

/**
* Numbers in the response may be a number, a BigInt, or a DynamoDBNumber depending
* on how you set `useDynamoDBNumberWrapper`.
*
* In the example above, if setting useDynamoDBNumberWrapper=never,
* the operation will throw an error because the decimal
* stored in the database cannot be converted to the fallback BigInt.
*
* `bigNumbersOnly` converts only numbers exceeding [MAX|MIN]_SAFE_INTEGER.
*
* `allNumbers` converts all numbers. For dealing with small decimals that
* need precision but do not exceed the default integer limits.
* Numbers in the response may be a number, a BigInt, or a NumberValue depending
* on how you set `wrapNumbers`.
*/
const value = response.Item.bigNumber;
```

`DynamoDBNumber` does not provide a way to do mathematical operations on itself.
To do mathematical operations, take the string value of `DynamoDBNumber` by calling
`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: 1 addition & 1 deletion lib/lib-dynamodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export * from "./DynamoDBDocumentClient";
export * from "./commands";
export * from "./pagination";

export { DynamoDBNumber } from "@aws-sdk/util-dynamodb";
export { NumberValueImpl as NumberValue } from "@aws-sdk/util-dynamodb";
60 changes: 30 additions & 30 deletions lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
BatchGetCommandOutput,
BatchWriteCommandOutput,
DynamoDBDocument,
DynamoDBNumber,
ExecuteStatementCommandOutput,
ExecuteTransactionCommandOutput,
GetCommandOutput,
NumberValue,
PutCommandOutput,
QueryCommandOutput,
ScanCommandOutput,
Expand All @@ -34,7 +34,7 @@ describe(DynamoDBDocument.name, () => {
convertTopLevelContainer: true,
},
unmarshallOptions: {
useDynamoDBNumberWrapper: "bigNumbersOnly",
wrapNumbers: true,
},
});

Expand Down Expand Up @@ -80,39 +80,39 @@ describe(DynamoDBDocument.name, () => {
const data = {
null: null,
string: "myString",
number: 1,
bigInt: DynamoDBNumber.from(
number: NumberValue.from(1),
bigInt: NumberValue.from(
"10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
),
bigNumber: DynamoDBNumber.from("3210000000000000000.0000000000000123"),
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]),
new Set([NumberValue.from(2), NumberValue.from(3), NumberValue.from(4)]),
new Set([
DynamoDBNumber.from("3210000000000000000.0000000000000123"),
DynamoDBNumber.from("3210000000000000001.0000000000000123"),
DynamoDBNumber.from("3210000000000000002.0000000000000123"),
NumberValue.from("3210000000000000000.0000000000000123"),
NumberValue.from("3210000000000000001.0000000000000123"),
NumberValue.from("3210000000000000002.0000000000000123"),
]),
["listInList", 1, null],
["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 @@ -129,8 +129,8 @@ describe(DynamoDBDocument.name, () => {
if (input instanceof Set) {
return new Set([...input].map(updateTransform)) as T;
}
if (input instanceof DynamoDBNumber) {
return DynamoDBNumber.from(input.toString()) 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);
Expand Down Expand Up @@ -452,37 +452,37 @@ describe(DynamoDBDocument.name, () => {
expect(updateTransform(data)).toEqual({
"null-x": null,
"string-x": "myString-x",
"number-x": 2,
"bigInt-x": DynamoDBNumber.from(
"number-x": NumberValue.from(1),
"bigInt-x": NumberValue.from(
"10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
),
"bigNumber-x": DynamoDBNumber.from("3210000000000000000.0000000000000123"),
"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]),
new Set([2, 3, 4].map(NumberValue.from)),
new Set([
DynamoDBNumber.from("3210000000000000000.0000000000000123"),
DynamoDBNumber.from("3210000000000000001.0000000000000123"),
DynamoDBNumber.from("3210000000000000002.0000000000000123"),
NumberValue.from("3210000000000000000.0000000000000123"),
NumberValue.from("3210000000000000001.0000000000000123"),
NumberValue.from("3210000000000000002.0000000000000123"),
]),
["listInList-x", 2, null],
["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: 0 additions & 38 deletions packages/util-dynamodb/src/DynamoDBNumber.spec.ts

This file was deleted.

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));
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NumberValue as INumberValue } from "./models";

/**
*
* Class for storing DynamoDB numbers that exceed the scale of
Expand All @@ -11,8 +13,8 @@
* @public
*
*/
export class DynamoDBNumber {
private value: string;
export class NumberValue implements INumberValue {
public value: string;

/**
* This class does not validate that your string input is a valid number.
Expand All @@ -35,7 +37,7 @@ export class DynamoDBNumber {

if (imprecise) {
throw new Error(
`DynamoDBNumber should not be initialized with an imprecise number=${valueOf}. Use a string instead.`
`NumberValue should not be initialized with an imprecise number=${valueOf}. Use a string instead.`
);
}
}
Expand All @@ -46,7 +48,7 @@ export class DynamoDBNumber {
* @param value - a precise number, or any BigInt or string, or AttributeValue.
*/
public static from(value: number | Number | BigInt | string | { N: string }) {
return new DynamoDBNumber(value);
return new NumberValue(value);
}

/**
Expand Down
Loading

0 comments on commit 2ca27ed

Please sign in to comment.