From a2359e43d7a59acf69785104b5f6e6c1516a5e27 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 18 May 2022 11:59:27 -0400 Subject: [PATCH] feat(NODE-4192): make MongoClient.connect optional (#3232) --- global.d.ts | 6 +- src/admin.ts | 14 +- src/bulk/common.ts | 8 +- src/change_stream.ts | 26 +- src/cmap/wire_protocol/shared.ts | 6 +- src/collection.ts | 61 +- src/cursor/abstract_cursor.ts | 29 +- src/cursor/aggregation_cursor.ts | 12 +- src/cursor/find_cursor.ts | 14 +- src/db.ts | 32 +- src/mongo_client.ts | 16 +- src/operations/connect.ts | 1 + src/operations/execute_operation.ts | 51 +- src/operations/indexes.ts | 12 +- src/operations/list_collections.ts | 6 +- src/sdam/topology.ts | 6 +- src/sessions.ts | 29 +- src/utils.ts | 2 +- .../connection.test.js | 53 +- .../node-specific/mongo_client.test.js | 384 ----------- .../node-specific/mongo_client.test.ts | 631 ++++++++++++++++++ test/unit/cursor/aggregation_cursor.test.js | 43 +- test/unit/cursor/find_cursor.test.js | 59 +- test/unit/sessions.test.js | 2 +- 24 files changed, 881 insertions(+), 622 deletions(-) delete mode 100644 test/integration/node-specific/mongo_client.test.js create mode 100644 test/integration/node-specific/mongo_client.test.ts diff --git a/global.d.ts b/global.d.ts index d28457ca1c..356eb2e45d 100644 --- a/global.d.ts +++ b/global.d.ts @@ -73,7 +73,7 @@ declare global { * An optional string the test author can attach to print out why a test is skipped * * @example - * ``` + * ```ts * it.skip('my test', () => { * //... * }).skipReason = 'TODO(NODE-XXXX): Feature implementation impending!'; @@ -81,13 +81,13 @@ declare global { * * The reporter (`test/tools/reporter/mongodb_reporter.js`) will print out the skipReason * indented directly below the test name. - * ``` + * ```txt * - my test * - TODO(NODE-XXXX): Feature implementation impending! * ``` * * You can also skip a set of tests via beforeEach: - * ``` + * ```ts * beforeEach(() => { * if ('some condition') { * this.currentTest.skipReason = 'requires to run'; diff --git a/src/admin.ts b/src/admin.ts index 78d9df17cf..303af58a71 100644 --- a/src/admin.ts +++ b/src/admin.ts @@ -83,7 +83,7 @@ export class Admin { options = Object.assign({ dbName: 'admin' }, options); return executeOperation( - this.s.db, + this.s.db.s.client, new RunCommandOperation(this.s.db, command, options), callback ); @@ -207,7 +207,7 @@ export class Admin { options = Object.assign({ dbName: 'admin' }, options); return executeOperation( - this.s.db, + this.s.db.s.client, new AddUserOperation(this.s.db, username, password, options), callback ); @@ -233,7 +233,7 @@ export class Admin { options = Object.assign({ dbName: 'admin' }, options); return executeOperation( - this.s.db, + this.s.db.s.client, new RemoveUserOperation(this.s.db, username, options), callback ); @@ -263,7 +263,7 @@ export class Admin { options = options ?? {}; return executeOperation( - this.s.db, + this.s.db.s.client, new ValidateCollectionOperation(this, collectionName, options), callback ); @@ -286,7 +286,11 @@ export class Admin { if (typeof options === 'function') (callback = options), (options = {}); options = options ?? {}; - return executeOperation(this.s.db, new ListDatabasesOperation(this.s.db, options), callback); + return executeOperation( + this.s.db.s.client, + new ListDatabasesOperation(this.s.db, options), + callback + ); } /** diff --git a/src/bulk/common.ts b/src/bulk/common.ts index 9a050f918a..41b69da657 100644 --- a/src/bulk/common.ts +++ b/src/bulk/common.ts @@ -655,19 +655,19 @@ function executeCommands( try { if (isInsertBatch(batch)) { executeOperation( - bulkOperation.s.collection, + bulkOperation.s.collection.s.db.s.client, new InsertOperation(bulkOperation.s.namespace, batch.operations, finalOptions), resultHandler ); } else if (isUpdateBatch(batch)) { executeOperation( - bulkOperation.s.collection, + bulkOperation.s.collection.s.db.s.client, new UpdateOperation(bulkOperation.s.namespace, batch.operations, finalOptions), resultHandler ); } else if (isDeleteBatch(batch)) { executeOperation( - bulkOperation.s.collection, + bulkOperation.s.collection.s.db.s.client, new DeleteOperation(bulkOperation.s.namespace, batch.operations, finalOptions), resultHandler ); @@ -1288,7 +1288,7 @@ export abstract class BulkOperationBase { const finalOptions = { ...this.s.options, ...options }; const operation = new BulkWriteShimOperation(this, finalOptions); - return executeOperation(this.s.collection, operation, callback); + return executeOperation(this.s.collection.s.db.s.client, operation, callback); } /** diff --git a/src/change_stream.ts b/src/change_stream.ts index 7472f11113..86aafa6373 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -600,8 +600,24 @@ export class ChangeStream< const cursorOptions: ChangeStreamCursorOptions = filterOptions(options, CURSOR_OPTIONS); + const client: MongoClient | null = + this.type === CHANGE_DOMAIN_TYPES.CLUSTER + ? (this.parent as MongoClient) + : this.type === CHANGE_DOMAIN_TYPES.DATABASE + ? (this.parent as Db).s.client + : this.type === CHANGE_DOMAIN_TYPES.COLLECTION + ? (this.parent as Collection).s.db.s.client + : null; + + if (client == null) { + // This should never happen because of the assertion in the constructor + throw new MongoRuntimeError( + `Changestream type should only be one of cluster, database, collection. Found ${this.type.toString()}` + ); + } + const changeStreamCursor = new ChangeStreamCursor( - getTopology(this.parent), + client, this.namespace, pipeline, cursorOptions @@ -835,12 +851,12 @@ export class ChangeStreamCursor< pipeline: Document[]; constructor( - topology: Topology, + client: MongoClient, namespace: MongoDBNamespace, pipeline: Document[] = [], options: ChangeStreamCursorOptions = {} ) { - super(topology, namespace, options); + super(client, namespace, options); this.pipeline = pipeline; this.options = options; @@ -907,7 +923,7 @@ export class ChangeStreamCursor< } clone(): AbstractCursor { - return new ChangeStreamCursor(this.topology, this.namespace, this.pipeline, { + return new ChangeStreamCursor(this.client, this.namespace, this.pipeline, { ...this.cursorOptions }); } @@ -920,7 +936,7 @@ export class ChangeStreamCursor< }); executeOperation>( - session, + session.client, aggregateOperation, (err, response) => { if (err || response == null) { diff --git a/src/cmap/wire_protocol/shared.ts b/src/cmap/wire_protocol/shared.ts index ee4a35c0d4..bc13ff6d85 100644 --- a/src/cmap/wire_protocol/shared.ts +++ b/src/cmap/wire_protocol/shared.ts @@ -56,7 +56,11 @@ export function applyCommonQueryOptions( return queryOptions; } -export function isSharded(topologyOrServer: Topology | Server | Connection): boolean { +export function isSharded(topologyOrServer?: Topology | Server | Connection): boolean { + if (topologyOrServer == null) { + return false; + } + if (topologyOrServer.description && topologyOrServer.description.type === ServerType.Mongos) { return true; } diff --git a/src/collection.ts b/src/collection.ts index b749b8e76b..8bb2fb0b8a 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -93,7 +93,6 @@ import { checkCollectionName, DEFAULT_PK_FACTORY, emitWarningOnce, - getTopology, MongoDBNamespace, normalizeHintField, resolveOptions @@ -296,7 +295,7 @@ export class Collection { } return executeOperation( - this, + this.s.db.s.client, new InsertOneOperation( this as TODO_NODE_3286, doc, @@ -338,7 +337,7 @@ export class Collection { options = options ? Object.assign({}, options) : { ordered: true }; return executeOperation( - this, + this.s.db.s.client, new InsertManyOperation( this as TODO_NODE_3286, docs, @@ -406,7 +405,7 @@ export class Collection { } return executeOperation( - this, + this.s.db.s.client, new BulkWriteOperation( this as TODO_NODE_3286, operations as TODO_NODE_3286, @@ -453,7 +452,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new UpdateOneOperation( this as TODO_NODE_3286, filter, @@ -501,7 +500,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new ReplaceOneOperation( this as TODO_NODE_3286, filter, @@ -549,7 +548,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new UpdateManyOperation( this as TODO_NODE_3286, filter, @@ -583,7 +582,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new DeleteOneOperation(this as TODO_NODE_3286, filter, resolveOptions(this, options)), callback ); @@ -623,7 +622,7 @@ export class Collection { } return executeOperation( - this, + this.s.db.s.client, new DeleteManyOperation(this as TODO_NODE_3286, filter, resolveOptions(this, options)), callback ); @@ -652,7 +651,7 @@ export class Collection { // Intentionally, we do not inherit options from parent for this operation. return executeOperation( - this, + this.s.db.s.client, new RenameOperation(this as TODO_NODE_3286, newName, { ...options, readPreference: ReadPreference.PRIMARY @@ -679,7 +678,7 @@ export class Collection { options = options ?? {}; return executeOperation( - this, + this.s.db.s.client, new DropCollectionOperation(this.s.db, this.collectionName, options), callback ); @@ -759,7 +758,7 @@ export class Collection { } return new FindCursor>( - getTopology(this), + this.s.db.s.client, this.s.namespace, filter, resolveOptions(this as TODO_NODE_3286, options) @@ -783,7 +782,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new OptionsOperation(this as TODO_NODE_3286, resolveOptions(this, options)), callback ); @@ -806,7 +805,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new IsCappedOperation(this as TODO_NODE_3286, resolveOptions(this, options)), callback ); @@ -857,7 +856,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new CreateIndexOperation( this as TODO_NODE_3286, this.collectionName, @@ -918,7 +917,7 @@ export class Collection { if (typeof options.maxTimeMS !== 'number') delete options.maxTimeMS; return executeOperation( - this, + this.s.db.s.client, new CreateIndexesOperation( this as TODO_NODE_3286, this.collectionName, @@ -952,7 +951,7 @@ export class Collection { options.readPreference = ReadPreference.primary; return executeOperation( - this, + this.s.db.s.client, new DropIndexOperation(this as TODO_NODE_3286, indexName, options), callback ); @@ -975,7 +974,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new DropIndexesOperation(this as TODO_NODE_3286, resolveOptions(this, options)), callback ); @@ -1013,7 +1012,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new IndexExistsOperation(this as TODO_NODE_3286, indexes, resolveOptions(this, options)), callback ); @@ -1036,7 +1035,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new IndexInformationOperation(this.s.db, this.collectionName, resolveOptions(this, options)), callback ); @@ -1058,7 +1057,7 @@ export class Collection { ): Promise | void { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new EstimatedDocumentCountOperation(this as TODO_NODE_3286, resolveOptions(this, options)), callback ); @@ -1118,7 +1117,7 @@ export class Collection { filter ??= {}; return executeOperation( - this, + this.s.db.s.client, new CountDocumentsOperation( this as TODO_NODE_3286, filter as Document, @@ -1193,7 +1192,7 @@ export class Collection { filter ??= {}; return executeOperation( - this, + this.s.db.s.client, new DistinctOperation( this as TODO_NODE_3286, key as TODO_NODE_3286, @@ -1221,7 +1220,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new IndexesOperation(this as TODO_NODE_3286, resolveOptions(this, options)), callback ); @@ -1245,7 +1244,7 @@ export class Collection { options = options ?? {}; return executeOperation( - this, + this.s.db.s.client, new CollStatsOperation(this as TODO_NODE_3286, options), callback ); @@ -1277,7 +1276,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new FindOneAndDeleteOperation( this as TODO_NODE_3286, filter, @@ -1324,7 +1323,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new FindOneAndReplaceOperation( this as TODO_NODE_3286, filter, @@ -1372,7 +1371,7 @@ export class Collection { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.db.s.client, new FindOneAndUpdateOperation( this as TODO_NODE_3286, filter, @@ -1408,7 +1407,7 @@ export class Collection { } return new AggregationCursor( - getTopology(this), + this.s.db.s.client, this.s.namespace, pipeline, resolveOptions(this, options) @@ -1526,7 +1525,7 @@ export class Collection { } return executeOperation( - this, + this.s.db.s.client, new MapReduceOperation( this as TODO_NODE_3286, map, @@ -1667,7 +1666,7 @@ export class Collection { filter ??= {}; return executeOperation( - this, + this.s.db.s.client, new CountOperation( MongoDBNamespace.fromString(this.namespace), filter, diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 0b562d7758..851f1d5f05 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -10,13 +10,13 @@ import { MongoRuntimeError, MongoTailableCursorError } from '../error'; +import type { MongoClient } from '../mongo_client'; import { TODO_NODE_3286, TypedEventEmitter } from '../mongo_types'; import { executeOperation, ExecutionResult } from '../operations/execute_operation'; import { GetMoreOperation } from '../operations/get_more'; import { ReadConcern, ReadConcernLike } from '../read_concern'; import { ReadPreference, ReadPreferenceLike } from '../read_preference'; import type { Server } from '../sdam/server'; -import type { Topology } from '../sdam/topology'; import { ClientSession, maybeClearPinnedConnection } from '../sessions'; import { Callback, maybePromise, MongoDBNamespace, ns } from '../utils'; @@ -29,7 +29,7 @@ const kServer = Symbol('server'); /** @internal */ const kNamespace = Symbol('namespace'); /** @internal */ -const kTopology = Symbol('topology'); +const kClient = Symbol('client'); /** @internal */ const kSession = Symbol('session'); /** @internal */ @@ -126,7 +126,7 @@ export abstract class AbstractCursor< /** @internal */ [kDocuments]: TSchema[]; /** @internal */ - [kTopology]: Topology; + [kClient]: MongoClient; /** @internal */ [kTransform]?: (doc: TSchema) => any; /** @internal */ @@ -143,13 +143,16 @@ export abstract class AbstractCursor< /** @internal */ constructor( - topology: Topology, + client: MongoClient, namespace: MongoDBNamespace, options: AbstractCursorOptions = {} ) { super(); - this[kTopology] = topology; + if (!client.s.isMongoClient) { + throw new MongoRuntimeError('Cursor must be constructed with MongoClient'); + } + this[kClient] = client; this[kNamespace] = namespace; this[kDocuments] = []; // TODO: https://github.com/microsoft/TypeScript/issues/36230 this[kInitialized] = false; @@ -192,8 +195,8 @@ export abstract class AbstractCursor< } /** @internal */ - get topology(): Topology { - return this[kTopology]; + get client(): MongoClient { + return this[kClient]; } /** @internal */ @@ -236,7 +239,7 @@ export abstract class AbstractCursor< } get loadBalanced(): boolean { - return this[kTopology].loadBalanced; + return !!this[kClient].topology?.loadBalanced; } /** Returns current buffered documents length */ @@ -630,7 +633,7 @@ export abstract class AbstractCursor< batchSize }); - executeOperation(this, getMoreOperation, callback); + executeOperation(this[kClient], getMoreOperation, callback); } /** @@ -642,13 +645,13 @@ export abstract class AbstractCursor< */ [kInit](callback: Callback): void { if (this[kSession] == null) { - if (this[kTopology].shouldCheckForSessionSupport()) { - return this[kTopology].selectServer(ReadPreference.primaryPreferred, {}, err => { + if (this[kClient].topology?.shouldCheckForSessionSupport()) { + return this[kClient].topology?.selectServer(ReadPreference.primaryPreferred, {}, err => { if (err) return callback(err); return this[kInit](callback); }); - } else if (this[kTopology].hasSessionSupport()) { - this[kSession] = this[kTopology].startSession({ owner: this, explicit: false }); + } else if (this[kClient].topology?.hasSessionSupport()) { + this[kSession] = this[kClient].topology?.startSession({ owner: this, explicit: false }); } } diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index 39d57f073f..350696ee4e 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -1,8 +1,8 @@ import type { Document } from '../bson'; import type { ExplainVerbosityLike } from '../explain'; +import type { MongoClient } from '../mongo_client'; import { AggregateOperation, AggregateOptions } from '../operations/aggregate'; import { executeOperation, ExecutionResult } from '../operations/execute_operation'; -import type { Topology } from '../sdam/topology'; import type { ClientSession } from '../sessions'; import type { Sort } from '../sort'; import type { Callback, MongoDBNamespace } from '../utils'; @@ -33,12 +33,12 @@ export class AggregationCursor extends AbstractCursor { /** @internal */ constructor( - topology: Topology, + client: MongoClient, namespace: MongoDBNamespace, pipeline: Document[] = [], options: AggregateOptions = {} ) { - super(topology, namespace, options); + super(client, namespace, options); this[kPipeline] = pipeline; this[kOptions] = options; @@ -51,7 +51,7 @@ export class AggregationCursor extends AbstractCursor { clone(): AggregationCursor { const clonedOptions = mergeOptions({}, this[kOptions]); delete clonedOptions.session; - return new AggregationCursor(this.topology, this.namespace, this[kPipeline], { + return new AggregationCursor(this.client, this.namespace, this[kPipeline], { ...clonedOptions }); } @@ -68,7 +68,7 @@ export class AggregationCursor extends AbstractCursor { session }); - executeOperation(this, aggregateOperation, (err, response) => { + executeOperation(this.client, aggregateOperation, (err, response) => { if (err || response == null) return callback(err); // TODO: NODE-2882 @@ -88,7 +88,7 @@ export class AggregationCursor extends AbstractCursor { if (verbosity == null) verbosity = true; return executeOperation( - this, + this.client, new AggregateOperation(this.namespace, this[kPipeline], { ...this[kOptions], // NOTE: order matters here, we may need to refine this ...this.cursorOptions, diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index ae483ca215..8034bfd7c1 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -1,12 +1,12 @@ import type { Document } from '../bson'; import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error'; import type { ExplainVerbosityLike } from '../explain'; +import type { MongoClient } from '../mongo_client'; import type { CollationOptions } from '../operations/command'; import { CountOperation, CountOptions } from '../operations/count'; import { executeOperation, ExecutionResult } from '../operations/execute_operation'; import { FindOperation, FindOptions } from '../operations/find'; import type { Hint } from '../operations/operation'; -import type { Topology } from '../sdam/topology'; import type { ClientSession } from '../sessions'; import { formatSort, Sort, SortDirection } from '../sort'; import { Callback, emitWarningOnce, mergeOptions, MongoDBNamespace } from '../utils'; @@ -40,12 +40,12 @@ export class FindCursor extends AbstractCursor { /** @internal */ constructor( - topology: Topology, + client: MongoClient, namespace: MongoDBNamespace, filter: Document | undefined, options: FindOptions = {} ) { - super(topology, namespace, options); + super(client, namespace, options); this[kFilter] = filter || {}; this[kBuiltOptions] = options; @@ -58,7 +58,7 @@ export class FindCursor extends AbstractCursor { clone(): FindCursor { const clonedOptions = mergeOptions({}, this[kBuiltOptions]); delete clonedOptions.session; - return new FindCursor(this.topology, this.namespace, this[kFilter], { + return new FindCursor(this.client, this.namespace, this[kFilter], { ...clonedOptions }); } @@ -75,7 +75,7 @@ export class FindCursor extends AbstractCursor { session }); - executeOperation(this, findOperation, (err, response) => { + executeOperation(this.client, findOperation, (err, response) => { if (err || response == null) return callback(err); // TODO: We only need this for legacy queries that do not support `limit`, maybe @@ -143,7 +143,7 @@ export class FindCursor extends AbstractCursor { options = options ?? {}; return executeOperation( - this, + this.client, new CountOperation(this.namespace, this[kFilter], { ...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this ...this.cursorOptions, @@ -165,7 +165,7 @@ export class FindCursor extends AbstractCursor { if (verbosity == null) verbosity = true; return executeOperation( - this, + this.client, new FindOperation(undefined, this.namespace, this[kFilter], { ...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this ...this.cursorOptions, diff --git a/src/db.ts b/src/db.ts index 257dfd2039..3f6a1f0f14 100644 --- a/src/db.ts +++ b/src/db.ts @@ -258,7 +258,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new CreateCollectionOperation(this, name, resolveOptions(this, options)) as TODO_NODE_3286, callback ) as TODO_NODE_3286; @@ -286,7 +286,11 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); // Intentionally, we do not inherit options from parent for this operation. - return executeOperation(this, new RunCommandOperation(this, command, options ?? {}), callback); + return executeOperation( + this.s.client, + new RunCommandOperation(this, command, options ?? {}), + callback + ); } /** @@ -310,7 +314,7 @@ export class Db { } return new AggregationCursor( - getTopology(this), + this.s.client, this.s.namespace, pipeline, resolveOptions(this, options) @@ -355,7 +359,7 @@ export class Db { ): Promise | void { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new DbStatsOperation(this, resolveOptions(this, options)), callback ); @@ -434,7 +438,7 @@ export class Db { options.new_collection = true; return executeOperation( - this, + this.s.client, new RenameOperation( this.collection(fromCollection) as TODO_NODE_3286, toCollection, @@ -463,7 +467,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new DropCollectionOperation(this, name, resolveOptions(this, options)), callback ); @@ -486,7 +490,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new DropDatabaseOperation(this, resolveOptions(this, options)), callback ); @@ -509,7 +513,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new CollectionsOperation(this, resolveOptions(this, options)), callback ); @@ -545,7 +549,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new CreateIndexOperation(this, name, indexSpec, resolveOptions(this, options)), callback ); @@ -591,7 +595,7 @@ export class Db { } return executeOperation( - this, + this.s.client, new AddUserOperation(this, username, password, resolveOptions(this, options)), callback ); @@ -616,7 +620,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new RemoveUserOperation(this, username, resolveOptions(this, options)), callback ); @@ -648,7 +652,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new SetProfilingLevelOperation(this, level, resolveOptions(this, options)), callback ); @@ -671,7 +675,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new ProfilingLevelOperation(this, resolveOptions(this, options)), callback ); @@ -700,7 +704,7 @@ export class Db { if (typeof options === 'function') (callback = options), (options = {}); return executeOperation( - this, + this.s.client, new IndexInformationOperation(this, name, resolveOptions(this, options)), callback ); diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 90edf2a1a4..2b04f01e63 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -268,11 +268,13 @@ export interface MongoClientPrivate { sessions: Set; bsonOptions: BSONSerializeOptions; namespace: MongoDBNamespace; - readonly options?: MongoOptions; + hasBeenClosed: boolean; + readonly options: MongoOptions; readonly readConcern?: ReadConcern; readonly writeConcern?: WriteConcern; readonly readPreference: ReadPreference; readonly logger: Logger; + readonly isMongoClient: true; } /** @public */ @@ -351,6 +353,7 @@ export class MongoClient extends TypedEventEmitter { sessions: new Set(), bsonOptions: resolveBSONOptions(this[kOptions]), namespace: ns('admin'), + hasBeenClosed: false, get options() { return client[kOptions]; @@ -366,6 +369,9 @@ export class MongoClient extends TypedEventEmitter { }, get logger() { return client[kOptions].logger; + }, + get isMongoClient(): true { + return true; } }; } @@ -446,6 +452,14 @@ export class MongoClient extends TypedEventEmitter { forceOrCallback?: boolean | Callback, callback?: Callback ): Promise | void { + // There's no way to set hasBeenClosed back to false + Object.defineProperty(this.s, 'hasBeenClosed', { + value: true, + enumerable: true, + configurable: false, + writable: false + }); + if (typeof forceOrCallback === 'function') { callback = forceOrCallback; } diff --git a/src/operations/connect.ts b/src/operations/connect.ts index f2685426df..a71e95867d 100644 --- a/src/operations/connect.ts +++ b/src/operations/connect.ts @@ -62,6 +62,7 @@ function createTopology( // Events can be emitted before initialization is complete so we have to // save the reference to the topology on the client ASAP if the event handlers need to access it mongoClient.topology = topology; + topology.client = mongoClient; topology.once(Topology.OPEN, () => mongoClient.emit('open', mongoClient)); diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index 22b69996c0..17674e5487 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -7,11 +7,13 @@ import { MongoError, MongoExpiredSessionError, MongoNetworkError, + MongoNotConnectedError, MongoRuntimeError, MongoServerError, MongoTransactionError, MongoUnexpectedServerResponseError } from '../error'; +import type { MongoClient } from '../mongo_client'; import { ReadPreference } from '../read_preference'; import type { Server } from '../sdam/server'; import { @@ -21,13 +23,7 @@ import { } from '../sdam/server_selection'; import type { Topology } from '../sdam/topology'; import type { ClientSession } from '../sessions'; -import { - Callback, - getTopology, - maybePromise, - supportsRetryableWrites, - TopologyProvider -} from '../utils'; +import { Callback, maybePromise, supportsRetryableWrites } from '../utils'; import { AbstractOperation, Aspect } from './operation'; const MMAPv1_RETRY_WRITES_ERROR_CODE = MONGODB_ERROR_CODES.IllegalOperation; @@ -66,45 +62,48 @@ export interface ExecutionResult { export function executeOperation< T extends AbstractOperation, TResult = ResultTypeFromOperation ->(topologyProvider: TopologyProvider, operation: T): Promise; +>(client: MongoClient, operation: T): Promise; export function executeOperation< T extends AbstractOperation, TResult = ResultTypeFromOperation ->(topologyProvider: TopologyProvider, operation: T, callback: Callback): void; +>(client: MongoClient, operation: T, callback: Callback): void; export function executeOperation< T extends AbstractOperation, TResult = ResultTypeFromOperation ->( - topologyProvider: TopologyProvider, - operation: T, - callback?: Callback -): Promise | void; +>(client: MongoClient, operation: T, callback?: Callback): Promise | void; export function executeOperation< T extends AbstractOperation, TResult = ResultTypeFromOperation ->( - topologyProvider: TopologyProvider, - operation: T, - callback?: Callback -): Promise | void { +>(client: MongoClient, operation: T, callback?: Callback): Promise | void { if (!(operation instanceof AbstractOperation)) { // TODO(NODE-3483): Extend MongoRuntimeError throw new MongoRuntimeError('This method requires a valid operation instance'); } return maybePromise(callback, callback => { - let topology: Topology; - try { - // TODO(NODE-4151): Use skipPingOnConnect and call connect here to make client.connect optional - topology = getTopology(topologyProvider); - } catch (error) { - return callback(error); + const topology = client.topology; + + if (topology == null) { + if (client.s.hasBeenClosed) { + return callback( + new MongoNotConnectedError('Client must be connected before running operations') + ); + } + client.s.options[Symbol.for('@@mdb.skipPingOnConnect')] = true; + return client.connect(error => { + delete client.s.options[Symbol.for('@@mdb.skipPingOnConnect')]; + if (error) { + return callback(error); + } + return executeOperation(client, operation, callback); + }); } + if (topology.shouldCheckForSessionSupport()) { return topology.selectServer(ReadPreference.primaryPreferred, {}, err => { if (err) return callback(err); - executeOperation(topologyProvider, operation, callback); + executeOperation(client, operation, callback); }); } diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index d7a27adfe0..af27bbcb13 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -7,13 +7,7 @@ import type { OneOrMore } from '../mongo_types'; import { ReadPreference } from '../read_preference'; import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; -import { - Callback, - getTopology, - maxWireVersion, - MongoDBNamespace, - parseIndexOptions -} from '../utils'; +import { Callback, maxWireVersion, MongoDBNamespace, parseIndexOptions } from '../utils'; import { CollationOptions, CommandOperation, @@ -424,7 +418,7 @@ export class ListIndexesCursor extends AbstractCursor { options?: ListIndexesOptions; constructor(collection: Collection, options?: ListIndexesOptions) { - super(getTopology(collection), collection.s.namespace, options); + super(collection.s.db.s.client, collection.s.namespace, options); this.parent = collection; this.options = options; } @@ -444,7 +438,7 @@ export class ListIndexesCursor extends AbstractCursor { session }); - executeOperation(this.parent, operation, (err, response) => { + executeOperation(this.parent.s.db.s.client, operation, (err, response) => { if (err || response == null) return callback(err); // TODO: NODE-2882 diff --git a/src/operations/list_collections.ts b/src/operations/list_collections.ts index ccc515baab..507b2c0b40 100644 --- a/src/operations/list_collections.ts +++ b/src/operations/list_collections.ts @@ -3,7 +3,7 @@ import { AbstractCursor } from '../cursor/abstract_cursor'; import type { Db } from '../db'; import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; -import { Callback, getTopology, maxWireVersion } from '../utils'; +import { Callback, maxWireVersion } from '../utils'; import { CommandOperation, CommandOperationOptions } from './command'; import { executeOperation, ExecutionResult } from './execute_operation'; import { Aspect, defineAspects } from './operation'; @@ -97,7 +97,7 @@ export class ListCollectionsCursor< options?: ListCollectionsOptions; constructor(db: Db, filter: Document, options?: ListCollectionsOptions) { - super(getTopology(db), db.s.namespace, options); + super(db.s.client, db.s.namespace, options); this.parent = db; this.filter = filter; this.options = options; @@ -118,7 +118,7 @@ export class ListCollectionsCursor< session }); - executeOperation(this.parent, operation, (err, response) => { + executeOperation(this.parent.s.client, operation, (err, response) => { if (err || response == null) return callback(err); // TODO: NODE-2882 diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index 45e0f7f9b8..edda138bd4 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -27,7 +27,7 @@ import { MongoServerSelectionError, MongoTopologyClosedError } from '../error'; -import type { MongoOptions, ServerApi } from '../mongo_client'; +import type { MongoClient, MongoOptions, ServerApi } from '../mongo_client'; import { TypedEventEmitter } from '../mongo_types'; import { ReadPreference, ReadPreferenceLike } from '../read_preference'; import { @@ -203,6 +203,8 @@ export class Topology extends TypedEventEmitter { /** @internal */ _type?: string; + client!: MongoClient; + /** @event */ static readonly SERVER_OPENING = SERVER_OPENING; /** @event */ @@ -626,7 +628,7 @@ export class Topology extends TypedEventEmitter { /** Start a logical session */ startSession(options: ClientSessionOptions, clientOptions?: MongoOptions): ClientSession { - const session = new ClientSession(this, this.s.sessionPool, options, clientOptions); + const session = new ClientSession(this.client, this.s.sessionPool, options, clientOptions); session.once('ended', () => { this.s.sessions.delete(session); }); diff --git a/src/sessions.ts b/src/sessions.ts index d2d6b5d003..36f2613231 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -19,7 +19,7 @@ import { MongoTransactionError, MongoWriteConcernError } from './error'; -import type { MongoOptions } from './mongo_client'; +import type { MongoClient, MongoOptions } from './mongo_client'; import { TypedEventEmitter } from './mongo_types'; import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; @@ -97,7 +97,7 @@ export interface EndSessionOptions { */ export class ClientSession extends TypedEventEmitter { /** @internal */ - topology: Topology; + client: MongoClient; /** @internal */ sessionPool: ServerSessionPool; hasEnded: boolean; @@ -124,22 +124,22 @@ export class ClientSession extends TypedEventEmitter { /** * Create a client session. * @internal - * @param topology - The current client's topology (Internal Class) + * @param client - The current client * @param sessionPool - The server session pool (Internal Class) * @param options - Optional settings * @param clientOptions - Optional settings provided when creating a MongoClient */ constructor( - topology: Topology, + client: MongoClient, sessionPool: ServerSessionPool, options: ClientSessionOptions, clientOptions?: MongoOptions ) { super(); - if (topology == null) { + if (client == null) { // TODO(NODE-3483) - throw new MongoRuntimeError('ClientSession requires a topology'); + throw new MongoRuntimeError('ClientSession requires a MongoClient'); } if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) { @@ -158,7 +158,7 @@ export class ClientSession extends TypedEventEmitter { } } - this.topology = topology; + this.client = client; this.sessionPool = sessionPool; this.hasEnded = false; this.clientOptions = clientOptions; @@ -205,7 +205,7 @@ export class ClientSession extends TypedEventEmitter { } get loadBalanced(): boolean { - return this.topology.description.type === TopologyType.LoadBalanced; + return this.client.topology?.description.type === TopologyType.LoadBalanced; } /** @internal */ @@ -394,9 +394,9 @@ export class ClientSession extends TypedEventEmitter { this.unpin(); } - const topologyMaxWireVersion = maxWireVersion(this.topology); + const topologyMaxWireVersion = maxWireVersion(this.client.topology); if ( - isSharded(this.topology) && + isSharded(this.client.topology) && topologyMaxWireVersion != null && topologyMaxWireVersion < minWireVersionForShardedTransactions ) { @@ -528,10 +528,11 @@ export function maybeClearPinnedConnection( return; } + const topology = session.client.topology; // NOTE: the spec talks about what to do on a network error only, but the tests seem to // to validate that we don't unpin on _all_ errors? - if (conn) { - const servers = Array.from(session.topology.s.servers.values()); + if (conn && topology != null) { + const servers = Array.from(topology.s.servers.values()); const loadBalancer = servers[0]; if (options?.error == null || options?.force) { @@ -770,7 +771,7 @@ function endTransaction( // send the command executeOperation( - session, + session.client, new RunAdminCommandOperation(undefined, command, { session, readPreference: ReadPreference.primary, @@ -794,7 +795,7 @@ function endTransaction( } return executeOperation( - session, + session.client, new RunAdminCommandOperation(undefined, command, { session, readPreference: ReadPreference.primary, diff --git a/src/utils.ts b/src/utils.ts index 6210e94dc7..9268b13562 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -348,7 +348,7 @@ export type TopologyProvider = */ export function getTopology(provider: TopologyProvider): Topology { // MongoClient or ClientSession or AbstractCursor - if (`topology` in provider && provider.topology) { + if ('topology' in provider && provider.topology) { return provider.topology; } else if ('s' in provider && 'client' in provider.s && provider.s.client.topology) { return provider.s.client.topology; diff --git a/test/integration/connection-monitoring-and-pooling/connection.test.js b/test/integration/connection-monitoring-and-pooling/connection.test.js index cfefbe987d..81653e06cf 100644 --- a/test/integration/connection-monitoring-and-pooling/connection.test.js +++ b/test/integration/connection-monitoring-and-pooling/connection.test.js @@ -1,10 +1,6 @@ 'use strict'; -const { - ServerHeartbeatStartedEvent, - MongoClient, - MongoNotConnectedError -} = require('../../../src'); +const { ServerHeartbeatStartedEvent, MongoClient } = require('../../../src'); const { Connection } = require('../../../src/cmap/connection'); const { connect } = require('../../../src/cmap/connect'); const { expect } = require('chai'); @@ -455,52 +451,5 @@ describe('Connection', function () { }); }) ); - - it('throws when attempting an operation if the client is not connected', function (done) { - const client = this.configuration.newClient(); - const collection = client.db('shouldCorrectlyFailOnRetry').collection('test'); - collection.insertOne({ a: 2 }, err => { - expect(err).to.be.instanceof(MongoNotConnectedError); - done(); - }); - }); - - it('throws when attempting an operation if the client is not connected (promises)', async function () { - const client = this.configuration.newClient(); - const collection = client.db('shouldCorrectlyFailOnRetry').collection('test'); - - const err = await collection.insertOne({ a: 2 }).catch(err => err); - expect(err).to.be.instanceof(MongoNotConnectedError); - }); - - it( - 'should correctly fail on retry when client has been closed', - withClient(function (client, done) { - const collection = client.db('shouldCorrectlyFailOnRetry').collection('test'); - collection.insertOne({ a: 1 }, (err, result) => { - expect(err).to.not.exist; - expect(result).to.exist; - - client.close(true, function (err) { - expect(err).to.not.exist; - - collection.insertOne({ a: 2 }, err => { - expect(err).to.be.instanceof(MongoNotConnectedError); - done(); - }); - }); - }); - }) - ); - - it('should correctly fail on retry when client has been closed (promises)', async function () { - const client = await this.configuration.newClient().connect(); - const collection = client.db('shouldCorrectlyFailOnRetry').collection('test'); - await collection.insertOne({ a: 1 }); - await client.close(true); - - const err = await collection.insertOne({ a: 2 }).catch(err => err); - expect(err).to.be.instanceof(MongoNotConnectedError); - }); }); }); diff --git a/test/integration/node-specific/mongo_client.test.js b/test/integration/node-specific/mongo_client.test.js deleted file mode 100644 index 5f9fd0d391..0000000000 --- a/test/integration/node-specific/mongo_client.test.js +++ /dev/null @@ -1,384 +0,0 @@ -'use strict'; -const { expect } = require('chai'); - -const sinon = require('sinon'); - -const { setupDatabase, assert: test } = require('../shared'); -const { format: f } = require('util'); - -const { MongoClient, ReadPreference } = require('../../../src'); -const { Db } = require('../../../src/db'); -const { Connection } = require('../../../src/cmap/connection'); -const { getTopology, isHello } = require('../../../src/utils'); - -describe('MongoClient integration', function () { - before(function () { - return setupDatabase(this.configuration); - }); - - it('Should correctly pass through extra db options', { - metadata: { - requires: { - topology: ['single'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient( - {}, - { - writeConcern: { w: 1, wtimeoutMS: 1000, fsync: true, j: true }, - readPreference: 'nearest', - readPreferenceTags: { loc: 'ny' }, - forceServerObjectId: true, - pkFactory: { - createPk() { - return 1; - } - }, - serializeFunctions: true - } - ); - - client.connect(function (err, client) { - expect(err).to.be.undefined; - - const db = client.db(configuration.db); - - test.equal(1, db.writeConcern.w); - test.equal(1000, db.writeConcern.wtimeout); - test.equal(true, db.writeConcern.fsync); - test.equal(true, db.writeConcern.j); - - test.equal('nearest', db.s.readPreference.mode); - test.deepEqual([{ loc: 'ny' }], db.s.readPreference.tags); - - test.equal(true, db.s.options.forceServerObjectId); - test.equal(1, db.s.pkFactory.createPk()); - test.equal(true, db.bsonOptions.serializeFunctions); - - client.close(done); - }); - } - }); - - it('Should fail due to wrong uri user:password@localhost', { - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded'] } - }, - test() { - expect(() => this.configuration.newClient('user:password@localhost:27017/test')).to.throw( - 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' - ); - } - }); - - it('correctly error out when no socket available on MongoClient `connect`', { - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded'] } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient('mongodb://localhost:27088/test', { - serverSelectionTimeoutMS: 10 - }); - - client.connect(function (err) { - test.ok(err != null); - - done(); - }); - } - }); - - it('should correctly connect to mongodb using domain socket', { - metadata: { requires: { topology: ['single'], os: '!win32' } }, - - test: function (done) { - var configuration = this.configuration; - const client = configuration.newClient('mongodb://%2Ftmp%2Fmongodb-27017.sock/test'); - client.connect(function (err) { - expect(err).to.not.exist; - client.close(done); - }); - } - }); - - it('should fail dure to garbage connection string', { - metadata: { - requires: { - topology: ['single'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient('mongodb://unknownhost:36363/ddddd', { - serverSelectionTimeoutMS: 10 - }); - - client.connect(function (err) { - test.ok(err != null); - done(); - }); - } - }); - - it('Should correctly pass through appname', { - metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const options = { - appName: 'hello world' - }; - const client = configuration.newClient(options); - - client.connect(function (err, client) { - expect(err).to.not.exist; - test.equal('hello world', client.topology.clientMetadata.application.name); - - client.close(done); - }); - } - }); - - it('Should correctly pass through appname in options', { - metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const url = configuration.url(); - - const client = configuration.newClient(url, { appname: 'hello world' }); - client.connect(err => { - expect(err).to.not.exist; - test.equal('hello world', client.topology.clientMetadata.application.name); - - client.close(done); - }); - } - }); - - it('Should correctly pass through socketTimeoutMS and connectTimeoutMS', { - metadata: { - requires: { - topology: ['single', 'replicaset', 'sharded'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient( - {}, - { - socketTimeoutMS: 0, - connectTimeoutMS: 0 - } - ); - - client.connect(function (err, client) { - expect(err).to.not.exist; - const topology = getTopology(client.db(configuration.db)); - expect(topology).nested.property('s.options.connectTimeoutMS').to.equal(0); - expect(topology).nested.property('s.options.socketTimeoutMS').to.equal(0); - - client.close(done); - }); - } - }); - - ////////////////////////////////////////////////////////////////////////////////////////// - // - // new MongoClient connection tests - // - ////////////////////////////////////////////////////////////////////////////////////////// - it('Should open a new MongoClient connection', { - metadata: { - requires: { - topology: ['single'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient(); - client.connect(function (err, mongoclient) { - expect(err).to.not.exist; - - mongoclient - .db('integration_tests') - .collection('new_mongo_client_collection') - .insertOne({ a: 1 }, function (err, r) { - expect(err).to.not.exist; - test.ok(r); - - mongoclient.close(done); - }); - }); - } - }); - - it('Should correctly connect with MongoClient `connect` using Promise', function () { - const configuration = this.configuration; - let url = configuration.url(); - url = - url.indexOf('?') !== -1 - ? f('%s&%s', url, 'maxPoolSize=100') - : f('%s?%s', url, 'maxPoolSize=100'); - - const client = configuration.newClient(url); - return client.connect().then(() => client.close()); - }); - - it('Should open a new MongoClient connection using promise', { - metadata: { - requires: { - topology: ['single'] - } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient(); - client.connect().then(function (mongoclient) { - mongoclient - .db('integration_tests') - .collection('new_mongo_client_collection') - .insertOne({ a: 1 }) - .then(function (r) { - test.ok(r); - - mongoclient.close(done); - }); - }); - } - }); - - it('should be able to access a database named "constructor"', function () { - const client = this.configuration.newClient(); - let err; - return client - .connect() - .then(() => { - const db = client.db('constructor'); - expect(db).to.not.be.a('function'); - expect(db).to.be.an.instanceOf(Db); - }) - .catch(_err => (err = _err)) - .then(() => client.close()) - .catch(() => {}) - .then(() => { - if (err) { - throw err; - } - }); - }); - - it('should cache a resolved readPreference from options', function () { - const client = this.configuration.newClient({}, { readPreference: ReadPreference.SECONDARY }); - expect(client.readPreference).to.be.instanceOf(ReadPreference); - expect(client.readPreference).to.have.property('mode', ReadPreference.SECONDARY); - }); - - it('should error on unexpected options', { - metadata: { requires: { topology: 'single' } }, - - test: function (done) { - var configuration = this.configuration; - MongoClient.connect( - configuration.url(), - { - maxPoolSize: 4, - notlegal: {}, - validateOptions: true - }, - function (err, client) { - expect(err) - .property('message') - .to.match(/options notlegal, validateoptions are not supported/); - expect(client).to.not.exist; - done(); - } - ); - } - }); - - it('should error on unexpected options (promise)', { - metadata: { requires: { topology: 'single' } }, - - test() { - MongoClient.connect(this.configuration.url(), { - maxPoolSize: 4, - notlegal: {}, - validateOptions: true - }) - .then(() => expect().fail()) - .catch(err => { - expect(err) - .property('message') - .to.match(/options notlegal, validateoptions are not supported/); - }); - } - }); - - it('must respect an infinite connectTimeoutMS for the streaming protocol', { - metadata: { requires: { topology: 'replicaset', mongodb: '>= 4.4' } }, - test: function (done) { - const client = this.configuration.newClient({ - connectTimeoutMS: 0, - heartbeatFrequencyMS: 500 - }); - client.connect(err => { - expect(err).to.not.exist; - const stub = sinon.stub(Connection.prototype, 'command').callsFake(function () { - const args = Array.prototype.slice.call(arguments); - const ns = args[0]; - const command = args[1]; - const options = args[2] || {}; - if (ns.toString() === 'admin.$cmd' && isHello(command) && options.exhaustAllowed) { - expect(options).property('socketTimeoutMS').to.equal(0); - stub.restore(); - client.close(done); - } - stub.wrappedMethod.apply(this, args); - }); - }); - } - }); - - it('must respect a finite connectTimeoutMS for the streaming protocol', { - metadata: { requires: { topology: 'replicaset', mongodb: '>= 4.4' } }, - test: function (done) { - const client = this.configuration.newClient({ - connectTimeoutMS: 10, - heartbeatFrequencyMS: 500 - }); - client.connect(err => { - expect(err).to.not.exist; - const stub = sinon.stub(Connection.prototype, 'command').callsFake(function () { - const args = Array.prototype.slice.call(arguments); - const ns = args[0]; - const command = args[1]; - const options = args[2] || {}; - if (ns.toString() === 'admin.$cmd' && isHello(command) && options.exhaustAllowed) { - expect(options).property('socketTimeoutMS').to.equal(510); - stub.restore(); - client.close(done); - } - stub.wrappedMethod.apply(this, args); - }); - }); - } - }); -}); diff --git a/test/integration/node-specific/mongo_client.test.ts b/test/integration/node-specific/mongo_client.test.ts new file mode 100644 index 0000000000..4a6659cbde --- /dev/null +++ b/test/integration/node-specific/mongo_client.test.ts @@ -0,0 +1,631 @@ +import { expect } from 'chai'; +import { once } from 'events'; +import * as sinon from 'sinon'; + +import { + MongoClient, + MongoNotConnectedError, + MongoServerSelectionError, + ReadPreference +} from '../../../src'; +import { Connection } from '../../../src/cmap/connection'; +import { Db } from '../../../src/db'; +import { Topology } from '../../../src/sdam/topology'; +import { getTopology, isHello } from '../../../src/utils'; +import { runLater } from '../../tools/utils'; +import { setupDatabase } from '../shared'; + +describe('class MongoClient', function () { + before(function () { + return setupDatabase(this.configuration); + }); + + it('should correctly pass through extra db options', { + metadata: { requires: { topology: ['single'] } }, + test: function (done) { + const configuration = this.configuration; + const client = configuration.newClient( + {}, + { + writeConcern: { w: 1, wtimeoutMS: 1000, fsync: true, j: true }, + readPreference: 'nearest', + readPreferenceTags: { loc: 'ny' }, + forceServerObjectId: true, + pkFactory: { + createPk() { + return 1; + } + }, + serializeFunctions: true + } + ); + + client.connect(function (err, client) { + expect(err).to.be.undefined; + + const db = client.db(configuration.db); + + expect(db).to.have.property('writeConcern'); + expect(db.writeConcern).to.have.property('w', 1); + expect(db.writeConcern).to.have.property('wtimeout', 1000); + expect(db.writeConcern).to.have.property('fsync', true); + expect(db.writeConcern).to.have.property('j', true); + + expect(db).to.have.property('s'); + expect(db.s).to.have.property('readPreference'); + expect(db.s.readPreference).to.have.property('mode', 'nearest'); + expect(db.s.readPreference) + .to.have.property('tags') + .that.deep.equals([{ loc: 'ny' }]); + + expect(db.s).to.have.nested.property('options.forceServerObjectId'); + expect(db.s.options).to.have.property('forceServerObjectId', true); + expect(db.s).to.have.nested.property('pkFactory.createPk').that.is.a('function'); + expect(db.s.pkFactory.createPk()).to.equal(1); + expect(db).to.have.nested.property('bsonOptions.serializeFunctions'); + + client.close(done); + }); + } + }); + + it('Should fail due to wrong uri user:password@localhost', { + metadata: { + requires: { topology: ['single', 'replicaset', 'sharded'] } + }, + test() { + expect(() => this.configuration.newClient('user:password@localhost:27017/test')).to.throw( + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' + ); + } + }); + + it('correctly error out when no socket available on MongoClient `connect`', { + metadata: { + requires: { topology: ['single', 'replicaset', 'sharded'] } + }, + + test: function (done) { + const configuration = this.configuration; + const client = configuration.newClient('mongodb://localhost:27088/test', { + serverSelectionTimeoutMS: 10 + }); + + client.connect(function (err) { + expect(err).to.exist; + + done(); + }); + } + }); + + it('should correctly connect to mongodb using domain socket', { + metadata: { requires: { topology: ['single'], os: '!win32' } }, + + test: function (done) { + const configuration = this.configuration; + const client = configuration.newClient('mongodb://%2Ftmp%2Fmongodb-27017.sock/test'); + client.connect(function (err) { + expect(err).to.not.exist; + client.close(done); + }); + } + }); + + it('should fail to connect due to unknown host in connection string', async function () { + const configuration = this.configuration; + const client = configuration.newClient('mongodb://iLoveJavascript:36363/ddddd', { + serverSelectionTimeoutMS: 10 + }); + + const error = await client.connect().catch(error => error); + expect(error).to.be.instanceOf(MongoServerSelectionError); + }); + + it('Should correctly pass through appname', { + metadata: { + requires: { + topology: ['single', 'replicaset', 'sharded'] + } + }, + + test: function (done) { + const configuration = this.configuration; + const options = { + appName: 'hello world' + }; + const client = configuration.newClient(options); + + client.connect(function (err, client) { + expect(err).to.not.exist; + expect(client) + .to.have.nested.property('topology.clientMetadata.application.name') + .to.equal('hello world'); + + client.close(done); + }); + } + }); + + it('Should correctly pass through appname in options', { + metadata: { + requires: { + topology: ['single', 'replicaset', 'sharded'] + } + }, + + test: function (done) { + const configuration = this.configuration; + const url = configuration.url(); + + const client = configuration.newClient(url, { appname: 'hello world' }); + client.connect(err => { + expect(err).to.not.exist; + expect(client) + .to.have.nested.property('topology.clientMetadata.application.name') + .to.equal('hello world'); + + client.close(done); + }); + } + }); + + it('Should correctly pass through socketTimeoutMS and connectTimeoutMS', { + metadata: { + requires: { + topology: ['single', 'replicaset', 'sharded'] + } + }, + + test: function (done) { + const configuration = this.configuration; + const client = configuration.newClient( + {}, + { + socketTimeoutMS: 0, + connectTimeoutMS: 0 + } + ); + + client.connect(function (err, client) { + expect(err).to.not.exist; + const topology = getTopology(client.db(configuration.db)); + expect(topology).nested.property('s.options.connectTimeoutMS').to.equal(0); + expect(topology).nested.property('s.options.socketTimeoutMS').to.equal(0); + + client.close(done); + }); + } + }); + + it('should open a new MongoClient connection', { + metadata: { + requires: { + topology: ['single'] + } + }, + + test: function (done) { + const configuration = this.configuration; + const client = configuration.newClient(); + client.connect(function (err, mongoclient) { + expect(err).to.not.exist; + + mongoclient + .db('integration_tests') + .collection('new_mongo_client_collection') + .insertOne({ a: 1 }, function (err, r) { + expect(err).to.not.exist; + expect(r).to.be.an('object'); + + mongoclient.close(done); + }); + }); + } + }); + + it('should correctly connect with MongoClient `connect` using Promise', function () { + const configuration = this.configuration; + let url = configuration.url(); + url = url.indexOf('?') !== -1 ? `${url}&maxPoolSize=100` : `${url}?maxPoolSize=100`; + + const client = configuration.newClient(url); + return client.connect().then(() => client.close()); + }); + + it('should open a new MongoClient connection using promise', { + metadata: { + requires: { + topology: ['single'] + } + }, + + test: function (done) { + const configuration = this.configuration; + const client = configuration.newClient(); + client.connect().then(function (mongoclient) { + mongoclient + .db('integration_tests') + .collection('new_mongo_client_collection') + .insertOne({ a: 1 }) + .then(function (r) { + expect(r).to.exist; + + mongoclient.close(done); + }); + }); + } + }); + + it('should be able to access a database named "constructor"', function () { + const client = this.configuration.newClient(); + let err; + return client + .connect() + .then(() => { + const db = client.db('constructor'); + expect(db).to.not.be.a('function'); + expect(db).to.be.an.instanceOf(Db); + }) + .catch(_err => (err = _err)) + .then(() => client.close()) + .catch(() => { + // ignore + }) + .then(() => { + if (err) { + throw err; + } + }); + }); + + it('should cache a resolved readPreference from options', function () { + const client = this.configuration.newClient({}, { readPreference: ReadPreference.SECONDARY }); + expect(client.readPreference).to.be.instanceOf(ReadPreference); + expect(client.readPreference).to.have.property('mode', ReadPreference.SECONDARY); + }); + + it('should error on unexpected options', { + metadata: { requires: { topology: 'single' } }, + + test: function (done) { + const configuration = this.configuration; + MongoClient.connect( + configuration.url(), + { + maxPoolSize: 4, + // @ts-expect-error: unexpected option test + notlegal: {}, + validateOptions: true + }, + function (err, client) { + expect(err) + .property('message') + .to.match(/options notlegal, validateoptions are not supported/); + expect(client).to.not.exist; + done(); + } + ); + } + }); + + it('should error on unexpected options (promise)', { + metadata: { requires: { topology: 'single' } }, + + test() { + const options = { + maxPoolSize: 4, + notlegal: {}, + validateOptions: true + }; + MongoClient.connect(this.configuration.url(), options) + .then(() => expect.fail()) + .catch(err => { + expect(err) + .property('message') + .to.match(/options notlegal, validateoptions are not supported/); + }); + } + }); + + it('must respect an infinite connectTimeoutMS for the streaming protocol', { + metadata: { requires: { topology: 'replicaset', mongodb: '>= 4.4' } }, + test: function (done) { + const client = this.configuration.newClient({ + connectTimeoutMS: 0, + heartbeatFrequencyMS: 500 + }); + client.connect(err => { + expect(err).to.not.exist; + const stub = sinon.stub(Connection.prototype, 'command').callsFake(function (...args) { + const ns = args[0]; + const command = args[1]; + const options = args[2] || {}; + + // @ts-expect-error: exhaustAllowed is a protocol option + if (ns.toString() === 'admin.$cmd' && isHello(command) && options.exhaustAllowed) { + expect(options).property('socketTimeoutMS').to.equal(0); + stub.restore(); + client.close(done); + } + stub.wrappedMethod.apply(this, args); + }); + }); + } + }); + + it('must respect a finite connectTimeoutMS for the streaming protocol', { + metadata: { requires: { topology: 'replicaset', mongodb: '>= 4.4' } }, + test: function (done) { + const client = this.configuration.newClient({ + connectTimeoutMS: 10, + heartbeatFrequencyMS: 500 + }); + client.connect(err => { + expect(err).to.not.exist; + const stub = sinon.stub(Connection.prototype, 'command').callsFake(function (...args) { + const ns = args[0]; + const command = args[1]; + const options = args[2] || {}; + + // @ts-expect-error: exhaustAllowed is a protocol option + if (ns.toString() === 'admin.$cmd' && isHello(command) && options.exhaustAllowed) { + expect(options).property('socketTimeoutMS').to.equal(510); + stub.restore(); + client.close(done); + } + stub.wrappedMethod.apply(this, args); + }); + }); + } + }); + + context('explict #connect()', () => { + let client: MongoClient; + beforeEach(function () { + client = this.configuration.newClient(this.configuration.url(), { + monitorCommands: true + }); + }); + + afterEach(async function () { + await client.close(); + }); + + it( + 'creates topology and send ping when auth is enabled', + { requires: { auth: 'enabled' } }, + async function () { + const commandToBeStarted = once(client, 'commandStarted'); + await client.connect(); + const [pingOnConnect] = await commandToBeStarted; + expect(pingOnConnect).to.have.property('commandName', 'ping'); + expect(client).to.have.property('topology').that.is.instanceOf(Topology); + } + ); + + it( + 'does not send ping when authentication is disabled', + { requires: { auth: 'disabled' } }, + async function () { + const commandToBeStarted = once(client, 'commandStarted'); + await client.connect(); + const delayedFind = runLater(async () => { + await client.db().collection('test').findOne(); + }, 300); + const [findOneOperation] = await commandToBeStarted; + // Proves that the first command started event that is emitted is a find and not a ping + expect(findOneOperation).to.have.property('commandName', 'find'); + await delayedFind; + expect(client).to.have.property('topology').that.is.instanceOf(Topology); + } + ); + + it( + 'permits operations to be run after connect is called', + { requires: { auth: 'enabled' } }, + async function () { + const pingCommandToBeStarted = once(client, 'commandStarted'); + await client.connect(); + const [pingOnConnect] = await pingCommandToBeStarted; + + const findCommandToBeStarted = once(client, 'commandStarted'); + await client.db('test').collection('test').findOne(); + const [findCommandStarted] = await findCommandToBeStarted; + + expect(pingOnConnect).to.have.property('commandName', 'ping'); + expect(findCommandStarted).to.have.property('commandName', 'find'); + expect(client).to.have.property('topology').that.is.instanceOf(Topology); + } + ); + }); + + context('implicit #connect()', () => { + let client: MongoClient; + beforeEach(function () { + client = this.configuration.newClient(this.configuration.url(), { + monitorCommands: true + }); + }); + + afterEach(async function () { + await client.close(); + }); + + it( + 'automatically connects upon first operation (find)', + { requires: { auth: 'enabled' } }, + async function () { + const findCommandToBeStarted = once(client, 'commandStarted'); + await client.db().collection('test').findOne(); + const [findCommandStarted] = await findCommandToBeStarted; + + expect(findCommandStarted).to.have.property('commandName', 'find'); + expect(client.options).to.not.have.property(Symbol.for('@@mdb.skipPingOnConnect')); + expect(client.s.options).to.not.have.property(Symbol.for('@@mdb.skipPingOnConnect')); + + // Assertion is redundant but it shows that no initial ping is run + expect(findCommandStarted.commandName).to.not.equal('ping'); + + expect(client).to.have.property('topology').that.is.instanceOf(Topology); + } + ); + + it( + 'automatically connects upon first operation (insertOne)', + { requires: { auth: 'enabled' } }, + async function () { + const insertOneCommandToBeStarted = once(client, 'commandStarted'); + await client.db().collection('test').insertOne({ a: 1 }); + const [insertCommandStarted] = await insertOneCommandToBeStarted; + + expect(insertCommandStarted).to.have.property('commandName', 'insert'); + expect(client.options).to.not.have.property(Symbol.for('@@mdb.skipPingOnConnect')); + expect(client.s.options).to.not.have.property(Symbol.for('@@mdb.skipPingOnConnect')); + + // Assertion is redundant but it shows that no initial ping is run + expect(insertCommandStarted.commandName).to.not.equal('ping'); + + expect(client).to.have.property('topology').that.is.instanceOf(Topology); + } + ); + + it( + 'passes connection errors to the user through the first operation', + { requires: { auth: 'enabled' } }, + async function () { + const client = this.configuration.newClient( + 'mongodb://iLoveJavascript?serverSelectionTimeoutMS=100', + { monitorCommands: true } + ); + + const result = await client + .db('test') + .collection('test') + .findOne() + .catch(error => error); + + expect(result).to.be.instanceOf(MongoServerSelectionError); + expect(client).to.be.instanceOf(MongoClient); + expect(client).to.have.property('topology').that.is.instanceOf(Topology); + } + ); + }); + + context('#close()', () => { + let client: MongoClient; + let db: Db; + + const RD_ONLY_HAS_BEEN_CLOSED = { + value: true, + enumerable: true, + configurable: false, + writable: false + }; + + const INIT_HAS_BEEN_CLOSED = { + value: false, + enumerable: true, + configurable: true, + writable: true + }; + + beforeEach(function () { + client = this.configuration.newClient(); + db = client.db(); + }); + + afterEach(async function () { + await client.close(); + db = null; + }); + + it('prevents automatic connection on a closed non-connected client', async () => { + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + await client.close(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + const error = await db.command({ ping: 1 }).catch(error => error); + expect(error).to.be.instanceOf(MongoNotConnectedError); + }); + + it('allows explicit connection on a closed non-connected client', async () => { + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + await client.close(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + await client.connect(); + const result = await db.command({ ping: 1 }).catch(error => error); + expect(result).to.not.be.instanceOf(MongoNotConnectedError); + expect(result).to.have.property('ok', 1); + }); + + it('prevents automatic reconnect on a closed previously explicitly connected client', async () => { + await client.connect(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + await client.close(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + const error = await db.command({ ping: 1 }).catch(error => error); + expect(error).to.be.instanceOf(MongoNotConnectedError); + }); + + it('allows explicit reconnect on a closed previously explicitly connected client', async () => { + await client.connect(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + await client.close(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + await client.connect(); + const result = await db.command({ ping: 1 }).catch(error => error); + expect(result).to.not.be.instanceOf(MongoNotConnectedError); + expect(result).to.have.property('ok', 1); + }); + + it('prevents auto reconnect on closed previously implicitly connected client', async () => { + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + const result = await db.command({ ping: 1 }).catch(error => error); // auto connect + expect(result).to.not.be.instanceOf(MongoNotConnectedError); + expect(result).to.have.property('ok', 1); + await client.close(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + const error = await db.command({ ping: 1 }).catch(error => error); + expect(error).to.be.instanceOf(MongoNotConnectedError); + }); + + it('allows explicit reconnect on closed previously implicitly connected client', async () => { + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + const result = await db.command({ ping: 1 }).catch(error => error); // auto connect + expect(result).to.not.be.instanceOf(MongoNotConnectedError); + expect(result).to.have.property('ok', 1); + await client.close(); + await client.connect(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + const result2 = await db.command({ ping: 1 }).catch(error => error); + expect(result2).to.not.be.instanceOf(MongoNotConnectedError); + expect(result2).to.have.property('ok', 1); + }); + + it('prevents auto reconnect on closed explicitly connected client after an operation', async () => { + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + await client.connect(); + const result = await db.command({ ping: 1 }).catch(error => error); + expect(result).to.not.be.instanceOf(MongoNotConnectedError); + expect(result).to.have.property('ok', 1); + await client.close(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + const error = await db.command({ ping: 1 }).catch(error => error); + expect(error).to.be.instanceOf(MongoNotConnectedError); + }); + + it('allows explicit reconnect on closed explicitly connected client after an operation', async () => { + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', INIT_HAS_BEEN_CLOSED); + await client.connect(); + const result = await db.command({ ping: 1 }).catch(error => error); + expect(result).to.not.be.instanceOf(MongoNotConnectedError); + expect(result).to.have.property('ok', 1); + await client.close(); + await client.connect(); + expect(client.s).to.have.ownPropertyDescriptor('hasBeenClosed', RD_ONLY_HAS_BEEN_CLOSED); + const result2 = await db.command({ ping: 1 }).catch(error => error); + expect(result2).to.not.be.instanceOf(MongoNotConnectedError); + expect(result2).to.have.property('ok', 1); + }); + }); +}); diff --git a/test/unit/cursor/aggregation_cursor.test.js b/test/unit/cursor/aggregation_cursor.test.js index 65a654dc09..226b7963c6 100644 --- a/test/unit/cursor/aggregation_cursor.test.js +++ b/test/unit/cursor/aggregation_cursor.test.js @@ -2,10 +2,11 @@ const { expect } = require('chai'); const mock = require('../../tools/mongodb-mock/index'); -const { Topology } = require('../../../src/sdam/topology'); const { Long } = require('bson'); const { MongoDBNamespace, isHello } = require('../../../src/utils'); const { AggregationCursor } = require('../../../src/cursor/aggregation_cursor'); +const { MongoClient } = require('../../../src/mongo_client'); +const { default: ConnectionString } = require('mongodb-connection-string-url'); const test = {}; describe('Aggregation Cursor', function () { @@ -37,17 +38,19 @@ describe('Aggregation Cursor', function () { }); it('sets the session on the cursor', function (done) { - const topology = new Topology(test.server.hostAddress()); + const client = new MongoClient( + new ConnectionString(`mongodb://${test.server.hostAddress()}`).toString() + ); const cursor = new AggregationCursor( - topology, + client, MongoDBNamespace.fromString('test.test'), [], {} ); - topology.connect(function () { + client.connect(function () { cursor.next(function () { expect(cursor.session).to.exist; - topology.close(done); + client.close(done); }); }); }); @@ -73,19 +76,22 @@ describe('Aggregation Cursor', function () { }); it('does not set the session on the cursor', function (done) { - const topology = new Topology(test.server.hostAddress(), { - serverSelectionTimeoutMS: 1000 - }); + const client = new MongoClient( + new ConnectionString(`mongodb://${test.server.hostAddress()}`).toString(), + { + serverSelectionTimeoutMS: 1000 + } + ); const cursor = new AggregationCursor( - topology, + client, MongoDBNamespace.fromString('test.test'), [], {} ); - topology.connect(function () { + client.connect(function () { cursor.next(function () { expect(cursor.session).to.not.exist; - topology.close(done); + client.close(done); }); }); }); @@ -117,19 +123,22 @@ describe('Aggregation Cursor', function () { }); it('sets the session on the cursor', function (done) { - const topology = new Topology(test.server.hostAddress(), { - serverSelectionTimeoutMS: 1000 - }); + const client = new MongoClient( + new ConnectionString(`mongodb://${test.server.hostAddress()}`).toString(), + { + serverSelectionTimeoutMS: 1000 + } + ); const cursor = new AggregationCursor( - topology, + client, MongoDBNamespace.fromString('test.test'), [], {} ); - topology.connect(function () { + client.connect(function () { cursor.next(function () { expect(cursor.session).to.exist; - topology.close(done); + client.close(done); }); }); }); diff --git a/test/unit/cursor/find_cursor.test.js b/test/unit/cursor/find_cursor.test.js index 4e4bb8d54b..bd8515424f 100644 --- a/test/unit/cursor/find_cursor.test.js +++ b/test/unit/cursor/find_cursor.test.js @@ -1,12 +1,12 @@ 'use strict'; -const expect = require('chai').expect; -const { MongoError } = require('../../../src/error'); +const { expect } = require('chai'); const mock = require('../../tools/mongodb-mock/index'); -const { Topology } = require('../../../src/sdam/topology'); const { Long } = require('bson'); const { MongoDBNamespace, isHello } = require('../../../src/utils'); const { FindCursor } = require('../../../src/cursor/find_cursor'); +const { MongoClient, MongoServerError } = require('../../../src'); +const { default: ConnectionString } = require('mongodb-connection-string-url'); const test = {}; describe('Find Cursor', function () { @@ -38,12 +38,17 @@ describe('Find Cursor', function () { }); it('sets the session on the cursor', function (done) { - const topology = new Topology(test.server.hostAddress()); - const cursor = new FindCursor(topology, MongoDBNamespace.fromString('test.test'), {}, {}); - topology.connect(function () { + const client = new MongoClient( + new ConnectionString(`mongodb://${test.server.hostAddress()}`).toString(), + { + serverSelectionTimeoutMS: 1000 + } + ); + const cursor = new FindCursor(client, MongoDBNamespace.fromString('test.test'), {}, {}); + client.connect(function () { cursor.next(function () { expect(cursor.session).to.exist; - topology.close(done); + client.close(done); }); }); }); @@ -69,14 +74,17 @@ describe('Find Cursor', function () { }); it('does not set the session on the cursor', function (done) { - const topology = new Topology(test.server.hostAddress(), { - serverSelectionTimeoutMS: 1000 - }); - const cursor = new FindCursor(topology, MongoDBNamespace.fromString('test.test'), {}, {}); - topology.connect(function () { + const client = new MongoClient( + new ConnectionString(`mongodb://${test.server.hostAddress()}`).toString(), + { + serverSelectionTimeoutMS: 1000 + } + ); + const cursor = new FindCursor(client, MongoDBNamespace.fromString('test.test'), {}, {}); + client.connect(function () { cursor.next(function () { expect(cursor.session).to.not.exist; - topology.close(done); + client.close(done); }); }); }); @@ -108,14 +116,17 @@ describe('Find Cursor', function () { }); it('sets the session on the cursor', function (done) { - const topology = new Topology(test.server.hostAddress(), { - serverSelectionTimeoutMS: 1000 - }); - const cursor = new FindCursor(topology, MongoDBNamespace.fromString('test.test'), {}, {}); - topology.connect(function () { + const client = new MongoClient( + new ConnectionString(`mongodb://${test.server.hostAddress()}`).toString(), + { + serverSelectionTimeoutMS: 1000 + } + ); + const cursor = new FindCursor(client, MongoDBNamespace.fromString('test.test'), {}, {}); + client.connect(function () { cursor.next(function () { expect(cursor.session).to.exist; - topology.close(done); + client.close(done); }); }); }); @@ -135,7 +146,10 @@ describe('Find Cursor', function () { errmsg: 'Cursor not found (namespace: "liveearth.entityEvents", id: 2018648316188432590).' }; - const client = new Topology(test.server.hostAddress()); + const client = new MongoClient( + new ConnectionString(`mongodb://${test.server.hostAddress()}`).toString(), + { serverSelectionTimeoutMS: 1000 } + ); test.server.setMessageHandler(request => { const doc = request.document; @@ -162,19 +176,18 @@ describe('Find Cursor', function () { }); client.on('error', done); - client.once('connect', () => { + client.connect(() => { const cursor = new FindCursor(client, MongoDBNamespace.fromString('test.test'), {}, {}); // Execute next cursor.next(function (err) { expect(err).to.exist; - expect(err).to.be.instanceof(MongoError); + expect(err).to.be.instanceof(MongoServerError); expect(err.message).to.equal(errdoc.errmsg); client.close(done); }); }); - client.connect(); }); }); }); diff --git a/test/unit/sessions.test.js b/test/unit/sessions.test.js index 84de683ab2..0ac8bf8bee 100644 --- a/test/unit/sessions.test.js +++ b/test/unit/sessions.test.js @@ -163,7 +163,7 @@ describe('Sessions - unit', function () { it('should throw errors with invalid parameters', function () { expect(() => { new ClientSession(); - }).to.throw(/ClientSession requires a topology/); + }).to.throw(/ClientSession requires a MongoClient/); expect(() => { new ClientSession({});