Skip to content

Commit

Permalink
fix(util-dynamodb): fix signature overload resolution for marshall() …
Browse files Browse the repository at this point in the history
…fn (#6195)
  • Loading branch information
kuhe authored Jun 13, 2024
1 parent 22a403f commit 3682a43
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 35 deletions.
183 changes: 158 additions & 25 deletions packages/util-dynamodb/src/marshall.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
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) => {
describe(`options.${option}`, () => {
[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" },
});
});
});
});
Expand All @@ -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", () => {
Expand All @@ -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<string, AttributeValue> = marshall({
pk: "abc",
sk: "xyz",
});
expect(object1).toEqual({
pk: { S: "abc" },
sk: { S: "xyz" },
} as Record<string, AttributeValue.SMember>);
const object2: Record<string, AttributeValue> = marshall(
{
pk: "abc",
sk: "xyz",
},
{}
);
expect(object2).toEqual({
pk: { S: "abc" },
sk: { S: "xyz" },
} as Record<string, AttributeValue.SMember>);
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<string, AttributeValue> | 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<string, AttributeValue> = marshall(new Map([["a", "a"]]));
expect(map).toEqual({
a: { S: "a" },
} as Record<string, AttributeValue.SMember>);
const unrecognizedClassInstance: Record<string, AttributeValue> = marshall(new Date(), {
convertClassInstanceToMap: true,
});
expect(unrecognizedClassInstance).toEqual({} as Record<string, AttributeValue>);
const unrecognizedClassInstance2: Record<string, AttributeValue> = marshall(
new (class {
public a = "a";
public b = "b";
})(),
{
convertClassInstanceToMap: true,
}
);
expect(unrecognizedClassInstance2).toEqual({
a: { S: "a" },
b: { S: "b" },
} as Record<string, AttributeValue>);

// 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<string, AttributeValue> = marshall({} as {});
expect(empty).toEqual({} as Record<string, AttributeValue>);

const empty2: AttributeValue.MMember = marshall({} as {}, { convertTopLevelContainer: true });
expect(empty2).toEqual({ M: {} } as AttributeValue.MMember);
});

it("with class instance as an input", () => {
Expand All @@ -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" },
});
});
});
51 changes: 42 additions & 9 deletions packages/util-dynamodb/src/marshall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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<bigint> | Set<number> | Set<NumberValue>,
options?: marshallOptions
): AttributeValue.NSMember;
export function marshall(data: Set<string>, options?: marshallOptions): AttributeValue.SSMember;
export function marshall(data: Set<number>, options?: marshallOptions): AttributeValue.NSMember;
export function marshall(data: Set<NativeAttributeBinary>, options?: marshallOptions): AttributeValue.BSMember;
export function marshall<M extends { [K in keyof M]: NativeAttributeValue }>(
data: M,
options?: marshallOptions
): Record<string, AttributeValue>;
export function marshall<L extends NativeAttributeValue[]>(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<O extends { convertTopLevelContainer: true }>(
data: NativeAttributeValue[],
options: marshallOptions & O
): AttributeValue.LMember;
export function marshall<O extends { convertTopLevelContainer: false }>(
data: NativeAttributeValue[],
options: marshallOptions & O
): AttributeValue[];
export function marshall<O extends { convertTopLevelContainer: boolean }>(
data: NativeAttributeValue[],
options: marshallOptions & O
): AttributeValue[] | AttributeValue.LMember;
export function marshall(data: NativeAttributeValue[], options?: marshallOptions): AttributeValue[];
export function marshall<O extends { convertTopLevelContainer: true }>(
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
options: marshallOptions & O
): AttributeValue.MMember;
export function marshall<O extends { convertTopLevelContainer: false }>(
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
options: marshallOptions & O
): Record<string, AttributeValue>;
export function marshall<O extends { convertTopLevelContainer: boolean }>(
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
options: marshallOptions & O
): Record<string, AttributeValue> | AttributeValue.MMember;
export function marshall(
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
options?: marshallOptions
): Record<string, AttributeValue>;
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);
Expand Down
17 changes: 16 additions & 1 deletion packages/util-dynamodb/src/models.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -10,13 +12,19 @@ export interface NumberValue {
readonly value: string;
}

/**
* @public
*/
export type NativeAttributeValue =
| NativeScalarAttributeValue
| { [key: string]: NativeAttributeValue }
| NativeAttributeValue[]
| Set<number | bigint | NumberValue | string | NativeAttributeBinary | undefined>
| InstanceType<{ new (...args: any[]): any }>; // accepts any class instance with options.convertClassInstanceToMap

/**
* @public
*/
export type NativeScalarAttributeValue =
| null
| undefined
Expand All @@ -36,9 +44,16 @@ declare global {
interface File {}
}

type Unavailable = never;
type BlobDefined = Exact<Blob, {}> extends true ? false : true;
type BlobOptionalType = BlobDefined extends true ? Blob : Unavailable;

/**
* @public
*/
export type NativeAttributeBinary =
| ArrayBuffer
| Blob
| BlobOptionalType
| Buffer
| DataView
| File
Expand Down

0 comments on commit 3682a43

Please sign in to comment.