Skip to content

Commit f4b16d9

Browse files
justingrantdaprahamian
authored andcommitted
fix: 4.x-1.x interop (incl. ObjectID _bsontype)
Enables documents created by 4.x to be parsed/serialized/deserialized by 1.x BSON (e.g. the current Node driver) and vice-versa. Fiesx Object ID interop with the current Node driver and with existing duck-typing code that depends on ObjectId._bsontype === 'ObjectID' Fixes NODE-1873
1 parent 6be7b8d commit f4b16d9

9 files changed

+315
-84
lines changed

Diff for: lib/db_ref.js

+7
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,11 @@ class DBRef {
6868
}
6969

7070
Object.defineProperty(DBRef.prototype, '_bsontype', { value: 'DBRef' });
71+
// the 1.x parser used a "namespace" property, while 4.x uses "collection". To ensure backwards
72+
// compatibility, let's expose "namespace"
73+
Object.defineProperty(DBRef.prototype, 'namespace', {
74+
get() { return this.collection; },
75+
set(val) { this.collection = val; },
76+
configurable: false
77+
});
7178
module.exports = DBRef;

Diff for: lib/extended_json.js

+54-24
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function deserializeValue(self, key, value, options) {
7070
const date = new Date();
7171

7272
if (typeof d === 'string') date.setTime(Date.parse(d));
73-
else if (d instanceof Long) date.setTime(d.toNumber());
73+
else if (Long.isLong(d)) date.setTime(d.toNumber());
7474
else if (typeof d === 'number' && options.relaxed) date.setTime(d);
7575
return date;
7676
}
@@ -265,38 +265,68 @@ function serializeValue(value, options) {
265265
return value;
266266
}
267267

