Skip to content

Commit 269df91

Browse files
feat(NODE-5958): add BSON iterating API (#656)
Co-authored-by: Aditi Khare <[email protected]>
1 parent 2f0effb commit 269df91

File tree

8 files changed

+631
-25
lines changed

8 files changed

+631
-25
lines changed

Diff for: .eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@typescript-eslint/no-unsafe-return": "off",
6565
"@typescript-eslint/no-unsafe-argument": "off",
6666
"@typescript-eslint/no-unsafe-call": "off",
67+
"@typescript-eslint/no-unsafe-enum-comparison": "off",
6768
"@typescript-eslint/consistent-type-imports": [
6869
"error",
6970
{

Diff for: src/bson.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export { BSONValue } from './bson_value';
5454
export { BSONError, BSONVersionError, BSONRuntimeError } from './error';
5555
export { BSONType } from './constants';
5656
export { EJSON } from './extended_json';
57-
export { onDemand } from './parser/on_demand/index';
57+
export { onDemand, type OnDemand } from './parser/on_demand/index';
5858

5959
/** @public */
6060
export interface Document {

Diff for: src/parser/on_demand/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type BSONError, BSONOffsetError } from '../../error';
22
import { type BSONElement, parseToElements } from './parse_to_elements';
3+
import { type BSONReviver, type Container, parseToStructure } from './parse_to_structure';
34
/**
45
* @experimental
56
* @public
@@ -12,6 +13,21 @@ export type OnDemand = {
1213
isBSONError(value: unknown): value is BSONError;
1314
};
1415
parseToElements: (this: void, bytes: Uint8Array, startOffset?: number) => Iterable<BSONElement>;
16+
parseToStructure: <
17+
TRoot extends Container = {
18+
dest: Record<string, unknown>;
19+
kind: 'object';
20+
}
21+
>(
22+
bytes: Uint8Array,
23+
startOffset?: number,
24+
root?: TRoot,
25+
reviver?: BSONReviver
26+
) => TRoot extends undefined ? Record<string, unknown> : TRoot['dest'];
27+
// Types
28+
BSONElement: BSONElement;
29+
Container: Container;
30+
BSONReviver: BSONReviver;
1531
};
1632

1733
/**
@@ -21,6 +37,7 @@ export type OnDemand = {
2137
const onDemand: OnDemand = Object.create(null);
2238

2339
onDemand.parseToElements = parseToElements;
40+
onDemand.parseToStructure = parseToStructure;
2441
onDemand.BSONOffsetError = BSONOffsetError;
2542

2643
Object.freeze(onDemand);

Diff for: src/parser/on_demand/parse_to_elements.ts

+41-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
21
import { BSONOffsetError } from '../../error';
32

43
/**
@@ -9,7 +8,7 @@ import { BSONOffsetError } from '../../error';
98
* - `minKey` is set to 255 so unsigned comparisons succeed
109
* - Modify with caution, double check the bundle contains literals
1110
*/
12-
const enum t {
11+
const enum BSONElementType {
1312
double = 1,
1413
string = 2,
1514
object = 3,
@@ -45,8 +44,11 @@ export type BSONElement = [
4544
length: number
4645
];
4746

48-
/** Parses a int32 little-endian at offset, throws if it is negative */
49-
function getSize(source: Uint8Array, offset: number): number {
47+
/**
48+
* @internal
49+
* Parses a int32 little-endian at offset, throws if it is negative
50+
*/
51+
export function getSize(source: Uint8Array, offset: number): number {
5052
if (source[offset + 3] > 127) {
5153
throw new BSONOffsetError('BSON size cannot be negative', offset);
5254
}
@@ -80,7 +82,12 @@ function findNull(bytes: Uint8Array, offset: number): number {
8082
* @public
8183
* @experimental
8284
*/
83-
export function parseToElements(bytes: Uint8Array, startOffset = 0): Iterable<BSONElement> {
85+
export function parseToElements(
86+
bytes: Uint8Array,
87+
startOffset: number | null = 0
88+
): Iterable<BSONElement> {
89+
startOffset ??= 0;
90+
8491
if (bytes.length < 5) {
8592
throw new BSONOffsetError(
8693
`Input must be at least 5 bytes, got ${bytes.length} bytes`,
@@ -121,37 +128,51 @@ export function parseToElements(bytes: Uint8Array, startOffset = 0): Iterable<BS
121128

122129
let length: number;
123130

124-
if (type === t.double || type === t.long || type === t.date || type === t.timestamp) {
131+
if (
132+
type === BSONElementType.double ||
133+
type === BSONElementType.long ||
134+
type === BSONElementType.date ||
135+
type === BSONElementType.timestamp
136+
) {
125137
length = 8;
126-
} else if (type === t.int) {
138+
} else if (type === BSONElementType.int) {
127139
length = 4;
128-
} else if (type === t.objectId) {
140+
} else if (type === BSONElementType.objectId) {
129141
length = 12;
130-
} else if (type === t.decimal) {
142+
} else if (type === BSONElementType.decimal) {
131143
length = 16;
132-
} else if (type === t.bool) {
144+
} else if (type === BSONElementType.bool) {
133145
length = 1;
134-
} else if (type === t.null || type === t.undefined || type === t.maxKey || type === t.minKey) {
146+
} else if (
147+
type === BSONElementType.null ||
148+
type === BSONElementType.undefined ||
149+
type === BSONElementType.maxKey ||
150+
type === BSONElementType.minKey
151+
) {
135152
length = 0;
136153
}
137154
// Needs a size calculation
138-
else if (type === t.regex) {
155+
else if (type === BSONElementType.regex) {
139156
length = findNull(bytes, findNull(bytes, offset) + 1) + 1 - offset;
140-
} else if (type === t.object || type === t.array || type === t.javascriptWithScope) {
157+
} else if (
158+
type === BSONElementType.object ||
159+
type === BSONElementType.array ||
160+
type === BSONElementType.javascriptWithScope
161+
) {
141162
length = getSize(bytes, offset);
142163
} else if (
143-
type === t.string ||
144-
type === t.binData ||
145-
type === t.dbPointer ||
146-
type === t.javascript ||
147-
type === t.symbol
164+
type === BSONElementType.string ||
165+
type === BSONElementType.binData ||
166+
type === BSONElementType.dbPointer ||
167+
type === BSONElementType.javascript ||
168+
type === BSONElementType.symbol
148169
) {
149170
length = getSize(bytes, offset) + 4;
150-
if (type === t.binData) {
171+
if (type === BSONElementType.binData) {
151172
// binary subtype
152173
length += 1;
153174
}
154-
if (type === t.dbPointer) {
175+
if (type === BSONElementType.dbPointer) {
155176
// dbPointer's objectId
156177
length += 12;
157178
}

Diff for: src/parser/on_demand/parse_to_structure.ts

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { type Code } from '../../code';
2+
import { type BSONElement, getSize, parseToElements } from './parse_to_elements';
3+
4+
/** @internal */
5+
const DEFAULT_REVIVER: BSONReviver = (
6+
_bytes: Uint8Array,
7+
_container: Container,
8+
_element: BSONElement
9+
) => null;
10+
11+
/** @internal */
12+
function parseToElementsToArray(bytes: Uint8Array, offset?: number | null): BSONElement[] {
13+
const res = parseToElements(bytes, offset);
14+
return Array.isArray(res) ? res : [...res];
15+
}
16+
17+
/** @internal */
18+
type ParseContext = {
19+
elementOffset: number;
20+
elements: BSONElement[];
21+
container: Container;
22+
previous: ParseContext | null;
23+
};
24+
25+
/**
26+
* @experimental
27+
* @public
28+
* A union of the possible containers for BSON elements.
29+
*
30+
* Depending on kind, a reviver can accurately assign a value to a name on the container.
31+
*/
32+
export type Container =
33+
| {
34+
dest: Record<string, unknown>;
35+
kind: 'object';
36+
}
37+
| {
38+
dest: Map<string, unknown>;
39+
kind: 'map';
40+
}
41+
| {
42+
dest: Array<unknown>;
43+
kind: 'array';
44+
}
45+
| {
46+
dest: Code;
47+
kind: 'code';
48+
}
49+
| {
50+
kind: 'custom';
51+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52+
dest: any;
53+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
54+
[key: string]: any;
55+
};
56+
57+
/**
58+
* @experimental
59+
* @public
60+
*/
61+
export type BSONReviver = (
62+
bytes: Uint8Array,
63+
container: Container,
64+
element: BSONElement
65+
) => Container | null;
66+
67+
/**
68+
* @experimental
69+
* @public
70+
*/
71+
export function parseToStructure<
72+
TRoot extends Container = {
73+
dest: Record<string, unknown>;
74+
kind: 'object';
75+
}
76+
>(
77+
bytes: Uint8Array,
78+
startOffset?: number | null,
79+
pRoot?: TRoot | null,
80+
pReviver?: BSONReviver | null
81+
): TRoot extends undefined ? Record<string, unknown> : TRoot['dest'] {
82+
const root = pRoot ?? {
83+
kind: 'object',
84+
dest: Object.create(null) as Record<string, unknown>
85+
};
86+
87+
const reviver = pReviver ?? DEFAULT_REVIVER;
88+
89+
let ctx: ParseContext | null = {
90+
elementOffset: 0,
91+
elements: parseToElementsToArray(bytes, startOffset),
92+
container: root,
93+
previous: null
94+
};
95+
96+
/** BSONElement offsets: type indicator and value offset */
97+
const enum BSONElementOffset {
98+
type = 0,
99+
offset = 3
100+
}
101+
102+
/** BSON Embedded types */
103+
const enum BSONElementType {
104+
object = 3,
105+
array = 4,
106+
javascriptWithScope = 15
107+
}
108+
109+
embedded: while (ctx !== null) {
110+
for (
111+
let bsonElement: BSONElement | undefined = ctx.elements[ctx.elementOffset++];
112+
bsonElement != null;
113+
bsonElement = ctx.elements[ctx.elementOffset++]
114+
) {
115+
const type = bsonElement[BSONElementOffset.type];
116+
const offset = bsonElement[BSONElementOffset.offset];
117+
118+
const container = reviver(bytes, ctx.container, bsonElement);
119+
const isEmbeddedType =
120+
type === BSONElementType.object ||
121+
type === BSONElementType.array ||
122+
type === BSONElementType.javascriptWithScope;
123+
124+
if (container != null && isEmbeddedType) {
125+
const docOffset: number =
126+
type !== BSONElementType.javascriptWithScope
127+
? offset
128+
: // value offset + codeSize + value int + code int
129+
offset + getSize(bytes, offset + 4) + 4 + 4;
130+
131+
ctx = {
132+
elementOffset: 0,
133+
elements: parseToElementsToArray(bytes, docOffset),
134+
container,
135+
previous: ctx
136+
};
137+
138+
continue embedded;
139+
}
140+
}
141+
ctx = ctx.previous;
142+
}
143+
144+
return root.dest;
145+
}

0 commit comments

Comments
 (0)