-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathdocument.ts
More file actions
358 lines (320 loc) · 11.3 KB
/
document.ts
File metadata and controls
358 lines (320 loc) · 11.3 KB
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
import {
Binary,
type BSONElement,
BSONError,
BSONType,
deserialize,
type DeserializeOptions,
getBigInt64LE,
getFloat64LE,
getInt32LE,
ObjectId,
parseToElementsToArray,
Timestamp,
toUTF8
} from '../../../bson';
const BSONElementOffset = {
type: 0,
nameOffset: 1,
nameLength: 2,
offset: 3,
length: 4
} as const;
/** @internal */
export type JSTypeOf = {
[BSONType.null]: null;
[BSONType.undefined]: null;
[BSONType.double]: number;
[BSONType.int]: number;
[BSONType.long]: bigint;
[BSONType.timestamp]: Timestamp;
[BSONType.binData]: Binary;
[BSONType.bool]: boolean;
[BSONType.objectId]: ObjectId;
[BSONType.string]: string;
[BSONType.date]: Date;
[BSONType.object]: OnDemandDocument;
[BSONType.array]: OnDemandDocument;
};
/** @internal */
type CachedBSONElement = { element: BSONElement; value: any | undefined };
/**
* @internal
*
* Options for `OnDemandDocument.toObject()`. Validation is required to ensure
* that callers provide utf8 validation options. */
export type OnDemandDocumentDeserializeOptions = Omit<DeserializeOptions, 'validation'> &
Required<Pick<DeserializeOptions, 'validation'>>;
/** @internal */
export class OnDemandDocument {
/**
* Maps JS strings to elements and jsValues for speeding up subsequent lookups.
* - If `false` then name does not exist in the BSON document
* - If `CachedBSONElement` instance name exists
* - If `cache[name].value == null` jsValue has not yet been parsed
* - Null/Undefined values do not get cached because they are zero-length values.
*/
private readonly cache: Record<string, CachedBSONElement | false | undefined> =
Object.create(null);
/** Caches the index of elements that have been named */
private readonly indexFound: Record<number, boolean> = Object.create(null);
/** All bson elements in this document */
private readonly elements: ReadonlyArray<BSONElement>;
/** BSON bytes, this document begins at offset */
protected readonly bson: Uint8Array;
/** The start of the document */
private readonly offset: number;
/** If this is an embedded document, indicates if this was a BSON array */
public readonly isArray: boolean;
constructor(
bson: Uint8Array,
offset = 0,
isArray = false,
/** If elements was already calculated */
elements?: BSONElement[]
) {
this.bson = bson;
this.offset = offset;
this.isArray = isArray;
this.elements = elements ?? parseToElementsToArray(this.bson, offset);
}
/** Only supports basic latin strings */
private isElementName(name: string, element: BSONElement): boolean {
const nameLength = element[BSONElementOffset.nameLength];
const nameOffset = element[BSONElementOffset.nameOffset];
if (name.length !== nameLength) return false;
const nameEnd = nameOffset + nameLength;
for (
let byteIndex = nameOffset, charIndex = 0;
charIndex < name.length && byteIndex < nameEnd;
charIndex++, byteIndex++
) {
if (this.bson[byteIndex] !== name.charCodeAt(charIndex)) return false;
}
return true;
}
/**
* Seeks into the elements array for an element matching the given name.
*
* @remarks
* Caching:
* - Caches the existence of a property making subsequent look ups for non-existent properties return immediately
* - Caches names mapped to elements to avoid reiterating the array and comparing the name again
* - Caches the index at which an element has been found to prevent rechecking against elements already determined to belong to another name
*
* @param name - a basic latin string name of a BSON element
* @returns
*/
private getElement(name: string | number): CachedBSONElement | null {
const cachedElement = this.cache[name];
if (cachedElement === false) return null;
if (cachedElement != null) {
return cachedElement;
}
if (typeof name === 'number') {
if (this.isArray) {
if (name < this.elements.length) {
const element = this.elements[name];
const cachedElement = { element, value: undefined };
this.cache[name] = cachedElement;
this.indexFound[name] = true;
return cachedElement;
} else {
return null;
}
} else {
return null;
}
}
for (let index = 0; index < this.elements.length; index++) {
const element = this.elements[index];
// skip this element if it has already been associated with a name
if (!(index in this.indexFound) && this.isElementName(name, element)) {
const cachedElement = { element, value: undefined };
this.cache[name] = cachedElement;
this.indexFound[index] = true;
return cachedElement;
}
}
this.cache[name] = false;
return null;
}
/**
* Translates BSON bytes into a javascript value. Checking `as` against the BSON element's type
* this methods returns the small subset of BSON types that the driver needs to function.
*
* @remarks
* - BSONType.null and BSONType.undefined always return null
* - If the type requested does not match this returns null
*
* @param element - The element to revive to a javascript value
* @param as - A type byte expected to be returned
*/
private toJSValue<T extends keyof JSTypeOf>(element: BSONElement, as: T): JSTypeOf[T];
private toJSValue(element: BSONElement, as: keyof JSTypeOf): any {
const type = element[BSONElementOffset.type];
const offset = element[BSONElementOffset.offset];
const length = element[BSONElementOffset.length];
if (as !== type) {
return null;
}
switch (as) {
case BSONType.null:
case BSONType.undefined:
return null;
case BSONType.double:
return getFloat64LE(this.bson, offset);
case BSONType.int:
return getInt32LE(this.bson, offset);
case BSONType.long:
return getBigInt64LE(this.bson, offset);
case BSONType.bool:
return Boolean(this.bson[offset]);
case BSONType.objectId:
return new ObjectId(this.bson.subarray(offset, offset + 12));
case BSONType.timestamp:
return new Timestamp(getBigInt64LE(this.bson, offset));
case BSONType.string:
return toUTF8(this.bson, offset + 4, offset + length - 1, false);
case BSONType.binData: {
const totalBinarySize = getInt32LE(this.bson, offset);
const subType = this.bson[offset + 4];
if (subType === 2) {
const subType2BinarySize = getInt32LE(this.bson, offset + 1 + 4);
if (subType2BinarySize < 0)
throw new BSONError('Negative binary type element size found for subtype 0x02');
if (subType2BinarySize > totalBinarySize - 4)
throw new BSONError('Binary type with subtype 0x02 contains too long binary size');
if (subType2BinarySize < totalBinarySize - 4)
throw new BSONError('Binary type with subtype 0x02 contains too short binary size');
return new Binary(
this.bson.subarray(offset + 1 + 4 + 4, offset + 1 + 4 + 4 + subType2BinarySize),
2
);
}
return new Binary(
this.bson.subarray(offset + 1 + 4, offset + 1 + 4 + totalBinarySize),
subType
);
}
case BSONType.date:
// Pretend this is correct.
return new Date(Number(getBigInt64LE(this.bson, offset)));
case BSONType.object:
return new OnDemandDocument(this.bson, offset);
case BSONType.array:
return new OnDemandDocument(this.bson, offset, true);
default:
throw new BSONError(`Unsupported BSON type: ${as}`);
}
}
/**
* Returns the number of elements in this BSON document
*/
public size() {
return this.elements.length;
}
/**
* Checks for the existence of an element by name.
*
* @remarks
* Uses `getElement` with the expectation that will populate caches such that a `has` call
* followed by a `getElement` call will not repeat the cost paid by the first look up.
*
* @param name - element name
*/
public has(name: string): boolean {
const cachedElement = this.cache[name];
if (cachedElement === false) return false;
if (cachedElement != null) return true;
return this.getElement(name) != null;
}
/**
* Turns BSON element with `name` into a javascript value.
*
* @typeParam T - must be one of the supported BSON types determined by `JSTypeOf` this will determine the return type of this function.
* @param name - the element name
* @param as - the bson type expected
* @param required - whether or not the element is expected to exist, if true this function will throw if it is not present
*/
public get<const T extends keyof JSTypeOf>(
name: string | number,
as: T,
required?: boolean
): JSTypeOf[T] | null;
/** `required` will make `get` throw if name does not exist or is null/undefined */
public get<const T extends keyof JSTypeOf>(
name: string | number,
as: T,
required: true
): JSTypeOf[T];
public get<const T extends keyof JSTypeOf>(
name: string | number,
as: T,
required?: boolean
): JSTypeOf[T] | null {
const element = this.getElement(name);
if (element == null) {
if (required === true) {
throw new BSONError(`BSON element "${name}" is missing`);
} else {
return null;
}
}
if (element.value == null) {
const value = this.toJSValue(element.element, as);
if (value == null) {
if (required === true) {
throw new BSONError(`BSON element "${name}" is missing`);
} else {
return null;
}
}
// It is important to never store null
element.value = value;
}
return element.value;
}
/**
* Supports returning int, double, long, and bool as javascript numbers
*
* @remarks
* **NOTE:**
* - Use this _only_ when you believe the potential precision loss of an int64 is acceptable
* - This method does not cache the result as Longs or booleans would be stored incorrectly
*
* @param name - element name
* @param required - throws if name does not exist
*/
public getNumber<const Req extends boolean = false>(
name: string,
required?: Req
): Req extends true ? number : number | null;
public getNumber(name: string, required: boolean): number | null {
const maybeBool = this.get(name, BSONType.bool);
const bool = maybeBool == null ? null : maybeBool ? 1 : 0;
const maybeLong = this.get(name, BSONType.long);
const long = maybeLong == null ? null : Number(maybeLong);
const result = bool ?? long ?? this.get(name, BSONType.int) ?? this.get(name, BSONType.double);
if (required === true && result == null) {
throw new BSONError(`BSON element "${name}" is missing`);
}
return result;
}
/**
* Deserialize this object, DOES NOT cache result so avoid multiple invocations
* @param options - BSON deserialization options
*/
public toObject(options?: OnDemandDocumentDeserializeOptions): Record<string, any> {
return deserialize(this.bson, {
...options,
index: this.offset,
allowObjectSmallerThanBufferSize: true
});
}
/** Returns this document's bytes only */
toBytes() {
const size = getInt32LE(this.bson, this.offset);
return this.bson.subarray(this.offset, this.offset + size);
}
}