268+
const BSON_TYPE_MAPPINGS = {
269+
Binary: o => new Binary(o.value(), o.subtype),
270+
Code: o => new Code(o.code, o.scope),
271+
DBRef: o => new DBRef(o.collection || o.namespace, o.oid, o.db, o.fields), // "namespace" for 1.x library backwards compat
272+
Decimal128: o => new Decimal128(o.bytes),
273+
Double: o => new Double(o.value),
274+
Int32: o => new Int32(o.value),
275+
Long: o => Long.fromBits( // underscore variants for 1.x backwards compatibility
276+
o.low != null ? o.low : o.low_,
277+
o.low != null ? o.high : o.high_,
278+
o.low != null ? o.unsigned : o.unsigned_
279+
),
280+
MaxKey: o => new MaxKey(),
281+
MinKey: o => new MinKey(),
282+
ObjectID: o => new ObjectId(o),
283+
ObjectId: o => new ObjectId(o), // support 4.0.0/4.0.1 before _bsontype was reverted back to ObjectID
284+
BSONRegExp: o => new BSONRegExp(o.pattern, o.options),
285+
Symbol: o => new Symbol(o.value),
286+
Timestamp: o => Timestamp.fromBits(o.low, o.high)
287+
};
288+
268289
function serializeDocument(doc, options) {
269290
if (doc == null || typeof doc !== 'object') throw new Error('not an object instance');
270291

271-
// the "document" is really just a BSON type
272-
if (doc._bsontype) {
273-
if (doc._bsontype === 'ObjectID') {
274-
// Deprecated ObjectID class with capital "D" is still used (e.g. by 'mongodb' package). It has
275-
// no "toExtendedJSON" method, so convert to new ObjectId (lowercase "d") class before serializing
276-
doc = ObjectId.createFromHexString(doc.toString());
292+
const bsontype = doc._bsontype;
293+
if (typeof bsontype === 'undefined') {
294+
295+
// It's a regular object. Recursively serialize its property values.
296+
const _doc = {};
297+
for (let name in doc) {
298+
_doc[name] = serializeValue(doc[name], options);
277299
}
278-
if (typeof doc.toExtendedJSON === 'function') {
279-
// TODO: the two cases below mutate the original document! Bad. I don't know
280-
// enough about these two BSON types to know how to safely clone these objects, but
281-
// someone who knows MongoDB better should fix this to clone instead of mutating input objects.
282-
if (doc._bsontype === 'Code' && doc.scope) {
283-
doc.scope = serializeDocument(doc.scope, options);
284-
} else if (doc._bsontype === 'DBRef' && doc.oid) {
285-
doc.oid = serializeDocument(doc.oid, options);
300+
return _doc;
301+
302+
} else if (typeof bsontype === 'string') {
303+
304+
// the "document" is really just a BSON type object
305+
let _doc = doc;
306+
if (typeof _doc.toExtendedJSON !== 'function') {
307+
// There's no EJSON serialization function on the object. It's probably an
308+
// object created by a previous version of this library (or another library)
309+
// that's duck-typing objects to look like they were generated by this library).
310+
// Copy the object into this library's version of that type.
311+
const mapper = BSON_TYPE_MAPPINGS[bsontype];
312+
if (!mapper) {
313+
throw new TypeError('Unrecognized or invalid _bsontype: ' + bsontype);
286314
}
315+
_doc = mapper(_doc);
316+
}
287317

288-
return doc.toExtendedJSON(options);
318+
// Two BSON types may have nested objects that may need to be serialized too
319+
if (bsontype === 'Code' && _doc.scope) {
320+
_doc = new Code(_doc.code, serializeValue(_doc.scope, options));
321+
} else if (bsontype === 'DBRef' && _doc.oid) {
322+
_doc = new DBRef(_doc.collection, serializeValue(_doc.oid, options), _doc.db, _doc.fields);
289323
}
290-
// TODO: should we throw an exception if there's a BSON type that has no toExtendedJSON method?
291-
}
292324

293-
// Recursively serialize this document's property values.
294-
const _doc = {};
295-
for (let name in doc) {
296-
_doc[name] = serializeValue(doc[name], options);
297-
}
325+
return _doc.toExtendedJSON(options);
298326

299-
return _doc;
327+
} else {
328+
throw new Error('_bsontype must be a string, but was: ' + typeof bsontype);
329+
}
300330
}
301331

302332
module.exports = {

Diff for: lib/objectid.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class ObjectId {
5252
/**
5353
* Create an ObjectId type
5454
*
55-
* @param {(string|number)} id Can be a 24 byte hex string, 12 byte binary string or a Number.
55+
* @param {(string|Buffer|number)} id Can be a 24 byte hex string, 12 byte binary Buffer, or a Number.
5656
* @property {number} generationTime The generation time of this ObjectId instance
5757
* @return {ObjectId} instance of ObjectId.
5858
*/
@@ -87,7 +87,7 @@ class ObjectId {
8787
this.id = id;
8888
} else if (id != null && id.toHexString) {
8989
// Duck-typing to support ObjectId from different npm packages
90-
return id;
90+
return ObjectId.createFromHexString(id.toHexString());
9191
} else {
9292
throw new TypeError(
9393
'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
@@ -210,7 +210,7 @@ class ObjectId {
210210
* Compares the equality of this ObjectId with `otherID`.
211211
*
212212
* @method
213-
* @param {object} otherID ObjectId instance to compare against.
213+
* @param {object} otherId ObjectId instance to compare against.
214214
* @return {boolean} the result of comparing two ObjectId's
215215
*/
216216
equals(otherId) {
@@ -246,7 +246,7 @@ class ObjectId {
246246
* Returns the generation date (accurate up to the second) that this ID was generated.
247247
*
248248
* @method
249-
* @return {date} the generation date
249+
* @return {Date} the generation date
250250
*/
251251
getTimestamp() {
252252
const timestamp = new Date();
@@ -411,5 +411,8 @@ ObjectId.prototype[util.inspect.custom || 'inspect'] = ObjectId.prototype.toStri
411411
*/
412412
ObjectId.index = ~~(Math.random() * 0xffffff);
413413

414-
Object.defineProperty(ObjectId.prototype, '_bsontype', { value: 'ObjectId' });
414+
// In 4.0.0 and 4.0.1, this property name was changed to ObjectId to match the class name.
415+
// This caused interoperability problems with previous versions of the library, so in
416+
// later builds we changed it back to ObjectID (capital D) to match legacy implementations.
417+
Object.defineProperty(ObjectId.prototype, '_bsontype', { value: 'ObjectID' });
415418
module.exports = ObjectId;

Diff for: lib/parser/calculate_size.js

+8-28
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
'use strict';
22

33
const Buffer = require('buffer').Buffer;
4-
const Long = require('../long');
5-
const Double = require('../double');
6-
const Timestamp = require('../timestamp');
7-
const ObjectId = require('../objectid');
8-
const BSONSymbol = require('../symbol');
9-
const BSONRegExp = require('../regexp');
10-
const Code = require('../code');
11-
const Decimal128 = require('../decimal128');
12-
const MinKey = require('../min_key');
13-
const MaxKey = require('../max_key');
14-
const DBRef = require('../db_ref');
154
const Binary = require('../binary');
165
const normalizedFunctionString = require('./utils').normalizedFunctionString;
176
const constants = require('../constants');
@@ -86,15 +75,9 @@ function calculateElement(name, value, serializeFunctions, isArray, ignoreUndefi
8675
case 'boolean':
8776
return (name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + (1 + 1);
8877
case 'object':
89-
if (
90-
value == null ||
91-
value instanceof MinKey ||
92-
value instanceof MaxKey ||
93-
value['_bsontype'] === 'MinKey' ||
94-
value['_bsontype'] === 'MaxKey'
95-
) {
78+
if (value == null || value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') {
9679
return (name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + 1;
97-
} else if (value instanceof ObjectId || value['_bsontype'] === 'ObjectId') {
80+
} else if (value['_bsontype'] === 'ObjectId' || value['_bsontype'] === 'ObjectID') {
9881
return (name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + (12 + 1);
9982
} else if (value instanceof Date || isDate(value)) {
10083
return (name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + (8 + 1);
@@ -103,17 +86,14 @@ function calculateElement(name, value, serializeFunctions, isArray, ignoreUndefi
10386
(name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + (1 + 4 + 1) + value.length
10487
);
10588
} else if (
106-
value instanceof Long ||
107-
value instanceof Double ||
108-
value instanceof Timestamp ||
10989
value['_bsontype'] === 'Long' ||
11090
value['_bsontype'] === 'Double' ||
11191
value['_bsontype'] === 'Timestamp'
11292
) {
11393
return (name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + (8 + 1);
114-
} else if (value instanceof Decimal128 || value['_bsontype'] === 'Decimal128') {
94+
} else if (value['_bsontype'] === 'Decimal128') {
11595
return (name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + (16 + 1);
116-
} else if (value instanceof Code || value['_bsontype'] === 'Code') {
96+
} else if (value['_bsontype'] === 'Code') {
11797
// Calculate size depending on the availability of a scope
11898
if (value.scope != null && Object.keys(value.scope).length > 0) {
11999
return (
@@ -134,7 +114,7 @@ function calculateElement(name, value, serializeFunctions, isArray, ignoreUndefi
134114
1
135115
);
136116
}
137-
} else if (value instanceof Binary || value['_bsontype'] === 'Binary') {
117+
} else if (value['_bsontype'] === 'Binary') {
138118
// Check what kind of subtype we have
139119
if (value.sub_type === Binary.SUBTYPE_BYTE_ARRAY) {
140120
return (
@@ -146,15 +126,15 @@ function calculateElement(name, value, serializeFunctions, isArray, ignoreUndefi
146126
(name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) + (value.position + 1 + 4 + 1)
147127
);
148128
}
149-
} else if (value instanceof BSONSymbol || value['_bsontype'] === 'Symbol') {
129+
} else if (value['_bsontype'] === 'Symbol') {
150130
return (
151131
(name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) +
152132
Buffer.byteLength(value.value, 'utf8') +
153133
4 +
154134
1 +
155135
1
156136
);
157-
} else if (value instanceof DBRef || value['_bsontype'] === 'DBRef') {
137+
} else if (value['_bsontype'] === 'DBRef') {
158138
// Set up correct object for serialization
159139
const ordered_values = Object.assign(
160140
{
@@ -188,7 +168,7 @@ function calculateElement(name, value, serializeFunctions, isArray, ignoreUndefi
188168
(value.multiline ? 1 : 0) +
189169
1
190170
);
191-
} else if (value instanceof BSONRegExp || value['_bsontype'] === 'BSONRegExp') {
171+
} else if (value['_bsontype'] === 'BSONRegExp') {
192172
return (
193173
(name != null ? Buffer.byteLength(name, 'utf8') + 1 : 0) +
194174
1 +

Diff for: lib/parser/serializer.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const Buffer = require('buffer').Buffer;
44
const writeIEEE754 = require('../float_parser').writeIEEE754;
55
const Long = require('../long');
66
const Map = require('../map');
7-
const MinKey = require('../min_key');
87
const Binary = require('../binary');
98
const constants = require('../constants');
109
const normalizedFunctionString = require('./utils').normalizedFunctionString;
@@ -254,7 +253,7 @@ function serializeMinMax(buffer, key, value, index, isArray) {
254253
// Write the type of either min or max key
255254
if (value === null) {
256255
buffer[index++] = constants.BSON_DATA_NULL;
257-
} else if (value instanceof MinKey) {
256+
} else if (value._bsontype === 'MinKey') {
258257
buffer[index++] = constants.BSON_DATA_MIN_KEY;
259258
} else {
260259
buffer[index++] = constants.BSON_DATA_MAX_KEY;
@@ -640,7 +639,7 @@ function serializeDBRef(buffer, key, value, index, depth, serializeFunctions, is
640639
let startIndex = index;
641640
let endIndex;
642641
let output = {
643-
$ref: value.collection,
642+
$ref: value.collection || value.namespace, // "namespace" was what library 1.x called "collection"
644643
$id: value.oid
645644
};
646645

@@ -765,6 +764,8 @@ function serializeInto(
765764
index = serializeInt32(buffer, key, value, index, true);
766765
} else if (value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') {
767766
index = serializeMinMax(buffer, key, value, index, true);
767+
} else if (typeof value['_bsontype'] !== 'undefined') {
768+
throw new TypeError('Unrecognized or invalid _bsontype: ' + value['_bsontype']);
768769
}
769770
}
770771
} else if (object instanceof Map) {
@@ -862,6 +863,8 @@ function serializeInto(
862863
index = serializeInt32(buffer, key, value, index);
863864
} else if (value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') {
864865
index = serializeMinMax(buffer, key, value, index);
866+
} else if (typeof value['_bsontype'] !== 'undefined') {
867+
throw new TypeError('Unrecognized or invalid _bsontype: ' + value['_bsontype']);
865868
}
866869
}
867870
} else {
@@ -964,6 +967,8 @@ function serializeInto(
964967
index = serializeInt32(buffer, key, value, index);
965968
} else if (value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') {
966969
index = serializeMinMax(buffer, key, value, index);
970+
} else if (typeof value['_bsontype'] !== 'undefined') {
971+
throw new TypeError('Unrecognized or invalid _bsontype: ' + value['_bsontype']);
967972
}
968973
}
969974
}

Diff for: lib/timestamp.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const Long = require('./long');
1010
*/
1111
class Timestamp extends Long {
1212
constructor(low, high) {
13-
if (low instanceof Long) {
13+
if (Long.isLong(low)) {
1414
super(low.low, low.high);
1515
} else {
1616
super(low, high);

Diff for: test/node/bson_test.js

+45-7
Original file line numberDiff line numberDiff line change
@@ -2286,8 +2286,34 @@ describe('BSON', function() {
22862286
});
22872287

22882288
it('should serialize ObjectIds from old bson versions', function() {
2289-
// create a wrapper simulating the old ObjectID class
2290-
class ObjectID {
2289+
// In versions 4.0.0 and 4.0.1, we used _bsontype="ObjectId" which broke
2290+
// backwards compatibility with mongodb-core and other code. It was reverted
2291+
// back to "ObjectID" (capital D) in later library versions.
2292+
// The test below ensures that all three versions of Object ID work OK:
2293+
// 1. The current version's class
2294+
// 2. A simulation of the class from library 4.0.0
2295+
// 3. The class currently in use by mongodb (not tested in browser where mongodb is unavailable)
2296+
2297+
// test the old ObjectID class (in mongodb-core 3.1) because MongoDB drivers still return it
2298+
function getOldBSON() {
2299+
try {
2300+
// do a dynamic resolve to avoid exception when running browser tests
2301+
const file = require.resolve('mongodb-core');
2302+
const oldModule = require(file).BSON;
2303+
const funcs = new oldModule.BSON();
2304+
oldModule.serialize = funcs.serialize;
2305+
oldModule.deserialize = funcs.deserialize;
2306+
return oldModule;
2307+
} catch (e) {
2308+
return BSON; // if mongo is unavailable, e.g. browser tests, just re-use new BSON
2309+
}
2310+
}
2311+
2312+
const OldBSON = getOldBSON();
2313+
const OldObjectID = OldBSON === BSON ? BSON.ObjectId : OldBSON.ObjectID;
2314+
2315+
// create a wrapper simulating the old ObjectId class from v4.0.0
2316+
class ObjectIdv400 {
22912317
constructor() {
22922318
this.oid = new ObjectId();
22932319
}
@@ -2298,10 +2324,10 @@ describe('BSON', function() {
22982324
return this.oid.toString();
22992325
}
23002326
}
2301-
Object.defineProperty(ObjectID.prototype, '_bsontype', { value: 'ObjectID' });
2327+
Object.defineProperty(ObjectIdv400.prototype, '_bsontype', { value: 'ObjectId' });
23022328

23032329
// Array
2304-
const array = [new ObjectID(), new ObjectId()];
2330+
const array = [new ObjectIdv400(), new OldObjectID(), new ObjectId()];
23052331
const deserializedArrayAsMap = BSON.deserialize(BSON.serialize(array));
23062332
const deserializedArray = Object.keys(deserializedArrayAsMap).map(
23072333
x => deserializedArrayAsMap[x]
@@ -2310,7 +2336,8 @@ describe('BSON', function() {
23102336

23112337
// Map
23122338
const map = new Map();
2313-
map.set('oldBsonType', new ObjectID());
2339+
map.set('oldBsonType', new ObjectIdv400());
2340+
map.set('reallyOldBsonType', new OldObjectID());
23142341
map.set('newBsonType', new ObjectId());
23152342
const deserializedMapAsObject = BSON.deserialize(BSON.serialize(map), { relaxed: false });
23162343
const deserializedMap = new Map(
@@ -2324,10 +2351,21 @@ describe('BSON', function() {
23242351
});
23252352

23262353
// Object
2327-
const record = { oldBsonType: new ObjectID(), newBsonType: new ObjectId() };
2354+
const record = { oldBsonType: new ObjectIdv400(), reallyOldBsonType: new OldObjectID, newBsonType: new ObjectId() };
23282355
const deserializedObject = BSON.deserialize(BSON.serialize(record));
2329-
expect(deserializedObject).to.have.keys(['oldBsonType', 'newBsonType']);
2356+
expect(deserializedObject).to.have.keys(['oldBsonType', 'reallyOldBsonType', 'newBsonType']);
23302357
expect(record.oldBsonType.toString()).to.equal(deserializedObject.oldBsonType.toString());
23312358
expect(record.newBsonType.toString()).to.equal(deserializedObject.newBsonType.toString());
23322359
});
2360+
2361+
it('should throw if invalid BSON types are input to BSON serializer', function() {
2362+
const oid = new ObjectId('111111111111111111111111');
2363+
const badBsonType = Object.assign({}, oid, { _bsontype: 'bogus' });
2364+
const badDoc = { bad: badBsonType };
2365+
const badArray = [oid, badDoc];
2366+
const badMap = new Map([['a', badBsonType], ['b', badDoc], ['c', badArray]]);
2367+
expect(() => BSON.serialize(badDoc)).to.throw();
2368+
expect(() => BSON.serialize(badArray)).to.throw();
2369+
expect(() => BSON.serialize(badMap)).to.throw();
2370+
});
23332371
});

0 commit comments

Comments
 (0)