Skip to content

Commit 314d3b3

Browse files
authored
feat(util-dynamodb): enable undefined values removal in marshall (#1840)
1 parent dbfee5b commit 314d3b3

File tree

5 files changed

+145
-36
lines changed

5 files changed

+145
-36
lines changed

Diff for: packages/util-dynamodb/src/convertToAttr.spec.ts

+99-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AttributeValue } from "@aws-sdk/client-dynamodb";
22

33
import { convertToAttr } from "./convertToAttr";
4+
import { marshallOptions } from "./marshall";
45
import { NativeAttributeValue } from "./models";
56

67
describe("convertToAttr", () => {
@@ -179,6 +180,31 @@ describe("convertToAttr", () => {
179180
L: [{ NULL: true }, { NULL: true }, { NULL: true }],
180181
});
181182
});
183+
184+
describe(`testing list with options.removeUndefinedValues`, () => {
185+
describe("throws error", () => {
186+
const testErrorListWithUndefinedValues = (options?: marshallOptions) => {
187+
expect(() => {
188+
convertToAttr(["defined", undefined], options);
189+
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
190+
};
191+
192+
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
193+
it(`when options=${options}`, () => {
194+
testErrorListWithUndefinedValues(options);
195+
});
196+
});
197+
});
198+
199+
it(`returns when options.removeUndefinedValues=true`, () => {
200+
expect(convertToAttr(["defined", undefined], { removeUndefinedValues: true })).toEqual({
201+
L: [{ S: "defined" }],
202+
});
203+
expect(convertToAttr([undefined, "defined", undefined], { removeUndefinedValues: true })).toEqual({
204+
L: [{ S: "defined" }],
205+
});
206+
});
207+
});
182208
});
183209

