-
Notifications
You must be signed in to change notification settings - Fork 584
/
convertToAttr.ts
171 lines (158 loc) · 6.39 KB
/
convertToAttr.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import { AttributeValue } from "@aws-sdk/client-dynamodb";
import { marshallOptions } from "./marshall";
import { NativeAttributeBinary, NativeAttributeValue, NativeScalarAttributeValue } from "./models";
/**
* Convert a JavaScript value to its equivalent DynamoDB AttributeValue type
*
* @param {NativeAttributeValue} data - The data to convert to a DynamoDB AttributeValue
* @param {marshallOptions} options - An optional configuration object for `convertToAttr`
*/
export const convertToAttr = (data: NativeAttributeValue, options?: marshallOptions): AttributeValue => {
if (data === undefined) {
throw new Error(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
} else if (data === null && typeof data === "object") {
return convertToNullAttr();
} else if (Array.isArray(data)) {
return convertToListAttr(data, options);
} else if (data?.constructor?.name === "Set") {
return convertToSetAttr(data as Set<any>, options);
} else if (data?.constructor?.name === "Object") {
return convertToMapAttr(data as { [key: string]: NativeAttributeValue }, options);
} else if (isBinary(data)) {
if (data.length === 0 && options?.convertEmptyValues) {
return convertToNullAttr();
}
// Do not alter binary data passed https://github.com/aws/aws-sdk-js-v3/issues/1530
// @ts-expect-error Type '{ B: NativeAttributeBinary; }' is not assignable to type 'AttributeValue'
return convertToBinaryAttr(data);
} else if (typeof data === "boolean" || data?.constructor?.name === "Boolean") {
return { BOOL: data.valueOf() };
} else if (typeof data === "number" || data?.constructor?.name === "Number") {
return convertToNumberAttr(data);
} else if (typeof data === "bigint") {
return convertToBigIntAttr(data);
} else if (typeof data === "string" || data?.constructor?.name === "String") {
if (data.length === 0 && options?.convertEmptyValues) {
return convertToNullAttr();
}
return convertToStringAttr(data);
} else if (options?.convertClassInstanceToMap && typeof data === "object") {
return convertToMapAttr(data as { [key: string]: NativeAttributeValue }, options);
}
throw new Error(
`Unsupported type passed: ${data}. Pass options.convertClassInstanceToMap=true to marshall typeof object as map attribute.`
);
};
const convertToListAttr = (data: NativeAttributeValue[], options?: marshallOptions): { L: AttributeValue[] } => ({
L: data
.filter((item) => !options?.removeUndefinedValues || (options?.removeUndefinedValues && item !== undefined))
.map((item) => convertToAttr(item, options)),
});
const convertToSetAttr = (
set: Set<any>,
options?: marshallOptions
): { NS: string[] } | { BS: Uint8Array[] } | { SS: string[] } | { NULL: true } => {
const setToOperate = options?.removeUndefinedValues ? new Set([...set].filter((value) => value !== undefined)) : set;
if (!options?.removeUndefinedValues && setToOperate.has(undefined)) {
throw new Error(`Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.`);
}
if (setToOperate.size === 0) {
if (options?.convertEmptyValues) {
return convertToNullAttr();
}
throw new Error(`Pass a non-empty set, or options.convertEmptyValues=true.`);
}
const item = setToOperate.values().next().value;
if (typeof item === "number") {
return {
NS: Array.from(setToOperate)
.map(convertToNumberAttr)
.map((item) => item.N),
};
} else if (typeof item === "bigint") {
return {
NS: Array.from(setToOperate)
.map(convertToBigIntAttr)
.map((item) => item.N),
};
} else if (typeof item === "string") {
return {
SS: Array.from(setToOperate)
.map(convertToStringAttr)
.map((item) => item.S),
};
} else if (isBinary(item)) {
return {
// Do not alter binary data passed https://github.com/aws/aws-sdk-js-v3/issues/1530
// @ts-expect-error Type 'ArrayBuffer' is not assignable to type 'Uint8Array'
BS: Array.from(setToOperate)
.map(convertToBinaryAttr)
.map((item) => item.B),
};
} else {
throw new Error(`Only Number Set (NS), Binary Set (BS) or String Set (SS) are allowed.`);
}
};
const convertToMapAttr = (
data: { [key: string]: NativeAttributeValue },
options?: marshallOptions
): { M: { [key: string]: AttributeValue } } => ({
M: Object.entries(data)
.filter(
([key, value]: [string, NativeAttributeValue]) =>
!options?.removeUndefinedValues || (options?.removeUndefinedValues && value !== undefined)
)
.reduce(
(acc: { [key: string]: AttributeValue }, [key, value]: [string, NativeAttributeValue]) => ({
...acc,
[key]: convertToAttr(value, options),
}),
{}
),
});
// For future-proofing: this functions are called from multiple places
const convertToNullAttr = (): { NULL: true } => ({ NULL: true });
const convertToBinaryAttr = (data: NativeAttributeBinary): { B: NativeAttributeBinary } => ({ B: data });
const convertToStringAttr = (data: string | String): { S: string } => ({ S: data.toString() });
const convertToBigIntAttr = (data: bigint): { N: string } => ({ N: data.toString() });
const validateBigIntAndThrow = (errorPrefix: string) => {
throw new Error(`${errorPrefix} ${typeof BigInt === "function" ? "Use BigInt." : "Pass string value instead."} `);
};
const convertToNumberAttr = (num: number | Number): { N: string } => {
if (
[Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]
.map((val) => val.toString())
.includes(num.toString())
) {
throw new Error(`Special numeric value ${num.toString()} is not allowed`);
} else if (num > Number.MAX_SAFE_INTEGER) {
validateBigIntAndThrow(`Number ${num.toString()} is greater than Number.MAX_SAFE_INTEGER.`);
} else if (num < Number.MIN_SAFE_INTEGER) {
validateBigIntAndThrow(`Number ${num.toString()} is lesser than Number.MIN_SAFE_INTEGER.`);
}
return { N: num.toString() };
};
const isBinary = (data: any): boolean => {
const binaryTypes = [
"ArrayBuffer",
"Blob",
"Buffer",
"DataView",
"File",
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
];
if (data?.constructor) {
return binaryTypes.includes(data.constructor.name);
}
return false;
};