Skip to content

Commit

Permalink
feat(NODE-4892)!: error on bson types not from this version (#543)
Browse files Browse the repository at this point in the history
Co-authored-by: Durran Jordan <[email protected]>
  • Loading branch information
nbbeeken and durran authored Jan 13, 2023
1 parent 946866d commit d9f0eaa
Show file tree
Hide file tree
Showing 32 changed files with 512 additions and 647 deletions.
16 changes: 16 additions & 0 deletions docs/upgrade-to-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,19 @@ try {
throw error;
}
```

### Explicit cross version incompatibility

Starting with v5.0.0 of the BSON library instances of types from previous versions will throw an error when passed to the serializer.
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.
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.

```ts
// npm install bson4@npm:bson@4
// npm install bson5@npm:bson@5
import { ObjectId } from 'bson4';
import { serialize } from 'bson5';

serialize({ _id: new ObjectId() });
// Uncaught BSONVersionError: Unsupported BSON version, bson types must be from bson 5.0 or later
```
4 changes: 3 additions & 1 deletion src/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { EJSONOptions } from './extended_json';
import { BSONError } from './error';
import { BSON_BINARY_SUBTYPE_UUID_NEW } from './constants';
import { ByteUtils } from './utils/byte_utils';
import { BSONValue } from './bson_value';

/** @public */
export type BinarySequence = Uint8Array | number[];
Expand All @@ -27,7 +28,7 @@ export interface BinaryExtended {
* @public
* @category BSONType
*/
export class Binary {
export class Binary extends BSONValue {
get _bsontype(): 'Binary' {
return 'Binary';
}
Expand Down Expand Up @@ -75,6 +76,7 @@ export class Binary {
* @param subType - the option binary type.
*/
constructor(buffer?: string | BinarySequence, subType?: number) {
super();
if (
!(buffer == null) &&
!(typeof buffer === 'string') &&
Expand Down
3 changes: 2 additions & 1 deletion src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export {
BSONRegExp,
Decimal128
};
export { BSONError } from './error';
export { BSONValue } from './bson_value';
export { BSONError, BSONVersionError } from './error';
export { BSONType } from './constants';
export { EJSON } from './extended_json';

Expand Down
18 changes: 18 additions & 0 deletions src/bson_value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BSON_MAJOR_VERSION } from './constants';

/** @public */
export abstract class BSONValue {
/** @public */
public abstract get _bsontype(): string;

/** @internal */
get [Symbol.for('@@mdb.bson.version')](): typeof BSON_MAJOR_VERSION {
return BSON_MAJOR_VERSION;
}

/** @public */
public abstract inspect(): string;

/** @internal */
abstract toExtendedJSON(): unknown;
}
4 changes: 3 additions & 1 deletion src/code.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Document } from './bson';
import { BSONValue } from './bson_value';

/** @public */
export interface CodeExtended {
Expand All @@ -11,7 +12,7 @@ export interface CodeExtended {
* @public
* @category BSONType
*/
export class Code {
export class Code extends BSONValue {
get _bsontype(): 'Code' {
return 'Code';
}
Expand All @@ -27,6 +28,7 @@ export class Code {
* @param scope - an optional scope for the function.
*/
constructor(code: string | Function, scope?: Document | null) {
super();
this.code = code.toString();
this.scope = scope ?? null;
}
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/** @internal */
export const BSON_MAJOR_VERSION = 5 as const;

/** @internal */
export const BSON_INT32_MAX = 0x7fffffff;
/** @internal */
Expand Down
4 changes: 3 additions & 1 deletion src/db_ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Document } from './bson';
import { BSONValue } from './bson_value';
import type { EJSONOptions } from './extended_json';
import type { ObjectId } from './objectid';

Expand Down Expand Up @@ -28,7 +29,7 @@ export function isDBRefLike(value: unknown): value is DBRefLike {
* @public
* @category BSONType
*/
export class DBRef {
export class DBRef extends BSONValue {
get _bsontype(): 'DBRef' {
return 'DBRef';
}
Expand All @@ -44,6 +45,7 @@ export class DBRef {
* @param db - optional db name, if omitted the reference is local to the current db.
*/
constructor(collection: string, oid: ObjectId, db?: string, fields?: Document) {
super();
// check if namespace has been provided
const parts = collection.split('.');
if (parts.length === 2) {
Expand Down
4 changes: 3 additions & 1 deletion src/decimal128.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BSONValue } from './bson_value';
import { BSONError } from './error';
import { Long } from './long';
import { isUint8Array } from './parser/utils';
Expand Down Expand Up @@ -126,7 +127,7 @@ export interface Decimal128Extended {
* @public
* @category BSONType
*/
export class Decimal128 {
export class Decimal128 extends BSONValue {
get _bsontype(): 'Decimal128' {
return 'Decimal128';
}
Expand All @@ -138,6 +139,7 @@ export class Decimal128 {
* or a string representation as returned by .toString()
*/
constructor(bytes: Uint8Array | string) {
super();
if (typeof bytes === 'string') {
this.bytes = Decimal128.fromString(bytes).bytes;
} else if (isUint8Array(bytes)) {
Expand Down
4 changes: 3 additions & 1 deletion src/double.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BSONValue } from './bson_value';
import type { EJSONOptions } from './extended_json';

/** @public */
Expand All @@ -10,7 +11,7 @@ export interface DoubleExtended {
* @public
* @category BSONType
*/
export class Double {
export class Double extends BSONValue {
get _bsontype(): 'Double' {
return 'Double';
}
Expand All @@ -22,6 +23,7 @@ export class Double {
* @param value - the number we want to represent as a double.
*/
constructor(value: number) {
super();
if ((value as unknown) instanceof Number) {
value = value.valueOf();
}
Expand Down
15 changes: 15 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BSON_MAJOR_VERSION } from './constants';

/**
* @public
* `BSONError` objects are thrown when runtime errors occur.
Expand Down Expand Up @@ -43,3 +45,16 @@ export class BSONError extends Error {
);
}
}

/** @public */
export class BSONVersionError extends BSONError {
get name(): 'BSONVersionError' {
return 'BSONVersionError';
}

constructor() {
super(
`Unsupported BSON version, bson types must be from bson ${BSON_MAJOR_VERSION}.0 or later`
);
}
}
23 changes: 16 additions & 7 deletions src/extended_json.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Binary } from './binary';
import type { Document } from './bson';
import { Code } from './code';
import { BSON_INT32_MAX, BSON_INT32_MIN, BSON_INT64_MAX, BSON_INT64_MIN } from './constants';
import {
BSON_INT32_MAX,
BSON_INT32_MIN,
BSON_INT64_MAX,
BSON_INT64_MIN,
BSON_MAJOR_VERSION
} from './constants';
import { DBRef, isDBRefLike } from './db_ref';
import { Decimal128 } from './decimal128';
import { Double } from './double';
import { BSONError } from './error';
import { BSONError, BSONVersionError } from './error';
import { Int32 } from './int_32';
import { Long } from './long';
import { MaxKey } from './max_key';
Expand Down Expand Up @@ -273,13 +279,9 @@ const BSON_TYPE_MAPPINGS = {
),
MaxKey: () => new MaxKey(),
MinKey: () => new MinKey(),
ObjectID: (o: ObjectId) => new ObjectId(o),
// The _bsontype for ObjectId is spelled with a capital "D", to the mapping above will be used (most of the time)
// specifically BSON versions 4.0.0 and 4.0.1 the _bsontype was changed to "ObjectId" so we keep this mapping to support
// those version of BSON
ObjectId: (o: ObjectId) => new ObjectId(o),
BSONRegExp: (o: BSONRegExp) => new BSONRegExp(o.pattern, o.options),
Symbol: (o: BSONSymbol) => new BSONSymbol(o.value),
BSONSymbol: (o: BSONSymbol) => new BSONSymbol(o.value),
Timestamp: (o: Timestamp) => Timestamp.fromBits(o.low, o.high)
} as const;

Expand Down Expand Up @@ -310,6 +312,13 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) {
}
}
return _doc;
} else if (
doc != null &&
typeof doc === 'object' &&
typeof doc._bsontype === 'string' &&
doc[Symbol.for('@@mdb.bson.version')] !== BSON_MAJOR_VERSION
) {
throw new BSONVersionError();
} else if (isBSONType(doc)) {
// the "document" is really just a BSON type object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
4 changes: 3 additions & 1 deletion src/int_32.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BSONValue } from './bson_value';
import type { EJSONOptions } from './extended_json';

/** @public */
Expand All @@ -10,7 +11,7 @@ export interface Int32Extended {
* @public
* @category BSONType
*/
export class Int32 {
export class Int32 extends BSONValue {
get _bsontype(): 'Int32' {
return 'Int32';
}
Expand All @@ -22,6 +23,7 @@ export class Int32 {
* @param value - the number we want to represent as an int32.
*/
constructor(value: number | string) {
super();
if ((value as unknown) instanceof Number) {
value = value.valueOf();
}
Expand Down
4 changes: 3 additions & 1 deletion src/long.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BSONValue } from './bson_value';
import { BSONError } from './error';
import type { EJSONOptions } from './extended_json';
import type { Timestamp } from './timestamp';
Expand Down Expand Up @@ -99,7 +100,7 @@ export interface LongExtended {
* case would often result in infinite recursion.
* Common constant values ZERO, ONE, NEG_ONE, etc. are found as static properties on this class.
*/
export class Long {
export class Long extends BSONValue {
get _bsontype(): 'Long' {
return 'Long';
}
Expand Down Expand Up @@ -138,6 +139,7 @@ export class Long {
* @param unsigned - Whether unsigned or not, defaults to signed
*/
constructor(low: number | bigint | string = 0, high?: number | boolean, unsigned?: boolean) {
super();
if (typeof low === 'bigint') {
Object.assign(this, Long.fromBigInt(low, !!high));
} else if (typeof low === 'string') {
Expand Down
4 changes: 3 additions & 1 deletion src/max_key.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BSONValue } from './bson_value';

/** @public */
export interface MaxKeyExtended {
$maxKey: 1;
Expand All @@ -8,7 +10,7 @@ export interface MaxKeyExtended {
* @public
* @category BSONType
*/
export class MaxKey {
export class MaxKey extends BSONValue {
get _bsontype(): 'MaxKey' {
return 'MaxKey';
}
Expand Down
4 changes: 3 additions & 1 deletion src/min_key.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BSONValue } from './bson_value';

/** @public */
export interface MinKeyExtended {
$minKey: 1;
Expand All @@ -8,7 +10,7 @@ export interface MinKeyExtended {
* @public
* @category BSONType
*/
export class MinKey {
export class MinKey extends BSONValue {
get _bsontype(): 'MinKey' {
return 'MinKey';
}
Expand Down
8 changes: 5 additions & 3 deletions src/objectid.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BSONValue } from './bson_value';
import { BSONError } from './error';
import { isUint8Array } from './parser/utils';
import { BSONDataView, ByteUtils } from './utils/byte_utils';
Expand Down Expand Up @@ -27,9 +28,9 @@ const kId = Symbol('id');
* @public
* @category BSONType
*/
export class ObjectId {
get _bsontype(): 'ObjectID' {
return 'ObjectID';
export class ObjectId extends BSONValue {
get _bsontype(): 'ObjectId' {
return 'ObjectId';
}

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

0 comments on commit d9f0eaa

Please sign in to comment.