1 | const {
|
2 | isPlainObject,
|
3 | isArray,
|
4 | isString,
|
5 | isNumber,
|
6 | hasIn,
|
7 | keys,
|
8 | without,
|
9 | toNumber,
|
10 | toString,
|
11 | } = require('lodash');
|
12 |
|
13 | const {
|
14 | ObjectId,
|
15 | MinKey,
|
16 | MaxKey,
|
17 | Long,
|
18 | Double,
|
19 | Int32,
|
20 | Decimal128,
|
21 | Binary,
|
22 | BSONRegExp,
|
23 | Code,
|
24 | BSONSymbol,
|
25 | Timestamp,
|
26 | BSONMap
|
27 | } = require('bson');
|
28 |
|
29 |
|
30 |
|
31 | const OBJECT = 'Object';
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | const ARRAY = 'Array';
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | const TRUE = 'true';
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | const FALSE = 'false';
|
47 |
|
48 |
|
49 |
|
50 |
|
51 | const LONG = 'Long';
|
52 |
|
53 | const INT_32 = 'Int32';
|
54 | const INT_64 = 'Int64';
|
55 | const DOUBLE = 'Double';
|
56 | const DECIMAL_128 = 'Decimal128';
|
57 | const OBJECT_TYPE = '[object Object]';
|
58 | const EMPTY = '';
|
59 | const OBJECT_ID = 'ObjectID';
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | const BSON_TYPE = '_bsontype';
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | const MATCH = /\[object (\w+)\]/;
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | const BSON_INT32_MAX = 0x7fffffff;
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | const BSON_INT32_MIN = -0x80000000;
|
80 |
|
81 | const BSON_INT64_MAX = Math.pow(2, 63) - 1;
|
82 | const BSON_INT64_MIN = -BSON_INT64_MAX;
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | const NUMBER_REGEX = /^-?\d+$/;
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | const NUMBER_TYPES = ['Long', 'Int32', 'Double', 'Decimal128'];
|
93 |
|
94 | const toDate = (object) => {
|
95 | return new Date(object);
|
96 | };
|
97 |
|
98 | const toMinKey = () => {
|
99 | return new MinKey();
|
100 | };
|
101 |
|
102 | const toMaxKey = () => {
|
103 | return new MaxKey();
|
104 | };
|
105 |
|
106 | const toUndefined = () => {
|
107 | return undefined;
|
108 | };
|
109 |
|
110 | const toNull = () => {
|
111 | return null;
|
112 | };
|
113 |
|
114 | const toBoolean = (object) => {
|
115 | if (isString(object)) {
|
116 | if (object.toLowerCase() === TRUE) {
|
117 | return true;
|
118 | } else if (object.toLowerCase() === FALSE) {
|
119 | return false;
|
120 | }
|
121 | throw new Error(`'${object}' is not a valid boolean string`);
|
122 | }
|
123 | if (object) {
|
124 | return true;
|
125 | }
|
126 | return false;
|
127 | };
|
128 |
|
129 | const toObject = (object) => {
|
130 | if (isPlainObject(object)) {
|
131 | return object;
|
132 | }
|
133 | return {};
|
134 | };
|
135 |
|
136 | const toArray = (object) => {
|
137 | if (isArray(object)) {
|
138 | return object;
|
139 | }
|
140 | if (isPlainObject(object)) {
|
141 | return [];
|
142 | }
|
143 | return [object];
|
144 | };
|
145 |
|
146 | const toInt32 = (object) => {
|
147 | if (object === '-' || object === '') {
|
148 | throw new Error(`Value '${object}' is not a valid Int32 value`);
|
149 | }
|
150 | const number = toNumber(object);
|
151 | if (number >= BSON_INT32_MIN && number <= BSON_INT32_MAX) {
|
152 | return new Int32(number);
|
153 | }
|
154 | throw new Error(`Value ${number} is outside the valid Int32 range`);
|
155 | };
|
156 |
|
157 | const toInt64 = (object) => {
|
158 | if (object === '-' || object === '') {
|
159 | throw new Error(`Value '${object}' is not a valid Int64 value`);
|
160 | }
|
161 |
|
162 | const number = toNumber(object);
|
163 |
|
164 | if (number >= BSON_INT64_MIN && number <= BSON_INT64_MAX) {
|
165 |
|
166 |
|
167 |
|
168 | if (object.value || typeof object === 'number') {
|
169 | return Long.fromNumber(number);
|
170 | } else if (typeof object === 'object') {
|
171 |
|
172 |
|
173 | return Long.fromString(object.toString());
|
174 | }
|
175 |
|
176 | return Long.fromString(object);
|
177 | }
|
178 | throw new Error(`Value ${object.toString()} is outside the valid Int64 range`);
|
179 | };
|
180 |
|
181 | const toDouble = (object) => {
|
182 | if (object === '-' || object === '') {
|
183 | throw new Error(`Value '${object}' is not a valid Double value`);
|
184 | }
|
185 | if (isString(object) && object.endsWith('.')) {
|
186 | throw new Error('Please enter at least one digit after the decimal');
|
187 | }
|
188 | const number = toNumber(object);
|
189 | return new Double(number);
|
190 | };
|
191 |
|
192 | const toDecimal128 = (object) => {
|
193 | |
194 |
|
195 |
|
196 | if (hasIn(object, BSON_TYPE) && NUMBER_TYPES.includes(object._bsontype)) {
|
197 | object = object._bsontype === LONG ? object.toString() : object.valueOf();
|
198 | }
|
199 | return Decimal128.fromString('' + object);
|
200 | };
|
201 |
|
202 | const toObjectID = (object) => {
|
203 | if (!isString(object) || object === '') {
|
204 | return new ObjectId();
|
205 | }
|
206 | return ObjectId.createFromHexString(object);
|
207 | };
|
208 |
|
209 | const toBinary = (object) => {
|
210 | return new Binary('' + object, Binary.SUBTYPE_DEFAULT);
|
211 | };
|
212 |
|
213 | const toRegex = (object) => {
|
214 | return new BSONRegExp('' + object);
|
215 | };
|
216 |
|
217 | const toCode = (object) => {
|
218 | return new Code('' + object, {});
|
219 | };
|
220 |
|
221 | const toSymbol = (object) => {
|
222 | return new BSONSymbol('' + object);
|
223 | };
|
224 |
|
225 | const toTimestamp = (object) => {
|
226 | const number = toNumber(object);
|
227 | return Timestamp.fromNumber(number);
|
228 | };
|
229 |
|
230 | const toMap = (object) => {
|
231 | return new BSONMap(object);
|
232 | };
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | const CASTERS = {
|
238 | Array: toArray,
|
239 | Binary: toBinary,
|
240 | Boolean: toBoolean,
|
241 | Code: toCode,
|
242 | Date: toDate,
|
243 | Decimal128: toDecimal128,
|
244 | Double: toDouble,
|
245 | Int32: toInt32,
|
246 | Int64: toInt64,
|
247 | MaxKey: toMaxKey,
|
248 | MinKey: toMinKey,
|
249 | Null: toNull,
|
250 | Object: toObject,
|
251 | ObjectId: toObjectID,
|
252 | BSONRegexp: toRegex,
|
253 | String: toString,
|
254 | BSONSymbol: toSymbol,
|
255 | BSONMap: toMap,
|
256 | Timestamp: toTimestamp,
|
257 | Undefined: toUndefined
|
258 | };
|
259 |
|
260 |
|
261 |
|
262 |
|
263 | const TYPES = keys(CASTERS);
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | class Int32Check {
|
269 | test(string) {
|
270 | if (NUMBER_REGEX.test(string)) {
|
271 | var value = toNumber(string);
|
272 | return value >= BSON_INT32_MIN && value <= BSON_INT32_MAX;
|
273 | }
|
274 | return false;
|
275 | }
|
276 | }
|
277 |
|
278 |
|
279 |
|
280 |
|
281 | class Int64Check {
|
282 | test(string) {
|
283 | if (NUMBER_REGEX.test(string)) {
|
284 | return Number.isSafeInteger(toNumber(string));
|
285 | }
|
286 | return false;
|
287 | }
|
288 | }
|
289 |
|
290 | const INT32_CHECK = new Int32Check();
|
291 | const INT64_CHECK = new Int64Check();
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 | const numberToBsonType = (number) => {
|
301 | var string = toString(number);
|
302 | if (INT32_CHECK.test(string)) {
|
303 | return INT_32;
|
304 | } else if (INT64_CHECK.test(string)) {
|
305 | return INT_64;
|
306 | }
|
307 | return DOUBLE;
|
308 | };
|
309 |
|
310 |
|
311 |
|
312 |
|
313 | class TypeChecker {
|
314 | |
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | cast(object, type) {
|
323 | var caster = CASTERS[type];
|
324 | var result = object;
|
325 | if (caster) {
|
326 | result = caster(object);
|
327 | }
|
328 | return result === OBJECT_TYPE ? EMPTY : result;
|
329 | }
|
330 |
|
331 | |
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 | type(object) {
|
339 | if (hasIn(object, BSON_TYPE)) {
|
340 | if (object._bsontype === LONG) {
|
341 | return INT_64;
|
342 | }
|
343 | if (object._bsontype === OBJECT_ID) {
|
344 | return 'ObjectId';
|
345 | }
|
346 | return object._bsontype;
|
347 | }
|
348 | if (isNumber(object)) {
|
349 | return numberToBsonType(object);
|
350 | }
|
351 | if (isPlainObject(object)) {
|
352 | return OBJECT;
|
353 | }
|
354 | if (isArray(object)) {
|
355 | return ARRAY;
|
356 | }
|
357 | return Object.prototype.toString.call(object).replace(MATCH, '$1');
|
358 | }
|
359 |
|
360 | |
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 | castableTypes(highPrecisionSupport = false) {
|
368 | if (highPrecisionSupport === true) {
|
369 | return TYPES;
|
370 | }
|
371 | return without(TYPES, DECIMAL_128);
|
372 | }
|
373 | }
|
374 |
|
375 | module.exports = new TypeChecker();
|