diff --git a/package-lock.json b/package-lock.json index 6cfd573be19..4e0220d4e45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "js-yaml": "^4.1.0", "mocha": "^9.2.2", "mocha-sinon": "^2.1.2", + "mongodb-legacy": "^4.0.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "rimraf": "^3.0.2", @@ -6827,6 +6828,24 @@ "node": ">=0.10.0" } }, + "node_modules/mongodb": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz", + "integrity": "sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==", + "dev": true, + "dependencies": { + "bson": "^4.7.0", + "mongodb-connection-string-url": "^2.5.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=12.9.0" + }, + "optionalDependencies": { + "@aws-sdk/credential-providers": "^3.186.0", + "saslprep": "^1.0.3" + } + }, "node_modules/mongodb-connection-string-url": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", @@ -6845,6 +6864,18 @@ "@types/webidl-conversions": "*" } }, + "node_modules/mongodb-legacy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mongodb-legacy/-/mongodb-legacy-4.0.0.tgz", + "integrity": "sha512-bO7di48oL4NNAPbDL4cFU+1movNoncxVTIX52m5At7lYB40apV8VD9cSO79FEZ0wYvQ3YL2q8+rpjVAEEKfegg==", + "dev": true, + "dependencies": { + "mongodb": "^4.10.0" + }, + "engines": { + "node": ">=12.9.0" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -14701,6 +14732,19 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "mongodb": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz", + "integrity": "sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==", + "dev": true, + "requires": { + "@aws-sdk/credential-providers": "^3.186.0", + "bson": "^4.7.0", + "mongodb-connection-string-url": "^2.5.4", + "saslprep": "^1.0.3", + "socks": "^2.7.1" + } + }, "mongodb-connection-string-url": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", @@ -14721,6 +14765,15 @@ } } }, + "mongodb-legacy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mongodb-legacy/-/mongodb-legacy-4.0.0.tgz", + "integrity": "sha512-bO7di48oL4NNAPbDL4cFU+1movNoncxVTIX52m5At7lYB40apV8VD9cSO79FEZ0wYvQ3YL2q8+rpjVAEEKfegg==", + "dev": true, + "requires": { + "mongodb": "^4.10.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index c4b4b5049a4..e8a7de29753 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "js-yaml": "^4.1.0", "mocha": "^9.2.2", "mocha-sinon": "^2.1.2", + "mongodb-legacy": "^4.0.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "rimraf": "^3.0.2", diff --git a/test/mongodb.ts b/test/mongodb.ts index 539927b6634..ae57f624a07 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-restricted-imports */ import * as fs from 'node:fs'; import * as path from 'node:path'; +import * as process from 'node:process'; +import * as vm from 'node:vm'; // eslint-disable-next-line @typescript-eslint/no-unused-vars function printExports() { @@ -23,6 +25,75 @@ function printExports() { } } +/** + * Using node's require resolution logic this function will locate the entrypoint for the `'mongodb-legacy'` module, + * then execute the `mongodb-legacy` module in a `vm` context that replaces the global require function with a custom + * implementation. The custom version of `require` will return the local instance of the driver import (magically compiled by ts-node) when + * the module specifier is 'mongodb' and otherwise defer to the normal require behavior to import relative files and stdlib modules. + * Each of the legacy module's patched classes are placed on the input object. + * + * @param exportsToOverride - An object that is an import of the MongoDB driver to be modified by this function + */ +function importMongoDBLegacy(exportsToOverride: Record) { + const mongodbLegacyEntryPoint = require.resolve('mongodb-legacy'); + const mongodbLegacyLocation = path.dirname(mongodbLegacyEntryPoint); + const mongodbLegacyIndex = fs.readFileSync(mongodbLegacyEntryPoint, { + encoding: 'utf8' + }); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const localMongoDB = require('../src/index'); + const ctx = vm.createContext({ + module: { exports: null }, + require: (mod: string) => { + if (mod === 'mongodb') { + return localMongoDB; + } else if (mod.startsWith('.')) { + return require(path.join(mongodbLegacyLocation, mod)); + } + return require(mod); + } + }); + vm.runInContext(mongodbLegacyIndex, ctx); + + const mongodbLegacy = ctx.module.exports; + + Object.defineProperty(exportsToOverride, 'Admin', { get: () => mongodbLegacy.Admin }); + Object.defineProperty(exportsToOverride, 'FindCursor', { get: () => mongodbLegacy.FindCursor }); + Object.defineProperty(exportsToOverride, 'ListCollectionsCursor', { + get: () => mongodbLegacy.ListCollectionsCursor + }); + Object.defineProperty(exportsToOverride, 'ListIndexesCursor', { + get: () => mongodbLegacy.ListIndexesCursor + }); + Object.defineProperty(exportsToOverride, 'AggregationCursor', { + get: () => mongodbLegacy.AggregationCursor + }); + Object.defineProperty(exportsToOverride, 'ChangeStream', { + get: () => mongodbLegacy.ChangeStream + }); + Object.defineProperty(exportsToOverride, 'Collection', { get: () => mongodbLegacy.Collection }); + Object.defineProperty(exportsToOverride, 'Db', { get: () => mongodbLegacy.Db }); + Object.defineProperty(exportsToOverride, 'GridFSBucket', { + get: () => mongodbLegacy.GridFSBucket + }); + Object.defineProperty(exportsToOverride, 'ClientSession', { + get: () => mongodbLegacy.ClientSession + }); + Object.defineProperty(exportsToOverride, 'MongoClient', { get: () => mongodbLegacy.MongoClient }); + Object.defineProperty(exportsToOverride, 'ClientSession', { + get: () => mongodbLegacy.ClientSession + }); + Object.defineProperty(exportsToOverride, 'GridFSBucketWriteStream', { + get: () => mongodbLegacy.GridFSBucketWriteStream + }); + Object.defineProperty(exportsToOverride, 'OrderedBulkOperation', { + get: () => mongodbLegacy.OrderedBulkOperation + }); + Object.defineProperty(exportsToOverride, 'UnorderedBulkOperation', { + get: () => mongodbLegacy.UnorderedBulkOperation + }); +} + export * from '../src/admin'; export * from '../src/bson'; export * from '../src/bulk/common'; @@ -125,3 +196,16 @@ export * from '../src/write_concern'; // Must be last for precedence export * from '../src/index'; + +/** + * TODO(NODE-4979): ENABLE_MONGODB_LEGACY is 'true' by default for now + */ +const ENABLE_MONGODB_LEGACY = + typeof process.env.ENABLE_MONGODB_LEGACY === 'string' && process.env.ENABLE_MONGODB_LEGACY !== '' + ? process.env.ENABLE_MONGODB_LEGACY + : 'true'; + +if (ENABLE_MONGODB_LEGACY === 'true') { + // Override our own exports with the legacy patched ones + importMongoDBLegacy(module.exports); +} diff --git a/test/unit/assorted/client.test.js b/test/unit/assorted/client.test.js index 1bfbf8b7836..b3645b83125 100644 --- a/test/unit/assorted/client.test.js +++ b/test/unit/assorted/client.test.js @@ -7,6 +7,7 @@ const { isHello } = require('../../mongodb'); describe('Client (unit)', function () { let server, client; + const isLegacyMongoClient = MongoClient.name === 'LegacyMongoClient'; afterEach(async () => { await client.close(); @@ -38,7 +39,12 @@ describe('Client (unit)', function () { return client.connect().then(() => { expect(handshake).to.have.nested.property('client.driver'); - expect(handshake).nested.property('client.driver.name').to.equal('nodejs|mongoose'); + expect(handshake) + .nested.property('client.driver.name') + // Currently the tests import either MongoClient or LegacyMongoClient, the latter of which overrides the client metadata + // We still are confirming here that a third party wrapper can set the metadata but it will change depending on the + // MongoClient constructor that is imported + .to.equal(isLegacyMongoClient ? 'nodejs|mongodb-legacy|mongoose' : 'nodejs|mongoose'); expect(handshake) .nested.property('client.driver.version') .to.match(/|5.7.10/); diff --git a/test/unit/cursor/abstract_cursor.test.ts b/test/unit/cursor/abstract_cursor.test.ts index b2bb254652b..56135f39c65 100644 --- a/test/unit/cursor/abstract_cursor.test.ts +++ b/test/unit/cursor/abstract_cursor.test.ts @@ -11,7 +11,7 @@ import { Server } from '../../mongodb'; -/** Minimal do nothing cursor to focus on testing the base cusor behavior */ +/** Minimal do nothing cursor to focus on testing the base cursor behavior */ class ConcreteCursor extends AbstractCursor { constructor(client: MongoClient, options: AbstractCursorOptions = {}) { super(client, ns('test.test'), options); diff --git a/test/unit/tools/mongodb-legacy.test.ts b/test/unit/tools/mongodb-legacy.test.ts new file mode 100644 index 00000000000..229fb03b4f1 --- /dev/null +++ b/test/unit/tools/mongodb-legacy.test.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; + +import { + Admin, + AggregationCursor, + ChangeStream, + ClientSession, + Collection, + Db, + FindCursor, + GridFSBucket, + GridFSBucketWriteStream, + ListCollectionsCursor, + ListIndexesCursor, + MongoClient, + OrderedBulkOperation, + UnorderedBulkOperation +} from '../../mongodb'; + +const classesWithAsyncAPIs = new Map([ + ['Admin', Admin], + ['FindCursor', FindCursor], + ['ListCollectionsCursor', ListCollectionsCursor], + ['ListIndexesCursor', ListIndexesCursor], + ['AggregationCursor', AggregationCursor], + ['ChangeStream', ChangeStream], + ['Collection', Collection], + ['Db', Db], + ['GridFSBucket', GridFSBucket], + ['ClientSession', ClientSession], + ['GridFSBucketWriteStream', GridFSBucketWriteStream], + ['OrderedBulkOperation', OrderedBulkOperation], + ['UnorderedBulkOperation', UnorderedBulkOperation] +]); + +describe('mongodb-legacy', () => { + for (const [className, ctor] of classesWithAsyncAPIs) { + it(`test suite imports a ${className} with the legacy symbol`, () => { + // Just confirming that the mongodb-legacy import is correctly overriding the local copies + // of these classes from "src". See test/mongodb.ts for more. + expect(ctor.prototype).to.have.property(Symbol.for('@@mdb.callbacks.toLegacy')); + }); + } + it('test suite imports a LegacyMongoClient as MongoClient', () => { + // Just confirming that the mongodb-legacy import is correctly overriding the local copy + // of MongoClient from "src". See test/mongodb.ts for more. + expect(MongoClient).to.have.property('name', 'LegacyMongoClient'); + }); +});