From 3682a4365c47c43396d103fefba7d28126784674 Mon Sep 17 00:00:00 2001 From: George Fu Date: Thu, 13 Jun 2024 16:20:20 -0400 Subject: [PATCH] fix(util-dynamodb): fix signature overload resolution for marshall() fn (#6195) --- packages/util-dynamodb/src/marshall.spec.ts | 183 +++++++++++++++++--- packages/util-dynamodb/src/marshall.ts | 51 +++++- packages/util-dynamodb/src/models.ts | 17 +- 3 files changed, 216 insertions(+), 35 deletions(-) diff --git a/packages/util-dynamodb/src/marshall.spec.ts b/packages/util-dynamodb/src/marshall.spec.ts index ea16c3cb2d03..9a1f784a43b5 100644 --- a/packages/util-dynamodb/src/marshall.spec.ts +++ b/packages/util-dynamodb/src/marshall.spec.ts @@ -1,21 +1,15 @@ -import { convertToAttr } from "./convertToAttr"; -import { marshall } from "./marshall"; +import { AttributeValue } from "@aws-sdk/client-dynamodb"; -jest.mock("./convertToAttr"); +import { marshall } from "./marshall"; +import { NumberValue } from "./NumberValue"; describe("marshall", () => { - const mockOutput = { S: "mockOutput" }; - (convertToAttr as jest.Mock).mockReturnValue({ M: mockOutput }); - - afterEach(() => { - jest.clearAllMocks(); - }); - it("with object as an input", () => { const input = { a: "A", b: "B" }; - expect(marshall(input)).toEqual(mockOutput); - expect(convertToAttr).toHaveBeenCalledTimes(1); - expect(convertToAttr).toHaveBeenCalledWith(input, undefined); + expect(marshall(input)).toEqual({ + a: { S: "A" }, + b: { S: "B" }, + }); }); ["convertEmptyValues", "removeUndefinedValues"].forEach((option) => { @@ -23,9 +17,10 @@ describe("marshall", () => { [false, true].forEach((value) => { it(`passes ${value} to convertToAttr`, () => { const input = { a: "A", b: "B" }; - expect(marshall(input, { [option]: value })).toEqual(mockOutput); - expect(convertToAttr).toHaveBeenCalledTimes(1); - expect(convertToAttr).toHaveBeenCalledWith(input, { [option]: value }); + expect(marshall(input, { [option]: value })).toEqual({ + a: { S: "A" }, + b: { S: "B" }, + }); }); }); }); @@ -35,9 +30,10 @@ describe("marshall", () => { type TestInputType = { a: string; b: string }; const input: TestInputType = { a: "A", b: "B" }; - expect(marshall(input)).toEqual(mockOutput); - expect(convertToAttr).toHaveBeenCalledTimes(1); - expect(convertToAttr).toHaveBeenCalledWith(input, undefined); + expect(marshall(input)).toEqual({ + a: { S: "A" }, + b: { S: "B" }, + }); }); it("with Interface as an input", () => { @@ -47,9 +43,145 @@ describe("marshall", () => { } const input: TestInputInterface = { a: "A", b: "B" }; - expect(marshall(input)).toEqual(mockOutput); - expect(convertToAttr).toHaveBeenCalledTimes(1); - expect(convertToAttr).toHaveBeenCalledWith(input, undefined); + expect(marshall(input)).toEqual({ + a: { S: "A" }, + b: { S: "B" }, + }); + }); + + it("should resolve signatures correctly", () => { + const ss: AttributeValue.SSMember = marshall(new Set(["a"])); + expect(ss).toEqual({ + SS: ["a"], + } as AttributeValue.SSMember); + const ns: AttributeValue.NSMember = marshall(new Set([0])); + expect(ns).toEqual({ + NS: ["0"], + } as AttributeValue.NSMember); + const bs: AttributeValue.BSMember = marshall(new Set([new Uint8Array(4)])); + expect(bs).toEqual({ + BS: [new Uint8Array(4)], + } as AttributeValue.BSMember); + const s: AttributeValue.SMember = marshall("a"); + expect(s).toEqual({ + S: "a", + } as AttributeValue.SMember); + const n1: AttributeValue.NMember = marshall(0); + expect(n1).toEqual({ N: "0" } as AttributeValue.NMember); + const n2: AttributeValue.NMember = marshall(BigInt(0)); + expect(n2).toEqual({ N: "0" } as AttributeValue.NMember); + const n3: AttributeValue.NMember = marshall(NumberValue.from(0)); + expect(n3).toEqual({ N: "0" } as AttributeValue.NMember); + const binary: AttributeValue.BMember = marshall(new Uint8Array(4)); + expect(binary).toEqual({ + B: new Uint8Array(4), + } as AttributeValue.BMember); + const nil: AttributeValue.NULLMember = marshall(null); + expect(nil).toEqual({ + NULL: true, + } as AttributeValue.NULLMember); + const bool: AttributeValue.BOOLMember = marshall(false as boolean); + expect(bool).toEqual({ + BOOL: false, + } as AttributeValue.BOOLMember); + const array: AttributeValue[] = marshall([1, 2, 3]); + expect(array).toEqual([{ N: "1" }, { N: "2" }, { N: "3" }] as AttributeValue.NMember[]); + const arrayLDefault: AttributeValue[] = marshall([1, 2, 3], {}); + expect(arrayLDefault).toEqual([{ N: "1" }, { N: "2" }, { N: "3" }] as AttributeValue.NMember[]); + const arrayLFalse: AttributeValue[] = marshall([1, 2, 3], { + convertTopLevelContainer: false, + }); + expect(arrayLFalse).toEqual([{ N: "1" }, { N: "2" }, { N: "3" }] as AttributeValue.NMember[]); + const arrayLTrue: AttributeValue.LMember = marshall([1, 2, 3], { + convertTopLevelContainer: true, + }); + expect(arrayLTrue).toEqual({ + L: [{ N: "1" }, { N: "2" }, { N: "3" }], + } as AttributeValue.LMember); + const arrayLBoolean: AttributeValue.LMember | AttributeValue[] = marshall([1, 2, 3], { + convertTopLevelContainer: true as boolean, + }); + expect(arrayLBoolean).toEqual({ + L: [{ N: "1" }, { N: "2" }, { N: "3" }], + } as AttributeValue.LMember); + const object1: Record = marshall({ + pk: "abc", + sk: "xyz", + }); + expect(object1).toEqual({ + pk: { S: "abc" }, + sk: { S: "xyz" }, + } as Record); + const object2: Record = marshall( + { + pk: "abc", + sk: "xyz", + }, + {} + ); + expect(object2).toEqual({ + pk: { S: "abc" }, + sk: { S: "xyz" }, + } as Record); + const object3: AttributeValue.MMember = marshall( + { + pk: "abc", + sk: "xyz", + }, + { convertTopLevelContainer: true } + ); + expect(object3).toEqual({ + M: { + pk: { S: "abc" }, + sk: { S: "xyz" }, + }, + } as AttributeValue.MMember); + const object4: Record | AttributeValue.MMember = marshall( + { + pk: "abc", + sk: "xyz", + }, + { convertTopLevelContainer: true as boolean } + ); + expect(object4).toEqual({ + M: { + pk: { S: "abc" }, + sk: { S: "xyz" }, + }, + } as AttributeValue.MMember); + const map: Record = marshall(new Map([["a", "a"]])); + expect(map).toEqual({ + a: { S: "a" }, + } as Record); + const unrecognizedClassInstance: Record = marshall(new Date(), { + convertClassInstanceToMap: true, + }); + expect(unrecognizedClassInstance).toEqual({} as Record); + const unrecognizedClassInstance2: Record = marshall( + new (class { + public a = "a"; + public b = "b"; + })(), + { + convertClassInstanceToMap: true, + } + ); + expect(unrecognizedClassInstance2).toEqual({ + a: { S: "a" }, + b: { S: "b" }, + } as Record); + + // this strange cast asserts that untyped fallback results in the `any` type. + const untyped: Symbol = marshall(null as any) as Symbol; + expect(untyped).toEqual({ + NULL: true, + }); + + const empty: Record = marshall({} as {}); + expect(empty).toEqual({} as Record); + + const empty2: AttributeValue.MMember = marshall({} as {}, { convertTopLevelContainer: true }); + expect(empty2).toEqual({ M: {} } as AttributeValue.MMember); }); it("with class instance as an input", () => { @@ -58,8 +190,9 @@ describe("marshall", () => { } const input = new TestInputClass("A", "B"); - expect(marshall(input)).toEqual(mockOutput); - expect(convertToAttr).toHaveBeenCalledTimes(1); - expect(convertToAttr).toHaveBeenCalledWith(input, undefined); + expect(marshall(input, { convertClassInstanceToMap: true })).toEqual({ + a: { S: "A" }, + b: { S: "B" }, + }); }); }); diff --git a/packages/util-dynamodb/src/marshall.ts b/packages/util-dynamodb/src/marshall.ts index 717838d564d2..2fefc0f679d9 100644 --- a/packages/util-dynamodb/src/marshall.ts +++ b/packages/util-dynamodb/src/marshall.ts @@ -2,6 +2,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb"; import { convertToAttr } from "./convertToAttr"; import { NativeAttributeBinary, NativeAttributeValue } from "./models"; +import { NumberValue } from "./NumberValue"; /** * An optional configuration object for `marshall` @@ -36,19 +37,51 @@ export interface marshallOptions { * @param options - An optional configuration object for `marshall` * */ +export function marshall(data: null, options?: marshallOptions): AttributeValue.NULLMember; +export function marshall( + data: Set | Set | Set, + options?: marshallOptions +): AttributeValue.NSMember; export function marshall(data: Set, options?: marshallOptions): AttributeValue.SSMember; -export function marshall(data: Set, options?: marshallOptions): AttributeValue.NSMember; export function marshall(data: Set, options?: marshallOptions): AttributeValue.BSMember; -export function marshall( - data: M, - options?: marshallOptions -): Record; -export function marshall(data: L, options?: marshallOptions): AttributeValue[]; -export function marshall(data: string, options?: marshallOptions): AttributeValue.SMember; -export function marshall(data: number, options?: marshallOptions): AttributeValue.NMember; export function marshall(data: NativeAttributeBinary, options?: marshallOptions): AttributeValue.BMember; -export function marshall(data: null, options?: marshallOptions): AttributeValue.NULLMember; export function marshall(data: boolean, options?: marshallOptions): AttributeValue.BOOLMember; +export function marshall(data: number | NumberValue | bigint, options?: marshallOptions): AttributeValue.NMember; +export function marshall(data: string, options?: marshallOptions): AttributeValue.SMember; +export function marshall(data: boolean, options?: marshallOptions): AttributeValue.BOOLMember; +export function marshall( + data: NativeAttributeValue[], + options: marshallOptions & O +): AttributeValue.LMember; +export function marshall( + data: NativeAttributeValue[], + options: marshallOptions & O +): AttributeValue[]; +export function marshall( + data: NativeAttributeValue[], + options: marshallOptions & O +): AttributeValue[] | AttributeValue.LMember; +export function marshall(data: NativeAttributeValue[], options?: marshallOptions): AttributeValue[]; +export function marshall( + data: Map | Record, + options: marshallOptions & O +): AttributeValue.MMember; +export function marshall( + data: Map | Record, + options: marshallOptions & O +): Record; +export function marshall( + data: Map | Record, + options: marshallOptions & O +): Record | AttributeValue.MMember; +export function marshall( + data: Map | Record, + options?: marshallOptions +): Record; +export function marshall(data: any, options?: marshallOptions): any; +/** + * This signature will be unmatchable but is included for information. + */ export function marshall(data: unknown, options?: marshallOptions): AttributeValue.$UnknownMember; export function marshall(data: unknown, options?: marshallOptions) { const attributeValue: AttributeValue = convertToAttr(data, options); diff --git a/packages/util-dynamodb/src/models.ts b/packages/util-dynamodb/src/models.ts index 8068add9a43d..b61953da7fc2 100644 --- a/packages/util-dynamodb/src/models.ts +++ b/packages/util-dynamodb/src/models.ts @@ -1,3 +1,5 @@ +import type { Exact } from "@smithy/types"; + /** * A interface recognizable as a numeric value that stores the underlying number * as a string. @@ -10,6 +12,9 @@ export interface NumberValue { readonly value: string; } +/** + * @public + */ export type NativeAttributeValue = | NativeScalarAttributeValue | { [key: string]: NativeAttributeValue } @@ -17,6 +22,9 @@ export type NativeAttributeValue = | Set | InstanceType<{ new (...args: any[]): any }>; // accepts any class instance with options.convertClassInstanceToMap +/** + * @public + */ export type NativeScalarAttributeValue = | null | undefined @@ -36,9 +44,16 @@ declare global { interface File {} } +type Unavailable = never; +type BlobDefined = Exact extends true ? false : true; +type BlobOptionalType = BlobDefined extends true ? Blob : Unavailable; + +/** + * @public + */ export type NativeAttributeBinary = | ArrayBuffer - | Blob + | BlobOptionalType | Buffer | DataView | File