From 55747360901f18fa45b0404ef2fad9583b76eb95 Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 27 Oct 2023 19:02:09 +0000 Subject: [PATCH 1/6] feat(lib-dynamodb): large number handling --- lib/lib-dynamodb/README.md | 4 +- lib/lib-dynamodb/src/index.ts | 2 + .../src/test/lib-dynamodb.e2e.spec.ts | 9 ++ .../util-dynamodb/src/DynamoDBNumber.spec.ts | 43 +++++++++ packages/util-dynamodb/src/DynamoDBNumber.ts | 96 +++++++++++++++++++ .../util-dynamodb/src/convertToAttr.spec.ts | 22 +++++ packages/util-dynamodb/src/convertToAttr.ts | 10 +- .../util-dynamodb/src/convertToNative.spec.ts | 32 ++++++- packages/util-dynamodb/src/convertToNative.ts | 29 +++++- packages/util-dynamodb/src/index.ts | 1 + packages/util-dynamodb/src/unmarshall.ts | 16 ++++ 11 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 packages/util-dynamodb/src/DynamoDBNumber.spec.ts create mode 100644 packages/util-dynamodb/src/DynamoDBNumber.ts diff --git a/lib/lib-dynamodb/README.md b/lib/lib-dynamodb/README.md index e821136fa2ba..1edcbb4761a7 100644 --- a/lib/lib-dynamodb/README.md +++ b/lib/lib-dynamodb/README.md @@ -23,13 +23,13 @@ by the `DocumentClient`. The `DocumentClient` does not accept | JavaScript Type | DynamoDB AttributeValue | | :-------------------------------: | ----------------------- | | String | S | -| Number / BigInt | N | +| Number / BigInt / DynamoDBNumber | N | | Boolean | BOOL | | null | NULL | | Array | L | | Object | M | | Set\ | BS | -| Set\ | NS | +|Set\| NS | | Set\ | SS | | Uint8Array, Buffer, File, Blob... | B | diff --git a/lib/lib-dynamodb/src/index.ts b/lib/lib-dynamodb/src/index.ts index c0a00ae48911..a21b7ea265c1 100644 --- a/lib/lib-dynamodb/src/index.ts +++ b/lib/lib-dynamodb/src/index.ts @@ -4,3 +4,5 @@ export * from "./DynamoDBDocumentClient"; // smithy-typescript generated code export * from "./commands"; export * from "./pagination"; + +export { DynamoDBNumber } from "@aws-sdk/util-dynamodb"; diff --git a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts index 4d24311c89b7..90dbe8655509 100644 --- a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts +++ b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts @@ -12,6 +12,7 @@ import { BatchGetCommandOutput, BatchWriteCommandOutput, DynamoDBDocument, + DynamoDBNumber, ExecuteStatementCommandOutput, ExecuteTransactionCommandOutput, GetCommandOutput, @@ -32,6 +33,9 @@ describe(DynamoDBDocument.name, () => { marshallOptions: { convertTopLevelContainer: true, }, + unmarshallOptions: { + useDynamoDBNumberWrapper: "bigNumbersOnly", + }, }); function throwIfError(e: unknown) { @@ -77,6 +81,7 @@ describe(DynamoDBDocument.name, () => { null: null, string: "myString", number: 1, + bigNumber: DynamoDBNumber.from("3210000000000000000.0000000000000123"), boolean: true, sSet: new Set(["my", "string", "set"]), nSet: new Set([2, 3, 4]), @@ -116,6 +121,9 @@ describe(DynamoDBDocument.name, () => { if (input instanceof Set) { return new Set([...input].map(updateTransform)) as T; } + if (input instanceof DynamoDBNumber) { + return DynamoDBNumber.from(input.toString() + "4") as T; + } return Object.entries(input).reduce((acc, [k, v]) => { acc[updateTransform(k)] = updateTransform(v); return acc; @@ -437,6 +445,7 @@ describe(DynamoDBDocument.name, () => { "null-x": null, "string-x": "myString-x", "number-x": 2, + "bigNumber-x": DynamoDBNumber.from("3210000000000000000.00000000000001234"), "boolean-x": false, "sSet-x": new Set(["my-x", "string-x", "set-x"]), "nSet-x": new Set([3, 4, 5]), diff --git a/packages/util-dynamodb/src/DynamoDBNumber.spec.ts b/packages/util-dynamodb/src/DynamoDBNumber.spec.ts new file mode 100644 index 000000000000..b942c5954ebf --- /dev/null +++ b/packages/util-dynamodb/src/DynamoDBNumber.spec.ts @@ -0,0 +1,43 @@ +import { DynamoDBNumber } from "./DynamoDBNumber"; + +const BIG_DECIMAL = + "123456789012345678901234567890123456789012345678901234567890.123456789012345678901234567890123456789012345678901234567890"; +const BIG_INT = "123456789012345678901234567890123456789012345678901234567890"; + +describe(DynamoDBNumber.name, () => { + it("can be statically constructed from numbers", () => { + expect(DynamoDBNumber.from(123.123).toString()).toEqual("123.123"); + + expect(() => DynamoDBNumber.from(1.23e100)).toThrow(); + expect(() => DynamoDBNumber.from(Infinity)).toThrow(); + expect(() => DynamoDBNumber.from(-Infinity)).toThrow(); + expect(() => DynamoDBNumber.from(NaN)).toThrow(); + }); + + it("can be statically constructed from strings", () => { + expect(DynamoDBNumber.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL); + }); + + it("can be statically constructed from BigInts", () => { + expect(DynamoDBNumber.from(BigInt(BIG_INT)).toString()).toEqual(BIG_INT); + }); + + it("can convert to number", () => { + expect(DynamoDBNumber.from(BIG_DECIMAL).toNumber(true)).toEqual(Number(BIG_DECIMAL)); + expect(() => DynamoDBNumber.from(BIG_DECIMAL).toNumber(false)).toThrow(); + }); + + it("can convert to AttributeValue", () => { + expect(DynamoDBNumber.from(BIG_DECIMAL).toAttributeValue()).toEqual({ + N: BIG_DECIMAL, + }); + }); + + it("can convert to string", () => { + expect(DynamoDBNumber.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL); + }); + + it("can convert to BigInt", () => { + expect(DynamoDBNumber.from(BIG_INT).toBigInt()).toEqual(BigInt(BIG_INT)); + }); +}); diff --git a/packages/util-dynamodb/src/DynamoDBNumber.ts b/packages/util-dynamodb/src/DynamoDBNumber.ts new file mode 100644 index 000000000000..e64bf154f784 --- /dev/null +++ b/packages/util-dynamodb/src/DynamoDBNumber.ts @@ -0,0 +1,96 @@ +/** + * + * Class for storing DynamoDB numbers that exceed the scale of + * JavaScript's MAX_SAFE_INTEGER and MIN_SAFE_INTEGER. + * + * This class does not support mathematical operations in JavaScript. + * Convert the contained string value to your application-specific + * large number implementation to perform mathematical operations. + * + */ +export class DynamoDBNumber { + private value: string; + + /** + * This class does not validate that your string input is a valid number. + * + * @param value - a precise number, or any BigInt or string, or AttributeValue. + */ + public constructor(value: number | Number | BigInt | string | { N: string }) { + if (typeof value === "object" && "N" in value) { + this.value = String(value.N); + } else { + this.value = String(value); + } + + const valueOf = typeof value.valueOf() === "number" ? (value.valueOf() as number) : 0; + const imprecise = + valueOf > Number.MAX_SAFE_INTEGER || + valueOf < Number.MIN_SAFE_INTEGER || + Math.abs(valueOf) === Infinity || + Number.isNaN(valueOf); + + if (imprecise) { + throw new Error( + `DynamoDBNumber should not be initialized with an imprecise number=${valueOf}. Use a string instead.` + ); + } + } + + /** + * This class does not validate that your string input is a valid number. + * + * @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); + } + + /** + * @returns the AttributeValue form for DynamoDB. + */ + public toAttributeValue() { + return { + N: this.toString(), + }; + } + + /** + * @param allowImprecision - if true, will throw if the number cannot be precisely conveyed as a JavaScript number. + * @returns the number representation. + */ + public toNumber(allowImprecision = false) { + const stringValue = this.toString(); + const numberValue = Number(stringValue); + if (!allowImprecision && String(numberValue) !== stringValue) { + throw new Error(`Cannot convert ${stringValue} with precision to number.`); + } + return numberValue; + } + + /** + * @returns BigInt representation. + * + * @throws SyntaxError if the string representation is not convertable to a BigInt. + */ + public toBigInt() { + const stringValue = this.toString(); + return BigInt(stringValue); + } + + /** + * @override + * + * @returns string representation. This is the canonical format in DynamoDB. + */ + public toString() { + return String(this.value); + } + + /** + * @override + */ + public valueOf() { + return this.toString(); + } +} diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index 26be9d6aab11..cbdd3b15a00c 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -4,6 +4,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToAttr } from "./convertToAttr"; +import { DynamoDBNumber } from "./DynamoDBNumber"; import { marshallOptions } from "./marshall"; import { NativeAttributeValue } from "./models"; @@ -115,6 +116,27 @@ describe("convertToAttr", () => { }); }); + describe("DynamoDBNumber", () => { + [true, false].forEach((convertClassInstanceToMap) => { + const maxSafe = BigInt(Number.MAX_SAFE_INTEGER); + [ + // @ts-expect-error BigInt literals are not available when targeting lower than ES2020. + 1n, + // @ts-expect-error BigInt literals are not available when targeting lower than ES2020. + maxSafe * 2n, + // @ts-expect-error BigInt literals are not available when targeting lower than ES2020. + maxSafe * -2n, + BigInt(Number.MAX_VALUE), + BigInt("0x1fffffffffffff"), + BigInt("0b11111111111111111111111111111111111111111111111111111"), + ].forEach((num) => { + it(`returns for DynamoDBNumber: ${num}`, () => { + expect(convertToAttr(DynamoDBNumber.from(num), { convertClassInstanceToMap })).toEqual({ N: num.toString() }); + }); + }); + }); + }); + describe("binary", () => { [true, false].forEach((convertClassInstanceToMap) => { const buffer = new ArrayBuffer(64); diff --git a/packages/util-dynamodb/src/convertToAttr.ts b/packages/util-dynamodb/src/convertToAttr.ts index d73527401b0e..81417245bfdd 100644 --- a/packages/util-dynamodb/src/convertToAttr.ts +++ b/packages/util-dynamodb/src/convertToAttr.ts @@ -1,5 +1,6 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; +import { DynamoDBNumber } from "./DynamoDBNumber"; import { marshallOptions } from "./marshall"; import { NativeAttributeBinary, NativeAttributeValue, NativeScalarAttributeValue } from "./models"; @@ -37,6 +38,8 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti return { BOOL: data.valueOf() }; } else if (typeof data === "number" || data?.constructor?.name === "Number") { return convertToNumberAttr(data); + } else if (data instanceof DynamoDBNumber) { + return data.toAttributeValue(); } else if (typeof data === "bigint") { return convertToBigIntAttr(data); } else if (typeof data === "string" || data?.constructor?.name === "String") { @@ -76,7 +79,12 @@ const convertToSetAttr = ( } const item = setToOperate.values().next().value; - if (typeof item === "number") { + + if (item instanceof DynamoDBNumber) { + return { + NS: Array.from(setToOperate).map((_) => _.toString()), + }; + } else if (typeof item === "number") { return { NS: Array.from(setToOperate) .map(convertToNumberAttr) diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index 5282850a214c..a8b92163a285 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -1,6 +1,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToNative } from "./convertToNative"; +import { DynamoDBNumber } from "./DynamoDBNumber"; import { NativeAttributeValue } from "./models"; describe("convertToNative", () => { @@ -63,7 +64,9 @@ describe("convertToNative", () => { (BigInt as any) = undefined; expect(() => { convertToNative({ N: numString }); - }).toThrowError(`${numString} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); + }).toThrowError( + `${numString} is outside SAFE_INTEGER bounds. Set options.useDynamoDBNumberWrapper to get string value.` + ); BigInt = BigIntConstructor; }); @@ -88,7 +91,32 @@ describe("convertToNative", () => { it(`throws if number cannot be converted into BigInt: ${numString}`, () => { expect(() => { convertToNative({ N: numString }); - }).toThrowError(`${numString} can't be converted to BigInt. Set options.wrapNumbers to get string value.`); + }).toThrowError( + `${numString} can't be converted to BigInt. Set options.useDynamoDBNumberWrapper to get string value.` + ); + }); + }); + + describe("options.useDynamoDBNumberWrapper enum", () => { + it("options.useDynamoDBNumberWrapper=never", () => { + expect(convertToNative({ N: "1" }, { useDynamoDBNumberWrapper: "never" })).toEqual(1); + expect(convertToNative({ N: "9007199254740992" }, { useDynamoDBNumberWrapper: "never" })).toEqual( + BigInt("9007199254740992") + ); + }); + it("options.useDynamoDBNumberWrapper=bigNumbersOnly", () => { + expect(convertToNative({ N: "1" }, { useDynamoDBNumberWrapper: "bigNumbersOnly" })).toEqual(1); + expect(convertToNative({ N: "9007199254740992" }, { useDynamoDBNumberWrapper: "bigNumbersOnly" })).toEqual( + DynamoDBNumber.from("9007199254740992") + ); + }); + it("options.useDynamoDBNumberWrapper=allNumbers", () => { + expect(convertToNative({ N: "1" }, { useDynamoDBNumberWrapper: "allNumbers" })).toEqual( + DynamoDBNumber.from("1") + ); + expect(convertToNative({ N: "9007199254740992" }, { useDynamoDBNumberWrapper: "allNumbers" })).toEqual( + DynamoDBNumber.from("9007199254740992") + ); }); }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 89ab5777fe6a..85c00d5086ff 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -1,5 +1,6 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; +import { DynamoDBNumber } from "./DynamoDBNumber"; import { NativeAttributeValue, NumberValue } from "./models"; import { unmarshallOptions } from "./unmarshall"; @@ -41,22 +42,40 @@ export const convertToNative = (data: AttributeValue, options?: unmarshallOption throw new Error(`No value defined: ${JSON.stringify(data)}`); }; -const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | NumberValue => { - if (options?.wrapNumbers) { +const convertNumber = ( + numString: string, + options?: unmarshallOptions +): number | bigint | NumberValue | DynamoDBNumber => { + if (options?.wrapNumbers && !options?.useDynamoDBNumberWrapper) { return { value: numString }; } + if (options?.useDynamoDBNumberWrapper === "allNumbers") { + return DynamoDBNumber.from(numString); + } + const num = Number(numString); const infinityValues = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]; - if ((num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) && !infinityValues.includes(num)) { + const isLargeFiniteNumber = + (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) && !infinityValues.includes(num); + + if (options?.useDynamoDBNumberWrapper === "bigNumbersOnly" && isLargeFiniteNumber) { + return DynamoDBNumber.from(numString); + } + + if (isLargeFiniteNumber) { if (typeof BigInt === "function") { try { return BigInt(numString); } catch (error) { - throw new Error(`${numString} can't be converted to BigInt. Set options.wrapNumbers to get string value.`); + throw new Error( + `${numString} can't be converted to BigInt. Set options.useDynamoDBNumberWrapper to get string value.` + ); } } else { - throw new Error(`${numString} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); + throw new Error( + `${numString} is outside SAFE_INTEGER bounds. Set options.useDynamoDBNumberWrapper to get string value.` + ); } } return num; diff --git a/packages/util-dynamodb/src/index.ts b/packages/util-dynamodb/src/index.ts index 0d6f5d7852bd..5bdf53908290 100644 --- a/packages/util-dynamodb/src/index.ts +++ b/packages/util-dynamodb/src/index.ts @@ -1,3 +1,4 @@ +export * from "./DynamoDBNumber"; export * from "./convertToAttr"; export * from "./convertToNative"; export * from "./marshall"; diff --git a/packages/util-dynamodb/src/unmarshall.ts b/packages/util-dynamodb/src/unmarshall.ts index c705e93ea70f..ee77a5192632 100644 --- a/packages/util-dynamodb/src/unmarshall.ts +++ b/packages/util-dynamodb/src/unmarshall.ts @@ -10,8 +10,12 @@ 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; + /** * When true, skip wrapping the data in `{ M: data }` before converting. * @@ -19,6 +23,18 @@ 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"; } /** From f478f6cb8f4375695a78f076e3e77f0f5f4bb39f Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 30 Oct 2023 15:12:33 +0000 Subject: [PATCH 2/6] feat(lib-dynamodb): large number handling docs and set test case --- lib/lib-dynamodb/README.md | 135 ++++++++++++++++-- .../src/test/lib-dynamodb.e2e.spec.ts | 10 ++ packages/util-dynamodb/src/unmarshall.ts | 1 + 3 files changed, 132 insertions(+), 14 deletions(-) diff --git a/lib/lib-dynamodb/README.md b/lib/lib-dynamodb/README.md index 1edcbb4761a7..f7b81432845f 100644 --- a/lib/lib-dynamodb/README.md +++ b/lib/lib-dynamodb/README.md @@ -98,20 +98,63 @@ 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. + * + * @deprecated set useDynamoDBNumberWrapper option instead. + * If useDynamoDBNumberWrapper option is set, the value of wrapNumbers will be ignored. + */ + 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; + + /** + * 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 = {}; +const unmarshallOptions: unmarshallOptions = {}; const translateConfig = { marshallOptions, unmarshallOptions }; @@ -160,6 +203,70 @@ await ddbDocClient.put({ }); ``` +### Large Numbers and `DynamoDBNumber`. + +On the input or marshalling side, the class `DynamoDBNumber` 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"; + +// 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: DynamoDBNumber.from("123"), + bigNumber: DynamoDBNumber.from("1000000000000000000000.000000000001"), + nSet: new Set([ + DynamoDBNumber.from("123"), + DynamoDBNumber.from("456") + ]) + } +}); +``` + +On the output or unmarshalling side, the class `DynamoDBNumber` is used +depending on your setting for the `unmarshallOptions` flag `useDynamoDBNumberWrapper`, +shown above. + +```ts +import { DynamoDB } from "@aws-sdk/client-dynamodb"; +import { DynamoDBNumber, 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 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. + */ +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 +`.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 diff --git a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts index 90dbe8655509..de7e524c9301 100644 --- a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts +++ b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts @@ -92,6 +92,11 @@ describe(DynamoDBDocument.name, () => { true, new Set(["my", "string", "set"]), new Set([2, 3, 4]), + new Set([ + DynamoDBNumber.from("3210000000000000000.0000000000000123"), + DynamoDBNumber.from("3210000000000000001.0000000000000123"), + DynamoDBNumber.from("3210000000000000002.0000000000000123"), + ]), ["listInList", 1, null], { mapInList: "mapInList", @@ -456,6 +461,11 @@ describe(DynamoDBDocument.name, () => { false, new Set(["my-x", "string-x", "set-x"]), new Set([3, 4, 5]), + new Set([ + DynamoDBNumber.from("3210000000000000000.00000000000001234"), + DynamoDBNumber.from("3210000000000000001.00000000000001234"), + DynamoDBNumber.from("3210000000000000002.00000000000001234"), + ]), ["listInList-x", 2, null], { "mapInList-x": "mapInList-x" }, ], diff --git a/packages/util-dynamodb/src/unmarshall.ts b/packages/util-dynamodb/src/unmarshall.ts index ee77a5192632..47de1bcbf100 100644 --- a/packages/util-dynamodb/src/unmarshall.ts +++ b/packages/util-dynamodb/src/unmarshall.ts @@ -33,6 +33,7 @@ export interface unmarshallOptions { * types of `number | DynamoDBNumber`. * * "allNumbers": all numbers will be wrapped with the DynamoDBNumber class. + * For example, if dealing with small decimals needing higher precision. */ useDynamoDBNumberWrapper?: "never" | "bigNumbersOnly" | "allNumbers"; } From 4005ef243ad1ad449c3bb9dea5993e1bca0b41e7 Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 30 Oct 2023 15:21:33 +0000 Subject: [PATCH 3/6] feat(lib-dynamodb): set release tag --- packages/util-dynamodb/src/DynamoDBNumber.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/util-dynamodb/src/DynamoDBNumber.ts b/packages/util-dynamodb/src/DynamoDBNumber.ts index e64bf154f784..234952e67381 100644 --- a/packages/util-dynamodb/src/DynamoDBNumber.ts +++ b/packages/util-dynamodb/src/DynamoDBNumber.ts @@ -1,12 +1,15 @@ /** * * Class for storing DynamoDB numbers that exceed the scale of - * JavaScript's MAX_SAFE_INTEGER and MIN_SAFE_INTEGER. + * JavaScript's MAX_SAFE_INTEGER and MIN_SAFE_INTEGER, or the + * decimal precision limit. * * This class does not support mathematical operations in JavaScript. * Convert the contained string value to your application-specific * large number implementation to perform mathematical operations. * + * @public + * */ export class DynamoDBNumber { private value: string; From 881166912f5b2bd3a3afe495fd28aa23773968b5 Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 30 Oct 2023 15:24:41 +0000 Subject: [PATCH 4/6] feat(lib-dynamodb): remove unsafe conversion feature --- packages/util-dynamodb/src/DynamoDBNumber.spec.ts | 5 ----- packages/util-dynamodb/src/DynamoDBNumber.ts | 13 ------------- 2 files changed, 18 deletions(-) diff --git a/packages/util-dynamodb/src/DynamoDBNumber.spec.ts b/packages/util-dynamodb/src/DynamoDBNumber.spec.ts index b942c5954ebf..a49dc3860ca5 100644 --- a/packages/util-dynamodb/src/DynamoDBNumber.spec.ts +++ b/packages/util-dynamodb/src/DynamoDBNumber.spec.ts @@ -22,11 +22,6 @@ describe(DynamoDBNumber.name, () => { expect(DynamoDBNumber.from(BigInt(BIG_INT)).toString()).toEqual(BIG_INT); }); - it("can convert to number", () => { - expect(DynamoDBNumber.from(BIG_DECIMAL).toNumber(true)).toEqual(Number(BIG_DECIMAL)); - expect(() => DynamoDBNumber.from(BIG_DECIMAL).toNumber(false)).toThrow(); - }); - it("can convert to AttributeValue", () => { expect(DynamoDBNumber.from(BIG_DECIMAL).toAttributeValue()).toEqual({ N: BIG_DECIMAL, diff --git a/packages/util-dynamodb/src/DynamoDBNumber.ts b/packages/util-dynamodb/src/DynamoDBNumber.ts index 234952e67381..370e5520671b 100644 --- a/packages/util-dynamodb/src/DynamoDBNumber.ts +++ b/packages/util-dynamodb/src/DynamoDBNumber.ts @@ -58,19 +58,6 @@ export class DynamoDBNumber { }; } - /** - * @param allowImprecision - if true, will throw if the number cannot be precisely conveyed as a JavaScript number. - * @returns the number representation. - */ - public toNumber(allowImprecision = false) { - const stringValue = this.toString(); - const numberValue = Number(stringValue); - if (!allowImprecision && String(numberValue) !== stringValue) { - throw new Error(`Cannot convert ${stringValue} with precision to number.`); - } - return numberValue; - } - /** * @returns BigInt representation. * From 6df2ebc9190eab2ed821a48a86ba121d0906b26b Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 30 Oct 2023 16:23:09 +0000 Subject: [PATCH 5/6] feat(lib-dynamodb): add 1e100 number test case --- .../src/test/lib-dynamodb.e2e.spec.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts index de7e524c9301..b0465859ff6c 100644 --- a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts +++ b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts @@ -81,6 +81,9 @@ describe(DynamoDBDocument.name, () => { null: null, string: "myString", number: 1, + bigInt: DynamoDBNumber.from( + "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ), bigNumber: DynamoDBNumber.from("3210000000000000000.0000000000000123"), boolean: true, sSet: new Set(["my", "string", "set"]), @@ -127,7 +130,7 @@ describe(DynamoDBDocument.name, () => { return new Set([...input].map(updateTransform)) as T; } if (input instanceof DynamoDBNumber) { - return DynamoDBNumber.from(input.toString() + "4") as T; + return DynamoDBNumber.from(input.toString()) as T; } return Object.entries(input).reduce((acc, [k, v]) => { acc[updateTransform(k)] = updateTransform(v); @@ -450,7 +453,10 @@ describe(DynamoDBDocument.name, () => { "null-x": null, "string-x": "myString-x", "number-x": 2, - "bigNumber-x": DynamoDBNumber.from("3210000000000000000.00000000000001234"), + "bigInt-x": DynamoDBNumber.from( + "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ), + "bigNumber-x": DynamoDBNumber.from("3210000000000000000.0000000000000123"), "boolean-x": false, "sSet-x": new Set(["my-x", "string-x", "set-x"]), "nSet-x": new Set([3, 4, 5]), @@ -462,9 +468,9 @@ describe(DynamoDBDocument.name, () => { new Set(["my-x", "string-x", "set-x"]), new Set([3, 4, 5]), new Set([ - DynamoDBNumber.from("3210000000000000000.00000000000001234"), - DynamoDBNumber.from("3210000000000000001.00000000000001234"), - DynamoDBNumber.from("3210000000000000002.00000000000001234"), + DynamoDBNumber.from("3210000000000000000.0000000000000123"), + DynamoDBNumber.from("3210000000000000001.0000000000000123"), + DynamoDBNumber.from("3210000000000000002.0000000000000123"), ]), ["listInList-x", 2, null], { "mapInList-x": "mapInList-x" }, From 2ca27edbc60a824b0b7e1e6ba6420191da66a386 Mon Sep 17 00:00:00 2001 From: George Fu Date: Tue, 31 Oct 2023 19:28:45 +0000 Subject: [PATCH 6/6] feat(lib-dynamodb): large number handling, remove extra unmarshall option --- lib/lib-dynamodb/README.md | 84 +++++++------------ lib/lib-dynamodb/src/index.ts | 2 +- .../src/test/lib-dynamodb.e2e.spec.ts | 60 ++++++------- .../util-dynamodb/src/DynamoDBNumber.spec.ts | 38 --------- .../util-dynamodb/src/NumberValue.spec.ts | 38 +++++++++ .../src/{DynamoDBNumber.ts => NumberValue.ts} | 10 ++- .../util-dynamodb/src/convertToAttr.spec.ts | 8 +- packages/util-dynamodb/src/convertToAttr.ts | 6 +- .../util-dynamodb/src/convertToNative.spec.ts | 32 +------ packages/util-dynamodb/src/convertToNative.ts | 29 ++----- packages/util-dynamodb/src/index.ts | 2 +- packages/util-dynamodb/src/unmarshall.ts | 16 ---- 12 files changed, 120 insertions(+), 205 deletions(-) delete mode 100644 packages/util-dynamodb/src/DynamoDBNumber.spec.ts create mode 100644 packages/util-dynamodb/src/NumberValue.spec.ts rename packages/util-dynamodb/src/{DynamoDBNumber.ts => NumberValue.ts} (88%) diff --git a/lib/lib-dynamodb/README.md b/lib/lib-dynamodb/README.md index f7b81432845f..72339396282f 100644 --- a/lib/lib-dynamodb/README.md +++ b/lib/lib-dynamodb/README.md @@ -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\ | BS | -|Set\| NS | -| Set\ | 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\ | BS | +| Set\ | NS | +| Set\ | SS | +| Uint8Array, Buffer, File, Blob... | B | ### Example @@ -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; @@ -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 = {}; @@ -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. @@ -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 diff --git a/lib/lib-dynamodb/src/index.ts b/lib/lib-dynamodb/src/index.ts index a21b7ea265c1..8b782ea2e99c 100644 --- a/lib/lib-dynamodb/src/index.ts +++ b/lib/lib-dynamodb/src/index.ts @@ -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"; diff --git a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts index b0465859ff6c..a6a5792a57b9 100644 --- a/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts +++ b/lib/lib-dynamodb/src/test/lib-dynamodb.e2e.spec.ts @@ -12,10 +12,10 @@ import { BatchGetCommandOutput, BatchWriteCommandOutput, DynamoDBDocument, - DynamoDBNumber, ExecuteStatementCommandOutput, ExecuteTransactionCommandOutput, GetCommandOutput, + NumberValue, PutCommandOutput, QueryCommandOutput, ScanCommandOutput, @@ -34,7 +34,7 @@ describe(DynamoDBDocument.name, () => { convertTopLevelContainer: true, }, unmarshallOptions: { - useDynamoDBNumberWrapper: "bigNumbersOnly", + wrapNumbers: true, }, }); @@ -80,27 +80,27 @@ 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", }, @@ -108,11 +108,11 @@ describe(DynamoDBDocument.name, () => { 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" }, }, }; @@ -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); @@ -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" }, }, }); diff --git a/packages/util-dynamodb/src/DynamoDBNumber.spec.ts b/packages/util-dynamodb/src/DynamoDBNumber.spec.ts deleted file mode 100644 index a49dc3860ca5..000000000000 --- a/packages/util-dynamodb/src/DynamoDBNumber.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { DynamoDBNumber } from "./DynamoDBNumber"; - -const BIG_DECIMAL = - "123456789012345678901234567890123456789012345678901234567890.123456789012345678901234567890123456789012345678901234567890"; -const BIG_INT = "123456789012345678901234567890123456789012345678901234567890"; - -describe(DynamoDBNumber.name, () => { - it("can be statically constructed from numbers", () => { - expect(DynamoDBNumber.from(123.123).toString()).toEqual("123.123"); - - expect(() => DynamoDBNumber.from(1.23e100)).toThrow(); - expect(() => DynamoDBNumber.from(Infinity)).toThrow(); - expect(() => DynamoDBNumber.from(-Infinity)).toThrow(); - expect(() => DynamoDBNumber.from(NaN)).toThrow(); - }); - - it("can be statically constructed from strings", () => { - expect(DynamoDBNumber.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL); - }); - - it("can be statically constructed from BigInts", () => { - expect(DynamoDBNumber.from(BigInt(BIG_INT)).toString()).toEqual(BIG_INT); - }); - - it("can convert to AttributeValue", () => { - expect(DynamoDBNumber.from(BIG_DECIMAL).toAttributeValue()).toEqual({ - N: BIG_DECIMAL, - }); - }); - - it("can convert to string", () => { - expect(DynamoDBNumber.from(BIG_DECIMAL).toString()).toEqual(BIG_DECIMAL); - }); - - it("can convert to BigInt", () => { - expect(DynamoDBNumber.from(BIG_INT).toBigInt()).toEqual(BigInt(BIG_INT)); - }); -}); diff --git a/packages/util-dynamodb/src/NumberValue.spec.ts b/packages/util-dynamodb/src/NumberValue.spec.ts new file mode 100644 index 000000000000..c7e3b047d1aa --- /dev/null +++ b/packages/util-dynamodb/src/NumberValue.spec.ts @@ -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)); + }); +}); diff --git a/packages/util-dynamodb/src/DynamoDBNumber.ts b/packages/util-dynamodb/src/NumberValue.ts similarity index 88% rename from packages/util-dynamodb/src/DynamoDBNumber.ts rename to packages/util-dynamodb/src/NumberValue.ts index 370e5520671b..d5cbe6f5aabb 100644 --- a/packages/util-dynamodb/src/DynamoDBNumber.ts +++ b/packages/util-dynamodb/src/NumberValue.ts @@ -1,3 +1,5 @@ +import { NumberValue as INumberValue } from "./models"; + /** * * Class for storing DynamoDB numbers that exceed the scale of @@ -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. @@ -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.` ); } } @@ -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); } /** diff --git a/packages/util-dynamodb/src/convertToAttr.spec.ts b/packages/util-dynamodb/src/convertToAttr.spec.ts index cbdd3b15a00c..07e77655522f 100644 --- a/packages/util-dynamodb/src/convertToAttr.spec.ts +++ b/packages/util-dynamodb/src/convertToAttr.spec.ts @@ -4,9 +4,9 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToAttr } from "./convertToAttr"; -import { DynamoDBNumber } from "./DynamoDBNumber"; import { marshallOptions } from "./marshall"; import { NativeAttributeValue } from "./models"; +import { NumberValue } from "./NumberValue"; describe("convertToAttr", () => { describe("null", () => { @@ -116,7 +116,7 @@ describe("convertToAttr", () => { }); }); - describe("DynamoDBNumber", () => { + describe("NumberValue", () => { [true, false].forEach((convertClassInstanceToMap) => { const maxSafe = BigInt(Number.MAX_SAFE_INTEGER); [ @@ -130,8 +130,8 @@ describe("convertToAttr", () => { BigInt("0x1fffffffffffff"), BigInt("0b11111111111111111111111111111111111111111111111111111"), ].forEach((num) => { - it(`returns for DynamoDBNumber: ${num}`, () => { - expect(convertToAttr(DynamoDBNumber.from(num), { convertClassInstanceToMap })).toEqual({ N: num.toString() }); + it(`returns for NumberValue: ${num}`, () => { + expect(convertToAttr(NumberValue.from(num), { convertClassInstanceToMap })).toEqual({ N: num.toString() }); }); }); }); diff --git a/packages/util-dynamodb/src/convertToAttr.ts b/packages/util-dynamodb/src/convertToAttr.ts index 81417245bfdd..e7e26c8bc45f 100644 --- a/packages/util-dynamodb/src/convertToAttr.ts +++ b/packages/util-dynamodb/src/convertToAttr.ts @@ -1,8 +1,8 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; -import { DynamoDBNumber } from "./DynamoDBNumber"; import { marshallOptions } from "./marshall"; import { NativeAttributeBinary, NativeAttributeValue, NativeScalarAttributeValue } from "./models"; +import { NumberValue } from "./NumberValue"; /** * Convert a JavaScript value to its equivalent DynamoDB AttributeValue type. @@ -38,7 +38,7 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti return { BOOL: data.valueOf() }; } else if (typeof data === "number" || data?.constructor?.name === "Number") { return convertToNumberAttr(data); - } else if (data instanceof DynamoDBNumber) { + } else if (data instanceof NumberValue) { return data.toAttributeValue(); } else if (typeof data === "bigint") { return convertToBigIntAttr(data); @@ -80,7 +80,7 @@ const convertToSetAttr = ( const item = setToOperate.values().next().value; - if (item instanceof DynamoDBNumber) { + if (item instanceof NumberValue) { return { NS: Array.from(setToOperate).map((_) => _.toString()), }; diff --git a/packages/util-dynamodb/src/convertToNative.spec.ts b/packages/util-dynamodb/src/convertToNative.spec.ts index a8b92163a285..5282850a214c 100644 --- a/packages/util-dynamodb/src/convertToNative.spec.ts +++ b/packages/util-dynamodb/src/convertToNative.spec.ts @@ -1,7 +1,6 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToNative } from "./convertToNative"; -import { DynamoDBNumber } from "./DynamoDBNumber"; import { NativeAttributeValue } from "./models"; describe("convertToNative", () => { @@ -64,9 +63,7 @@ describe("convertToNative", () => { (BigInt as any) = undefined; expect(() => { convertToNative({ N: numString }); - }).toThrowError( - `${numString} is outside SAFE_INTEGER bounds. Set options.useDynamoDBNumberWrapper to get string value.` - ); + }).toThrowError(`${numString} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); BigInt = BigIntConstructor; }); @@ -91,32 +88,7 @@ describe("convertToNative", () => { it(`throws if number cannot be converted into BigInt: ${numString}`, () => { expect(() => { convertToNative({ N: numString }); - }).toThrowError( - `${numString} can't be converted to BigInt. Set options.useDynamoDBNumberWrapper to get string value.` - ); - }); - }); - - describe("options.useDynamoDBNumberWrapper enum", () => { - it("options.useDynamoDBNumberWrapper=never", () => { - expect(convertToNative({ N: "1" }, { useDynamoDBNumberWrapper: "never" })).toEqual(1); - expect(convertToNative({ N: "9007199254740992" }, { useDynamoDBNumberWrapper: "never" })).toEqual( - BigInt("9007199254740992") - ); - }); - it("options.useDynamoDBNumberWrapper=bigNumbersOnly", () => { - expect(convertToNative({ N: "1" }, { useDynamoDBNumberWrapper: "bigNumbersOnly" })).toEqual(1); - expect(convertToNative({ N: "9007199254740992" }, { useDynamoDBNumberWrapper: "bigNumbersOnly" })).toEqual( - DynamoDBNumber.from("9007199254740992") - ); - }); - it("options.useDynamoDBNumberWrapper=allNumbers", () => { - expect(convertToNative({ N: "1" }, { useDynamoDBNumberWrapper: "allNumbers" })).toEqual( - DynamoDBNumber.from("1") - ); - expect(convertToNative({ N: "9007199254740992" }, { useDynamoDBNumberWrapper: "allNumbers" })).toEqual( - DynamoDBNumber.from("9007199254740992") - ); + }).toThrowError(`${numString} can't be converted to BigInt. Set options.wrapNumbers to get string value.`); }); }); }); diff --git a/packages/util-dynamodb/src/convertToNative.ts b/packages/util-dynamodb/src/convertToNative.ts index 85c00d5086ff..3b140d5c8dc5 100644 --- a/packages/util-dynamodb/src/convertToNative.ts +++ b/packages/util-dynamodb/src/convertToNative.ts @@ -1,7 +1,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; -import { DynamoDBNumber } from "./DynamoDBNumber"; -import { NativeAttributeValue, NumberValue } from "./models"; +import type { NativeAttributeValue, NumberValue as INumberValue } from "./models"; +import { NumberValue } from "./NumberValue"; import { unmarshallOptions } from "./unmarshall"; /** @@ -42,16 +42,9 @@ export const convertToNative = (data: AttributeValue, options?: unmarshallOption throw new Error(`No value defined: ${JSON.stringify(data)}`); }; -const convertNumber = ( - numString: string, - options?: unmarshallOptions -): number | bigint | NumberValue | DynamoDBNumber => { - if (options?.wrapNumbers && !options?.useDynamoDBNumberWrapper) { - return { value: numString }; - } - - if (options?.useDynamoDBNumberWrapper === "allNumbers") { - return DynamoDBNumber.from(numString); +const convertNumber = (numString: string, options?: unmarshallOptions): number | bigint | NumberValue => { + if (options?.wrapNumbers) { + return NumberValue.from(numString); } const num = Number(numString); @@ -59,23 +52,15 @@ const convertNumber = ( const isLargeFiniteNumber = (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) && !infinityValues.includes(num); - if (options?.useDynamoDBNumberWrapper === "bigNumbersOnly" && isLargeFiniteNumber) { - return DynamoDBNumber.from(numString); - } - if (isLargeFiniteNumber) { if (typeof BigInt === "function") { try { return BigInt(numString); } catch (error) { - throw new Error( - `${numString} can't be converted to BigInt. Set options.useDynamoDBNumberWrapper to get string value.` - ); + throw new Error(`${numString} can't be converted to BigInt. Set options.wrapNumbers to get string value.`); } } else { - throw new Error( - `${numString} is outside SAFE_INTEGER bounds. Set options.useDynamoDBNumberWrapper to get string value.` - ); + throw new Error(`${numString} is outside SAFE_INTEGER bounds. Set options.wrapNumbers to get string value.`); } } return num; diff --git a/packages/util-dynamodb/src/index.ts b/packages/util-dynamodb/src/index.ts index 5bdf53908290..60877561d334 100644 --- a/packages/util-dynamodb/src/index.ts +++ b/packages/util-dynamodb/src/index.ts @@ -1,4 +1,4 @@ -export * from "./DynamoDBNumber"; +export { NumberValue as NumberValueImpl } from "./NumberValue"; export * from "./convertToAttr"; export * from "./convertToNative"; export * from "./marshall"; diff --git a/packages/util-dynamodb/src/unmarshall.ts b/packages/util-dynamodb/src/unmarshall.ts index 47de1bcbf100..d6256fdc81ff 100644 --- a/packages/util-dynamodb/src/unmarshall.ts +++ b/packages/util-dynamodb/src/unmarshall.ts @@ -10,9 +10,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; @@ -23,19 +20,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. - * For example, if dealing with small decimals needing higher precision. - */ - useDynamoDBNumberWrapper?: "never" | "bigNumbersOnly" | "allNumbers"; } /**