Skip to content

Commit 32d1204

Browse files
committed
feat(NODE-5958): add BSON iterating API
1 parent 2ac17ec commit 32d1204

File tree

8 files changed

+577
-8
lines changed

8 files changed

+577
-8
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
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
{

src/bson.ts

Lines changed: 1 addition & 1 deletion
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 {

src/parser/on_demand/index.ts

Lines changed: 9 additions & 0 deletions
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,13 @@ export type OnDemand = {
1213
isBSONError(value: unknown): value is BSONError;
1314
};
1415
parseToElements: (this: void, bytes: Uint8Array, startOffset?: number) => Iterable<BSONElement>;
16+
parseToStructure: <TRoot = Record<string, unknown>>(
17+
this: void,
18+
bytes: Uint8Array,
19+
offset?: number,
20+
root?: Container,
21+
reviver?: BSONReviver
22+
) => TRoot;
1523
};
1624

1725
/**
@@ -21,6 +29,7 @@ export type OnDemand = {
2129
const onDemand: OnDemand = Object.create(null);
2230

2331
onDemand.parseToElements = parseToElements;
32+
onDemand.parseToStructure = parseToStructure;
2433
onDemand.BSONOffsetError = BSONOffsetError;
2534

2635
Object.freeze(onDemand);

src/parser/on_demand/parse_to_elements.ts

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

0 commit comments

Comments
 (0)