Skip to content

Commit 3682a43

Browse files
authored
fix(util-dynamodb): fix signature overload resolution for marshall() fn (#6195)
1 parent 22a403f commit 3682a43

File tree

3 files changed

+216
-35
lines changed

3 files changed

+216
-35
lines changed
Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
1-
import { convertToAttr } from "./convertToAttr";
2-
import { marshall } from "./marshall";
1+
import { AttributeValue } from "@aws-sdk/client-dynamodb";
32

4-
jest.mock("./convertToAttr");
3+
import { marshall } from "./marshall";
4+
import { NumberValue } from "./NumberValue";
55

66
describe("marshall", () => {
7-
const mockOutput = { S: "mockOutput" };
8-
(convertToAttr as jest.Mock).mockReturnValue({ M: mockOutput });
9-
10-
afterEach(() => {
11-
jest.clearAllMocks();
12-
});
13-
147
it("with object as an input", () => {
158
const input = { a: "A", b: "B" };
16-
expect(marshall(input)).toEqual(mockOutput);
17-
expect(convertToAttr).toHaveBeenCalledTimes(1);
18-
expect(convertToAttr).toHaveBeenCalledWith(input, undefined);
9+
expect(marshall(input)).toEqual({
10+
a: { S: "A" },
11+
b: { S: "B" },
12+
});
1913
});
2014

2115
["convertEmptyValues", "removeUndefinedValues"].forEach((option) => {
2216
describe(`options.${option}`, () => {
2317
[false, true].forEach((value) => {
2418
it(`passes ${value} to convertToAttr`, () => {
2519
const input = { a: "A", b: "B" };
26-
expect(marshall(input, { [option]: value })).toEqual(mockOutput);
27-
expect(convertToAttr).toHaveBeenCalledTimes(1);
28-
expect(convertToAttr).toHaveBeenCalledWith(input, { [option]: value });
20+
expect(marshall(input, { [option]: value })).toEqual({
21+
a: { S: "A" },
22+
b: { S: "B" },
23+
});
2924
});
3025
});
3126
});
@@ -35,9 +30,10 @@ describe("marshall", () => {
3530
type TestInputType = { a: string; b: string };
3631
const input: TestInputType = { a: "A", b: "B" };
3732

38-
expect(marshall(input)).toEqual(mockOutput);
39-
expect(convertToAttr).toHaveBeenCalledTimes(1);
40-
expect(convertToAttr).toHaveBeenCalledWith(input, undefined);
33+
expect(marshall(input)).toEqual({
34+
a: { S: "A" },
35+
b: { S: "B" },
36+
});
4137
});
4238

4339
it("with Interface as an input", () => {
@@ -47,9 +43,145 @@ describe("marshall", () => {
4743
}
4844
const input: TestInputInterface = { a: "A", b: "B" };
4945

50-
expect(marshall(input)).toEqual(mockOutput);
51-
expect(convertToAttr).toHaveBeenCalledTimes(1);
52-
expect(convertToAttr).toHaveBeenCalledWith(input, undefined);
46+
expect(marshall(input)).toEqual({
47+
a: { S: "A" },
48+
b: { S: "B" },
49+
});
50+
});
51+
52+
it("should resolve signatures correctly", () => {
53+
const ss: AttributeValue.SSMember = marshall(new Set(["a"]));
54+
expect(ss).toEqual({
55+
SS: ["a"],
56+
} as AttributeValue.SSMember);
57+
const ns: AttributeValue.NSMember = marshall(new Set([0]));
58+
expect(ns).toEqual({
59+
NS: ["0"],
60+
} as AttributeValue.NSMember);
61+
const bs: AttributeValue.BSMember = marshall(new Set([new Uint8Array(4)]));
62+
expect(bs).toEqual({
63+
BS: [new Uint8Array(4)],
64+
} as AttributeValue.BSMember);
65+
const s: AttributeValue.SMember = marshall("a");
66+
expect(s).toEqual({
67+
S: "a",
68+
} as AttributeValue.SMember);
69+
const n1: AttributeValue.NMember = marshall(0);
70+
expect(n1).toEqual({ N: "0" } as AttributeValue.NMember);
71+
const n2: AttributeValue.NMember = marshall(BigInt(0));
72+
expect(n2).toEqual({ N: "0" } as AttributeValue.NMember);
73+
const n3: AttributeValue.NMember = marshall(NumberValue.from(0));
74+
expect(n3).toEqual({ N: "0" } as AttributeValue.NMember);
75+
const binary: AttributeValue.BMember = marshall(new Uint8Array(4));
76+
expect(binary).toEqual({
77+
B: new Uint8Array(4),
78+
} as AttributeValue.BMember);
79+
const nil: AttributeValue.NULLMember = marshall(null);
80+
expect(nil).toEqual({
81+
NULL: true,
82+
} as AttributeValue.NULLMember);
83+
const bool: AttributeValue.BOOLMember = marshall(false as boolean);
84+
expect(bool).toEqual({
85+
BOOL: false,
86+
} as AttributeValue.BOOLMember);
87+
const array: AttributeValue[] = marshall([1, 2, 3]);
88+
expect(array).toEqual([{ N: "1" }, { N: "2" }, { N: "3" }] as AttributeValue.NMember[]);
89+
const arrayLDefault: AttributeValue[] = marshall([1, 2, 3], {});
90+
expect(arrayLDefault).toEqual([{ N: "1" }, { N: "2" }, { N: "3" }] as AttributeValue.NMember[]);
91+
const arrayLFalse: AttributeValue[] = marshall([1, 2, 3], {
92+
convertTopLevelContainer: false,
93+
});
94+
expect(arrayLFalse).toEqual([{ N: "1" }, { N: "2" }, { N: "3" }] as AttributeValue.NMember[]);
95+
const arrayLTrue: AttributeValue.LMember = marshall([1, 2, 3], {
96+
convertTopLevelContainer: true,
97+
});
98+
expect(arrayLTrue).toEqual({
99+
L: [{ N: "1" }, { N: "2" }, { N: "3" }],
100+
} as AttributeValue.LMember);
101+
const arrayLBoolean: AttributeValue.LMember | AttributeValue[] = marshall([1, 2, 3], {
102+
convertTopLevelContainer: true as boolean,
103+
});
104+
expect(arrayLBoolean).toEqual({
105+
L: [{ N: "1" }, { N: "2" }, { N: "3" }],
106+
} as AttributeValue.LMember);
107+
const object1: Record<string, AttributeValue> = marshall({
108+
pk: "abc",
109+
sk: "xyz",
110+
});
111+
expect(object1).toEqual({
112+
pk: { S: "abc" },
113+
sk: { S: "xyz" },
114+
} as Record<string, AttributeValue.SMember>);
115+
const object2: Record<string, AttributeValue> = marshall(
116+
{
117+
pk: "abc",
118+
sk: "xyz",
119+
},
120+
{}
121+
);
122+
expect(object2).toEqual({
123+
pk: { S: "abc" },
124+
sk: { S: "xyz" },
125+
} as Record<string, AttributeValue.SMember>);
126+
const object3: AttributeValue.MMember = marshall(
127+
{
128+
pk: "abc",
129+
sk: "xyz",
130+
},
131+
{ convertTopLevelContainer: true }
132+
);
133+
expect(object3).toEqual({
134+
M: {
135+
pk: { S: "abc" },
136+
sk: { S: "xyz" },
137+
},
138+
} as AttributeValue.MMember);
139+
const object4: Record<string, AttributeValue> | AttributeValue.MMember = marshall(
140+
{
141+
pk: "abc",
142+
sk: "xyz",
143+
},
144+
{ convertTopLevelContainer: true as boolean }
145+
);
146+
expect(object4).toEqual({
147+
M: {
148+
pk: { S: "abc" },
149+
sk: { S: "xyz" },
150+
},
151+
} as AttributeValue.MMember);
152+
const map: Record<string, AttributeValue> = marshall(new Map([["a", "a"]]));
153+
expect(map).toEqual({
154+
a: { S: "a" },
155+
} as Record<string, AttributeValue.SMember>);
156+
const unrecognizedClassInstance: Record<string, AttributeValue> = marshall(new Date(), {
157+
convertClassInstanceToMap: true,
158+
});
159+
expect(unrecognizedClassInstance).toEqual({} as Record<string, AttributeValue>);
160+
const unrecognizedClassInstance2: Record<string, AttributeValue> = marshall(
161+
new (class {
162+
public a = "a";
163+
public b = "b";
164+
})(),
165+
{
166+
convertClassInstanceToMap: true,
167+
}
168+
);
169+
expect(unrecognizedClassInstance2).toEqual({
170+
a: { S: "a" },
171+
b: { S: "b" },
172+
} as Record<string, AttributeValue>);
173+
174+
// this strange cast asserts that untyped fallback results in the `any` type.
175+
const untyped: Symbol = marshall(null as any) as Symbol;
176+
expect(untyped).toEqual({
177+
NULL: true,
178+
});
179+
180+
const empty: Record<string, AttributeValue> = marshall({} as {});
181+
expect(empty).toEqual({} as Record<string, AttributeValue>);
182+
183+
const empty2: AttributeValue.MMember = marshall({} as {}, { convertTopLevelContainer: true });
184+
expect(empty2).toEqual({ M: {} } as AttributeValue.MMember);
53185
});
54186

55187
it("with class instance as an input", () => {
@@ -58,8 +190,9 @@ describe("marshall", () => {
58190
}
59191
const input = new TestInputClass("A", "B");
60192

61-
expect(marshall(input)).toEqual(mockOutput);
62-
expect(convertToAttr).toHaveBeenCalledTimes(1);
63-
expect(convertToAttr).toHaveBeenCalledWith(input, undefined);
193+
expect(marshall(input, { convertClassInstanceToMap: true })).toEqual({
194+
a: { S: "A" },
195+
b: { S: "B" },
196+
});
64197
});
65198
});

packages/util-dynamodb/src/marshall.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AttributeValue } from "@aws-sdk/client-dynamodb";
22

33
import { convertToAttr } from "./convertToAttr";
44
import { NativeAttributeBinary, NativeAttributeValue } from "./models";
5+
import { NumberValue } from "./NumberValue";
56

67
/**
78
* An optional configuration object for `marshall`
@@ -36,19 +37,51 @@ export interface marshallOptions {
3637
* @param options - An optional configuration object for `marshall`
3738
*
3839
*/
40+
export function marshall(data: null, options?: marshallOptions): AttributeValue.NULLMember;
41+
export function marshall(
42+
data: Set<bigint> | Set<number> | Set<NumberValue>,
43+
options?: marshallOptions
44+
): AttributeValue.NSMember;
3945
export function marshall(data: Set<string>, options?: marshallOptions): AttributeValue.SSMember;
40-
export function marshall(data: Set<number>, options?: marshallOptions): AttributeValue.NSMember;
4146
export function marshall(data: Set<NativeAttributeBinary>, options?: marshallOptions): AttributeValue.BSMember;
42-
export function marshall<M extends { [K in keyof M]: NativeAttributeValue }>(
43-
data: M,
44-
options?: marshallOptions
45-
): Record<string, AttributeValue>;
46-
export function marshall<L extends NativeAttributeValue[]>(data: L, options?: marshallOptions): AttributeValue[];
47-
export function marshall(data: string, options?: marshallOptions): AttributeValue.SMember;
48-
export function marshall(data: number, options?: marshallOptions): AttributeValue.NMember;
4947
export function marshall(data: NativeAttributeBinary, options?: marshallOptions): AttributeValue.BMember;
50-
export function marshall(data: null, options?: marshallOptions): AttributeValue.NULLMember;
5148
export function marshall(data: boolean, options?: marshallOptions): AttributeValue.BOOLMember;
49+
export function marshall(data: number | NumberValue | bigint, options?: marshallOptions): AttributeValue.NMember;
50+
export function marshall(data: string, options?: marshallOptions): AttributeValue.SMember;
51+
export function marshall(data: boolean, options?: marshallOptions): AttributeValue.BOOLMember;
52+
export function marshall<O extends { convertTopLevelContainer: true }>(
53+
data: NativeAttributeValue[],
54+
options: marshallOptions & O
55+
): AttributeValue.LMember;
56+
export function marshall<O extends { convertTopLevelContainer: false }>(
57+
data: NativeAttributeValue[],
58+
options: marshallOptions & O
59+
): AttributeValue[];
60+
export function marshall<O extends { convertTopLevelContainer: boolean }>(
61+
data: NativeAttributeValue[],
62+
options: marshallOptions & O
63+
): AttributeValue[] | AttributeValue.LMember;
64+
export function marshall(data: NativeAttributeValue[], options?: marshallOptions): AttributeValue[];
65+
export function marshall<O extends { convertTopLevelContainer: true }>(
66+
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
67+
options: marshallOptions & O
68+
): AttributeValue.MMember;
69+
export function marshall<O extends { convertTopLevelContainer: false }>(
70+
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
71+
options: marshallOptions & O
72+
): Record<string, AttributeValue>;
73+
export function marshall<O extends { convertTopLevelContainer: boolean }>(
74+
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
75+
options: marshallOptions & O
76+
): Record<string, AttributeValue> | AttributeValue.MMember;
77+
export function marshall(
78+
data: Map<string, NativeAttributeValue> | Record<string, NativeAttributeValue>,
79+
options?: marshallOptions
80+
): Record<string, AttributeValue>;
81+
export function marshall(data: any, options?: marshallOptions): any;
82+
/**
83+
* This signature will be unmatchable but is included for information.
84+
*/
5285
export function marshall(data: unknown, options?: marshallOptions): AttributeValue.$UnknownMember;
5386
export function marshall(data: unknown, options?: marshallOptions) {
5487
const attributeValue: AttributeValue = convertToAttr(data, options);

packages/util-dynamodb/src/models.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Exact } from "@smithy/types";
2+
13
/**
24
* A interface recognizable as a numeric value that stores the underlying number
35
* as a string.
@@ -10,13 +12,19 @@ export interface NumberValue {
1012
readonly value: string;
1113
}
1214

15+
/**
16+
* @public
17+
*/
1318
export type NativeAttributeValue =
1419
| NativeScalarAttributeValue
1520
| { [key: string]: NativeAttributeValue }
1621
| NativeAttributeValue[]
1722
| Set<number | bigint | NumberValue | string | NativeAttributeBinary | undefined>
1823
| InstanceType<{ new (...args: any[]): any }>; // accepts any class instance with options.convertClassInstanceToMap
1924

25+
/**
26+
* @public
27+
*/
2028
export type NativeScalarAttributeValue =
2129
| null
2230
| undefined
@@ -36,9 +44,16 @@ declare global {
3644
interface File {}
3745
}
3846

47+
type Unavailable = never;
48+
type BlobDefined = Exact<Blob, {}> extends true ? false : true;
49+
type BlobOptionalType = BlobDefined extends true ? Blob : Unavailable;
50+
51+
/**
52+
* @public
53+
*/
3954
export type NativeAttributeBinary =
4055
| ArrayBuffer
41-
| Blob
56+
| BlobOptionalType
4257
| Buffer
4358
| DataView
4459
| File

0 commit comments

Comments
 (0)