Skip to content

Commit d9f0eaa

Browse files
nbbeekendurran
andauthored
feat(NODE-4892)!: error on bson types not from this version (#543)
Co-authored-by: Durran Jordan <[email protected]>
1 parent 946866d commit d9f0eaa

32 files changed

+512
-647
lines changed

docs/upgrade-to-v5.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,19 @@ try {
288288
throw error;
289289
}
290290
```
291+
292+
### Explicit cross version incompatibility
293+
294+
Starting with v5.0.0 of the BSON library instances of types from previous versions will throw an error when passed to the serializer.
295+
This is to ensure that types are always serialized correctly and that there is no unexpected silent BSON serialization mistakes that could occur when mixing versions.
296+
It's unexpected for any applications to have more than one version of the BSON library but with nested dependencies and re-exporting, this new error will illuminate those incorrect combinations.
297+
298+
```ts
299+
// npm install bson4@npm:bson@4
300+
// npm install bson5@npm:bson@5
301+
import { ObjectId } from 'bson4';
302+
import { serialize } from 'bson5';
303+
304+
serialize({ _id: new ObjectId() });
305+
// Uncaught BSONVersionError: Unsupported BSON version, bson types must be from bson 5.0 or later
306+
```

src/binary.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { EJSONOptions } from './extended_json';
44
import { BSONError } from './error';
55
import { BSON_BINARY_SUBTYPE_UUID_NEW } from './constants';
66
import { ByteUtils } from './utils/byte_utils';
7+
import { BSONValue } from './bson_value';
78

89
/** @public */
910
export type BinarySequence = Uint8Array | number[];
@@ -27,7 +28,7 @@ export interface BinaryExtended {
2728
* @public
2829
* @category BSONType
2930
*/
30-
export class Binary {
31+
export class Binary extends BSONValue {
3132
get _bsontype(): 'Binary' {
3233
return 'Binary';
3334
}
@@ -75,6 +76,7 @@ export class Binary {
7576
* @param subType - the option binary type.
7677
*/
7778
constructor(buffer?: string | BinarySequence, subType?: number) {
79+
super();
7880
if (
7981
!(buffer == null) &&
8082
!(typeof buffer === 'string') &&

src/bson.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export {
4949
BSONRegExp,
5050
Decimal128
5151
};
52-
export { BSONError } from './error';
52+
export { BSONValue } from './bson_value';
53+
export { BSONError, BSONVersionError } from './error';
5354
export { BSONType } from './constants';
5455
export { EJSON } from './extended_json';
5556

src/bson_value.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BSON_MAJOR_VERSION } from './constants';
2+
3+
/** @public */
4+
export abstract class BSONValue {
5+
/** @public */
6+
public abstract get _bsontype(): string;
7+
8+
/** @internal */
9+
get [Symbol.for('@@mdb.bson.version')](): typeof BSON_MAJOR_VERSION {
10+
return BSON_MAJOR_VERSION;
11+
}
12+
13+
/** @public */
14+
public abstract inspect(): string;
15+
16+
/** @internal */
17+
abstract toExtendedJSON(): unknown;
18+
}

src/code.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Document } from './bson';
2+
import { BSONValue } from './bson_value';
23

34
/** @public */
45
export interface CodeExtended {
@@ -11,7 +12,7 @@ export interface CodeExtended {
1112
* @public
1213
* @category BSONType
1314
*/
14-
export class Code {
15+
export class Code extends BSONValue {
1516
get _bsontype(): 'Code' {
1617
return 'Code';
1718
}
@@ -27,6 +28,7 @@ export class Code {
2728
* @param scope - an optional scope for the function.
2829
*/
2930
constructor(code: string | Function, scope?: Document | null) {
31+
super();
3032
this.code = code.toString();
3133
this.scope = scope ?? null;
3234
}

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/** @internal */
2+
export const BSON_MAJOR_VERSION = 5 as const;
3+
14
/** @internal */
25
export const BSON_INT32_MAX = 0x7fffffff;
36
/** @internal */

src/db_ref.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Document } from './bson';
2+
import { BSONValue } from './bson_value';
23
import type { EJSONOptions } from './extended_json';
34
import type { ObjectId } from './objectid';
45

@@ -28,7 +29,7 @@ export function isDBRefLike(value: unknown): value is DBRefLike {
2829
* @public
2930
* @category BSONType
3031
*/
31-
export class DBRef {
32+
export class DBRef extends BSONValue {
3233
get _bsontype(): 'DBRef' {
3334
return 'DBRef';
3435
}
@@ -44,6 +45,7 @@ export class DBRef {
4445
* @param db - optional db name, if omitted the reference is local to the current db.
4546
*/
4647
constructor(collection: string, oid: ObjectId, db?: string, fields?: Document) {
48+
super();
4749
// check if namespace has been provided
4850
const parts = collection.split('.');
4951
if (parts.length === 2) {

src/decimal128.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BSONValue } from './bson_value';
12
import { BSONError } from './error';
23
import { Long } from './long';
34
import { isUint8Array } from './parser/utils';
@@ -126,7 +127,7 @@ export interface Decimal128Extended {
126127
* @public
127128
* @category BSONType
128129
*/
129-
export class Decimal128 {
130+
export class Decimal128 extends BSONValue {
130131
get _bsontype(): 'Decimal128' {
131132
return 'Decimal128';
132133
}
@@ -138,6 +139,7 @@ export class Decimal128 {
138139
* or a string representation as returned by .toString()
139140
*/
140141
constructor(bytes: Uint8Array | string) {
142+
super();
141143
if (typeof bytes === 'string') {
142144
this.bytes = Decimal128.fromString(bytes).bytes;
143145
} else if (isUint8Array(bytes)) {

src/double.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BSONValue } from './bson_value';
12
import type { EJSONOptions } from './extended_json';
23

34
/** @public */
@@ -10,7 +11,7 @@ export interface DoubleExtended {
1011
* @public
1112
* @category BSONType
1213
*/
13-
export class Double {
14+
export class Double extends BSONValue {
1415
get _bsontype(): 'Double' {
1516
return 'Double';
1617
}
@@ -22,6 +23,7 @@ export class Double {
2223
* @param value - the number we want to represent as a double.
2324
*/
2425
constructor(value: number) {
26+
super();
2527
if ((value as unknown) instanceof Number) {
2628
value = value.valueOf();
2729
}

src/error.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { BSON_MAJOR_VERSION } from './constants';
2+
13
/**
24
* @public
35
* `BSONError` objects are thrown when runtime errors occur.
@@ -43,3 +45,16 @@ export class BSONError extends Error {
4345
);
4446
}
4547
}
48+
49+
/** @public */
50+
export class BSONVersionError extends BSONError {
51+
get name(): 'BSONVersionError' {
52+
return 'BSONVersionError';
53+
}
54+
55+
constructor() {
56+
super(
57+
`Unsupported BSON version, bson types must be from bson ${BSON_MAJOR_VERSION}.0 or later`
58+
);
59+
}
60+
}

src/extended_json.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { Binary } from './binary';
22
import type { Document } from './bson';
33
import { Code } from './code';
4-
import { BSON_INT32_MAX, BSON_INT32_MIN, BSON_INT64_MAX, BSON_INT64_MIN } from './constants';
4+
import {
5+
BSON_INT32_MAX,
6+
BSON_INT32_MIN,
7+
BSON_INT64_MAX,
8+
BSON_INT64_MIN,
9+
BSON_MAJOR_VERSION
10+
} from './constants';
511
import { DBRef, isDBRefLike } from './db_ref';
612
import { Decimal128 } from './decimal128';
713
import { Double } from './double';
8-
import { BSONError } from './error';
14+
import { BSONError, BSONVersionError } from './error';
915
import { Int32 } from './int_32';
1016
import { Long } from './long';
1117
import { MaxKey } from './max_key';
@@ -273,13 +279,9 @@ const BSON_TYPE_MAPPINGS = {
273279
),
274280
MaxKey: () => new MaxKey(),
275281
MinKey: () => new MinKey(),
276-
ObjectID: (o: ObjectId) => new ObjectId(o),
277-
// The _bsontype for ObjectId is spelled with a capital "D", to the mapping above will be used (most of the time)
278-
// specifically BSON versions 4.0.0 and 4.0.1 the _bsontype was changed to "ObjectId" so we keep this mapping to support
279-
// those version of BSON
280282
ObjectId: (o: ObjectId) => new ObjectId(o),
281283
BSONRegExp: (o: BSONRegExp) => new BSONRegExp(o.pattern, o.options),
282-
Symbol: (o: BSONSymbol) => new BSONSymbol(o.value),
284+
BSONSymbol: (o: BSONSymbol) => new BSONSymbol(o.value),
283285
Timestamp: (o: Timestamp) => Timestamp.fromBits(o.low, o.high)
284286
} as const;
285287

@@ -310,6 +312,13 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) {
310312
}
311313
}
312314
return _doc;
315+
} else if (
316+
doc != null &&
317+
typeof doc === 'object' &&
318+
typeof doc._bsontype === 'string' &&
319+
doc[Symbol.for('@@mdb.bson.version')] !== BSON_MAJOR_VERSION
320+
) {
321+
throw new BSONVersionError();
313322
} else if (isBSONType(doc)) {
314323
// the "document" is really just a BSON type object
315324
// eslint-disable-next-line @typescript-eslint/no-explicit-any

src/int_32.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BSONValue } from './bson_value';
12
import type { EJSONOptions } from './extended_json';
23

34
/** @public */
@@ -10,7 +11,7 @@ export interface Int32Extended {
1011
* @public
1112
* @category BSONType
1213
*/
13-
export class Int32 {
14+
export class Int32 extends BSONValue {
1415
get _bsontype(): 'Int32' {
1516
return 'Int32';
1617
}
@@ -22,6 +23,7 @@ export class Int32 {
2223
* @param value - the number we want to represent as an int32.
2324
*/
2425
constructor(value: number | string) {
26+
super();
2527
if ((value as unknown) instanceof Number) {
2628
value = value.valueOf();
2729
}

src/long.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BSONValue } from './bson_value';
12
import { BSONError } from './error';
23
import type { EJSONOptions } from './extended_json';
34
import type { Timestamp } from './timestamp';
@@ -99,7 +100,7 @@ export interface LongExtended {
99100
* case would often result in infinite recursion.
100101
* Common constant values ZERO, ONE, NEG_ONE, etc. are found as static properties on this class.
101102
*/
102-
export class Long {
103+
export class Long extends BSONValue {
103104
get _bsontype(): 'Long' {
104105
return 'Long';
105106
}
@@ -138,6 +139,7 @@ export class Long {
138139
* @param unsigned - Whether unsigned or not, defaults to signed
139140
*/
140141
constructor(low: number | bigint | string = 0, high?: number | boolean, unsigned?: boolean) {
142+
super();
141143
if (typeof low === 'bigint') {
142144
Object.assign(this, Long.fromBigInt(low, !!high));
143145
} else if (typeof low === 'string') {

src/max_key.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { BSONValue } from './bson_value';
2+
13
/** @public */
24
export interface MaxKeyExtended {
35
$maxKey: 1;
@@ -8,7 +10,7 @@ export interface MaxKeyExtended {
810
* @public
911
* @category BSONType
1012
*/
11-
export class MaxKey {
13+
export class MaxKey extends BSONValue {
1214
get _bsontype(): 'MaxKey' {
1315
return 'MaxKey';
1416
}

src/min_key.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { BSONValue } from './bson_value';
2+
13
/** @public */
24
export interface MinKeyExtended {
35
$minKey: 1;
@@ -8,7 +10,7 @@ export interface MinKeyExtended {
810
* @public
911
* @category BSONType
1012
*/
11-
export class MinKey {
13+
export class MinKey extends BSONValue {
1214
get _bsontype(): 'MinKey' {
1315
return 'MinKey';
1416
}

src/objectid.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BSONValue } from './bson_value';
12
import { BSONError } from './error';
23
import { isUint8Array } from './parser/utils';
34
import { BSONDataView, ByteUtils } from './utils/byte_utils';
@@ -27,9 +28,9 @@ const kId = Symbol('id');
2728
* @public
2829
* @category BSONType
2930
*/
30-
export class ObjectId {
31-
get _bsontype(): 'ObjectID' {
32-
return 'ObjectID';
31+
export class ObjectId extends BSONValue {
32+
get _bsontype(): 'ObjectId' {
33+
return 'ObjectId';
3334
}
3435

3536
/** @internal */
@@ -48,6 +49,7 @@ export class ObjectId {
4849
* @param inputId - Can be a 24 character hex string, 12 byte binary Buffer, or a number.
4950
*/
5051
constructor(inputId?: string | number | ObjectId | ObjectIdLike | Uint8Array) {
52+
super();
5153
// workingId is set based on type of input and whether valid id exists for the input
5254
let workingId;
5355
if (typeof inputId === 'object' && inputId && 'id' in inputId) {

0 commit comments

Comments
 (0)