184210
describe("set", () => {
@@ -204,21 +230,53 @@ describe("convertToAttr", () => {
204230
expect(convertToAttr(set)).toEqual({ SS: Array.from(set) });
205231
});
206232

207-
it("returns null for empty set for options.convertEmptyValues=true", () => {
208-
expect(convertToAttr(new Set([]), { convertEmptyValues: true })).toEqual({ NULL: true });
233+
describe("set with undefined", () => {
234+
describe("throws error", () => {
235+
const testErrorSetWithUndefined = (options?: marshallOptions) => {
236+
expect(() => {
237+
convertToAttr(new Set([1, undefined, 3]), options);
238+
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
239+
};
240+
241+
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
242+
it(`when options=${options}`, () => {
243+
testErrorSetWithUndefined(options);
244+
});
245+
});
246+
});
247+
248+
it("returns when options.removeUndefinedValues=true", () => {
249+
expect(convertToAttr(new Set([1, undefined, 3]), { removeUndefinedValues: true })).toEqual({ NS: ["1", "3"] });
250+
});
209251
});
210252

211-
it("throws error for empty set", () => {
212-
expect(() => {
213-
convertToAttr(new Set([]));
214-
}).toThrowError(`Please pass a non-empty set, or set convertEmptyValues to true.`);
253+
describe("empty set", () => {
254+
describe("throws error", () => {
255+
const testErrorEmptySet = (options?: marshallOptions) => {
256+
expect(() => {
257+
convertToAttr(new Set([]), options);
258+
}).toThrowError(`Pass a non-empty set, or options.convertEmptyValues=true.`);
259+
};
260+
261+
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
262+
it(`when options=${options}`, () => {
263+
testErrorEmptySet(options);
264+
});
265+
});
266+
});
267+
268+
it("returns null when options.convertEmptyValues=true", () => {
269+
expect(convertToAttr(new Set([]), { convertEmptyValues: true })).toEqual({ NULL: true });
270+
});
215271
});
216272

217-
it("thows error for unallowed set", () => {
218-
expect(() => {
219-
// @ts-expect-error Type 'Set<boolean>' is not assignable
220-
convertToAttr(new Set([true, false]));
221-
}).toThrowError(`Only Number Set (NS), Binary Set (BS) or String Set (SS) are allowed.`);
273+
describe("unallowed set", () => {
274+
it("throws error", () => {
275+
expect(() => {
276+
// @ts-expect-error Type 'Set<boolean>' is not assignable
277+
convertToAttr(new Set([true, false]));
278+
}).toThrowError(`Only Number Set (NS), Binary Set (BS) or String Set (SS) are allowed.`);
279+
});
222280
});
223281
});
224282

@@ -278,6 +336,29 @@ describe("convertToAttr", () => {
278336
M: { stringKey: { NULL: true }, binaryKey: { NULL: true }, setKey: { NULL: true } },
279337
});
280338
});
339+
340+
describe(`testing map with options.removeUndefinedValues`, () => {
341+
describe("throws error", () => {
342+
const testErrorMapWithUndefinedValues = (options?: marshallOptions) => {
343+
expect(() => {
344+
convertToAttr({ definedKey: "definedKey", undefinedKey: undefined }, options);
345+
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
346+
};
347+
348+
[undefined, {}, { convertEmptyValues: false }].forEach((options) => {
349+
it(`when options=${options}`, () => {
350+
testErrorMapWithUndefinedValues(options);
351+
});
352+
});
353+
});
354+
355+
it(`returns when options.removeUndefinedValues=true`, () => {
356+
const input = { definedKey: "definedKey", undefinedKey: undefined };
357+
expect(convertToAttr(input, { removeUndefinedValues: true })).toEqual({
358+
M: { definedKey: { S: "definedKey" } },
359+
});
360+
});
361+
});
281362
});
282363

283364
describe("string", () => {
@@ -297,8 +378,14 @@ describe("convertToAttr", () => {
297378
constructor(private readonly foo: string) {}
298379
}
299380

381+
it(`throws for: undefined`, () => {
382+
expect(() => {
383+
convertToAttr(undefined);
384+
}).toThrowError(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
385+
});
386+
300387
// ToDo: Serialize ES6 class objects as string https://github.com/aws/aws-sdk-js-v3/issues/1535
301-
[undefined, new Date(), new FooObj("foo")].forEach((data) => {
388+
[new Date(), new FooObj("foo")].forEach((data) => {
302389
it(`throws for: ${String(data)}`, () => {
303390
expect(() => {
304391
// @ts-expect-error Argument is not assignable to parameter of type 'NativeAttributeValue'

Diff for: packages/util-dynamodb/src/convertToAttr.ts

+31-16
Original file line numberDiff line numberDiff line change
@@ -22,44 +22,52 @@ export const convertToAttr = (data: NativeAttributeValue, options?: marshallOpti
2222
};
2323

2424
const convertToListAttr = (data: NativeAttributeValue[], options?: marshallOptions): { L: AttributeValue[] } => ({
25-
L: data.map((item) => convertToAttr(item, options)),
25+
L: data
26+
.filter((item) => !options?.removeUndefinedValues || (options?.removeUndefinedValues && item !== undefined))
27+
.map((item) => convertToAttr(item, options)),
2628
});
2729

2830
const convertToSetAttr = (
2931
set: Set<any>,
3032
options?: marshallOptions
3133
): { NS: string[] } | { BS: Uint8Array[] } | { SS: string[] } | { NULL: true } => {
32-
if (set.size === 0) {
34+
const setToOperate = options?.removeUndefinedValues ? new Set([...set].filter((value) => value !== undefined)) : set;
35+
36+
if (!options?.removeUndefinedValues && setToOperate.has(undefined)) {
37+
throw new Error(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
38+
}
39+
40+
if (setToOperate.size === 0) {
3341
if (options?.convertEmptyValues) {
3442
return convertToNullAttr();
3543
}
36-
throw new Error(`Please pass a non-empty set, or set convertEmptyValues to true.`);
44+
throw new Error(`Pass a non-empty set, or options.convertEmptyValues=true.`);
3745
}
3846

39-
const item = set.values().next().value;
47+
const item = setToOperate.values().next().value;
4048
if (typeof item === "number") {
4149
return {
42-
NS: Array.from(set)
50+
NS: Array.from(setToOperate)
4351
.map(convertToNumberAttr)
4452
.map((item) => item.N),
4553
};
4654
} else if (typeof item === "bigint") {
4755
return {
48-
NS: Array.from(set)
56+
NS: Array.from(setToOperate)
4957
.map(convertToBigIntAttr)
5058
.map((item) => item.N),
5159
};
5260
} else if (typeof item === "string") {
5361
return {
54-
SS: Array.from(set)
62+
SS: Array.from(setToOperate)
5563
.map(convertToStringAttr)
5664
.map((item) => item.S),
5765
};
5866
} else if (isBinary(item)) {
5967
return {
6068
// Do not alter binary data passed https://github.com/aws/aws-sdk-js-v3/issues/1530
6169
// @ts-expect-error Type 'ArrayBuffer' is not assignable to type 'Uint8Array'
62-
BS: Array.from(set)
70+
BS: Array.from(setToOperate)
6371
.map(convertToBinaryAttr)
6472
.map((item) => item.B),
6573
};
@@ -72,17 +80,24 @@ const convertToMapAttr = (
7280
data: { [key: string]: NativeAttributeValue },
7381
options?: marshallOptions
7482
): { M: { [key: string]: AttributeValue } } => ({
75-
M: Object.entries(data).reduce(
76-
(acc: { [key: string]: AttributeValue }, [key, value]: [string, NativeAttributeValue]) => ({
77-
...acc,
78-
[key]: convertToAttr(value, options),
79-
}),
80-
{}
81-
),
83+
M: Object.entries(data)
84+
.filter(
85+
([key, value]: [string, NativeAttributeValue]) =>
86+
!options?.removeUndefinedValues || (options?.removeUndefinedValues && value !== undefined)
87+
)
88+
.reduce(
89+
(acc: { [key: string]: AttributeValue }, [key, value]: [string, NativeAttributeValue]) => ({
90+
...acc,
91+
[key]: convertToAttr(value, options),
92+
}),
93+
{}
94+
),
8295
});
8396

8497
const convertToScalarAttr = (data: NativeScalarAttributeValue, options?: marshallOptions): AttributeValue => {
85-
if (data === null && typeof data === "object") {
98+
if (data === undefined) {
99+
throw new Error(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
100+
} else if (data === null && typeof data === "object") {
86101
return convertToNullAttr();
87102
} else if (typeof data === "boolean") {
88103
return { BOOL: data };

Diff for: packages/util-dynamodb/src/marshall.spec.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ describe("marshall", () => {
1515
});
1616

1717
it("calls convertToAttr", () => {
18-
// @ts-ignore output mocked for testing
1918
expect(marshall(input)).toEqual(input);
2019
expect(convertToAttr).toHaveBeenCalledTimes(1);
2120
expect(convertToAttr).toHaveBeenCalledWith(input, undefined);
2221
});
2322

24-
[false, true].forEach((convertEmptyValues) => {
25-
it(`passes convertEmptyValues=${convertEmptyValues} to convertToAttr`, () => {
26-
// @ts-ignore output mocked for testing
27-
expect(marshall(input, { convertEmptyValues })).toEqual(input);
28-
expect(convertToAttr).toHaveBeenCalledTimes(1);
29-
expect(convertToAttr).toHaveBeenCalledWith(input, { convertEmptyValues });
23+
["convertEmptyValues", "removeUndefinedValues"].forEach((option) => {
24+
describe(`options.${option}`, () => {
25+
[false, true].forEach((value) => {
26+
it(`passes ${value} to convertToAttr`, () => {
27+
expect(marshall(input, { [option]: value })).toEqual(input);
28+
expect(convertToAttr).toHaveBeenCalledTimes(1);
29+
expect(convertToAttr).toHaveBeenCalledWith(input, { [option]: value });
30+
});
31+
});
3032
});
3133
});
3234
});

Diff for: packages/util-dynamodb/src/marshall.ts

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export interface marshallOptions {
1111
* Whether to automatically convert empty strings, blobs, and sets to `null`
1212
*/
1313
convertEmptyValues?: boolean;
14+
/**
15+
* Whether to remove undefined values while marshalling.
16+
*/
17+
removeUndefinedValues?: boolean;
1418
}
1519

1620
/**

Diff for: packages/util-dynamodb/src/models.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ export type NativeAttributeValue =
1414
| NativeScalarAttributeValue
1515
| { [key: string]: NativeAttributeValue }
1616
| NativeAttributeValue[]
17-
| Set<number | bigint | NumberValue | string | NativeAttributeBinary>;
17+
| Set<number | bigint | NumberValue | string | NativeAttributeBinary | undefined>;
1818

1919
export type NativeScalarAttributeValue =
2020
| null
21+
| undefined
2122
| boolean
2223
| number
2324
| NumberValue

0 commit comments

Comments
 (0)