something is working
This commit is contained in:
+314
@@ -0,0 +1,314 @@
|
||||
import type { Document } from './bson';
|
||||
import type { Db } from './db';
|
||||
import { AddUserOperation, AddUserOptions } from './operations/add_user';
|
||||
import type { CommandOperationOptions } from './operations/command';
|
||||
import { executeOperation } from './operations/execute_operation';
|
||||
import {
|
||||
ListDatabasesOperation,
|
||||
ListDatabasesOptions,
|
||||
ListDatabasesResult
|
||||
} from './operations/list_databases';
|
||||
import { RemoveUserOperation, RemoveUserOptions } from './operations/remove_user';
|
||||
import { RunCommandOperation, RunCommandOptions } from './operations/run_command';
|
||||
import {
|
||||
ValidateCollectionOperation,
|
||||
ValidateCollectionOptions
|
||||
} from './operations/validate_collection';
|
||||
import type { Callback } from './utils';
|
||||
|
||||
/** @internal */
|
||||
export interface AdminPrivate {
|
||||
db: Db;
|
||||
}
|
||||
|
||||
/**
|
||||
* The **Admin** class is an internal class that allows convenient access to
|
||||
* the admin functionality and commands for MongoDB.
|
||||
*
|
||||
* **ADMIN Cannot directly be instantiated**
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const MongoClient = require('mongodb').MongoClient;
|
||||
* const test = require('assert');
|
||||
* // Connection url
|
||||
* const url = 'mongodb://localhost:27017';
|
||||
* // Database Name
|
||||
* const dbName = 'test';
|
||||
*
|
||||
* // Connect using MongoClient
|
||||
* MongoClient.connect(url, function(err, client) {
|
||||
* // Use the admin database for the operation
|
||||
* const adminDb = client.db(dbName).admin();
|
||||
*
|
||||
* // List all the available databases
|
||||
* adminDb.listDatabases(function(err, dbs) {
|
||||
* expect(err).to.not.exist;
|
||||
* test.ok(dbs.databases.length > 0);
|
||||
* client.close();
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class Admin {
|
||||
/** @internal */
|
||||
s: AdminPrivate;
|
||||
|
||||
/**
|
||||
* Create a new Admin instance
|
||||
* @internal
|
||||
*/
|
||||
constructor(db: Db) {
|
||||
this.s = { db };
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command
|
||||
*
|
||||
* @param command - The command to execute
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
command(command: Document): Promise<Document>;
|
||||
command(command: Document, callback: Callback<Document>): void;
|
||||
command(command: Document, options: RunCommandOptions): Promise<Document>;
|
||||
command(command: Document, options: RunCommandOptions, callback: Callback<Document>): void;
|
||||
command(
|
||||
command: Document,
|
||||
options?: RunCommandOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = Object.assign({ dbName: 'admin' }, options);
|
||||
|
||||
return executeOperation(
|
||||
this.s.db.s.client,
|
||||
new RunCommandOperation(this.s.db, command, options),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the server build information
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
buildInfo(): Promise<Document>;
|
||||
buildInfo(callback: Callback<Document>): void;
|
||||
buildInfo(options: CommandOperationOptions): Promise<Document>;
|
||||
buildInfo(options: CommandOperationOptions, callback: Callback<Document>): void;
|
||||
buildInfo(
|
||||
options?: CommandOperationOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
return this.command({ buildinfo: 1 }, options, callback as Callback<Document>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the server build information
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
serverInfo(): Promise<Document>;
|
||||
serverInfo(callback: Callback<Document>): void;
|
||||
serverInfo(options: CommandOperationOptions): Promise<Document>;
|
||||
serverInfo(options: CommandOperationOptions, callback: Callback<Document>): void;
|
||||
serverInfo(
|
||||
options?: CommandOperationOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
return this.command({ buildinfo: 1 }, options, callback as Callback<Document>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve this db's server status.
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
serverStatus(): Promise<Document>;
|
||||
serverStatus(callback: Callback<Document>): void;
|
||||
serverStatus(options: CommandOperationOptions): Promise<Document>;
|
||||
serverStatus(options: CommandOperationOptions, callback: Callback<Document>): void;
|
||||
serverStatus(
|
||||
options?: CommandOperationOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
return this.command({ serverStatus: 1 }, options, callback as Callback<Document>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the MongoDB server and retrieve results
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
ping(): Promise<Document>;
|
||||
ping(callback: Callback<Document>): void;
|
||||
ping(options: CommandOperationOptions): Promise<Document>;
|
||||
ping(options: CommandOperationOptions, callback: Callback<Document>): void;
|
||||
ping(
|
||||
options?: CommandOperationOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
return this.command({ ping: 1 }, options, callback as Callback<Document>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to the database
|
||||
*
|
||||
* @param username - The username for the new user
|
||||
* @param password - An optional password for the new user
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
addUser(username: string): Promise<Document>;
|
||||
addUser(username: string, callback: Callback<Document>): void;
|
||||
addUser(username: string, password: string): Promise<Document>;
|
||||
addUser(username: string, password: string, callback: Callback<Document>): void;
|
||||
addUser(username: string, options: AddUserOptions): Promise<Document>;
|
||||
addUser(username: string, options: AddUserOptions, callback: Callback<Document>): void;
|
||||
addUser(username: string, password: string, options: AddUserOptions): Promise<Document>;
|
||||
addUser(
|
||||
username: string,
|
||||
password: string,
|
||||
options: AddUserOptions,
|
||||
callback: Callback<Document>
|
||||
): void;
|
||||
addUser(
|
||||
username: string,
|
||||
password?: string | AddUserOptions | Callback<Document>,
|
||||
options?: AddUserOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof password === 'function') {
|
||||
(callback = password), (password = undefined), (options = {});
|
||||
} else if (typeof password !== 'string') {
|
||||
if (typeof options === 'function') {
|
||||
(callback = options), (options = password), (password = undefined);
|
||||
} else {
|
||||
(options = password), (callback = undefined), (password = undefined);
|
||||
}
|
||||
} else {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
}
|
||||
|
||||
options = Object.assign({ dbName: 'admin' }, options);
|
||||
|
||||
return executeOperation(
|
||||
this.s.db.s.client,
|
||||
new AddUserOperation(this.s.db, username, password, options),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from a database
|
||||
*
|
||||
* @param username - The username to remove
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
removeUser(username: string): Promise<boolean>;
|
||||
removeUser(username: string, callback: Callback<boolean>): void;
|
||||
removeUser(username: string, options: RemoveUserOptions): Promise<boolean>;
|
||||
removeUser(username: string, options: RemoveUserOptions, callback: Callback<boolean>): void;
|
||||
removeUser(
|
||||
username: string,
|
||||
options?: RemoveUserOptions | Callback<boolean>,
|
||||
callback?: Callback<boolean>
|
||||
): Promise<boolean> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = Object.assign({ dbName: 'admin' }, options);
|
||||
|
||||
return executeOperation(
|
||||
this.s.db.s.client,
|
||||
new RemoveUserOperation(this.s.db, username, options),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an existing collection
|
||||
*
|
||||
* @param collectionName - The name of the collection to validate.
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
validateCollection(collectionName: string): Promise<Document>;
|
||||
validateCollection(collectionName: string, callback: Callback<Document>): void;
|
||||
validateCollection(collectionName: string, options: ValidateCollectionOptions): Promise<Document>;
|
||||
validateCollection(
|
||||
collectionName: string,
|
||||
options: ValidateCollectionOptions,
|
||||
callback: Callback<Document>
|
||||
): void;
|
||||
validateCollection(
|
||||
collectionName: string,
|
||||
options?: ValidateCollectionOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
|
||||
return executeOperation(
|
||||
this.s.db.s.client,
|
||||
new ValidateCollectionOperation(this, collectionName, options),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List the available databases
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
listDatabases(): Promise<ListDatabasesResult>;
|
||||
listDatabases(callback: Callback<ListDatabasesResult>): void;
|
||||
listDatabases(options: ListDatabasesOptions): Promise<ListDatabasesResult>;
|
||||
listDatabases(options: ListDatabasesOptions, callback: Callback<ListDatabasesResult>): void;
|
||||
listDatabases(
|
||||
options?: ListDatabasesOptions | Callback<ListDatabasesResult>,
|
||||
callback?: Callback<ListDatabasesResult>
|
||||
): Promise<ListDatabasesResult> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
|
||||
return executeOperation(
|
||||
this.s.db.s.client,
|
||||
new ListDatabasesOperation(this.s.db, options),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ReplicaSet status
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
replSetGetStatus(): Promise<Document>;
|
||||
replSetGetStatus(callback: Callback<Document>): void;
|
||||
replSetGetStatus(options: CommandOperationOptions): Promise<Document>;
|
||||
replSetGetStatus(options: CommandOperationOptions, callback: Callback<Document>): void;
|
||||
replSetGetStatus(
|
||||
options?: CommandOperationOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
return this.command({ replSetGetStatus: 1 }, options, callback as Callback<Document>);
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import type {
|
||||
calculateObjectSize as calculateObjectSizeFn,
|
||||
deserialize as deserializeFn,
|
||||
DeserializeOptions,
|
||||
serialize as serializeFn,
|
||||
SerializeOptions
|
||||
} from 'bson';
|
||||
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
let BSON = require('bson');
|
||||
|
||||
try {
|
||||
// Ensure you always wrap an optional require in the try block NODE-3199
|
||||
BSON = require('bson-ext');
|
||||
} catch {} // eslint-disable-line
|
||||
|
||||
/** @internal */
|
||||
export const deserialize = BSON.deserialize as typeof deserializeFn;
|
||||
/** @internal */
|
||||
export const serialize = BSON.serialize as typeof serializeFn;
|
||||
/** @internal */
|
||||
export const calculateObjectSize = BSON.calculateObjectSize as typeof calculateObjectSizeFn;
|
||||
|
||||
export {
|
||||
Binary,
|
||||
BSONRegExp,
|
||||
BSONSymbol,
|
||||
Code,
|
||||
DBRef,
|
||||
Decimal128,
|
||||
Document,
|
||||
Double,
|
||||
Int32,
|
||||
Long,
|
||||
Map,
|
||||
MaxKey,
|
||||
MinKey,
|
||||
ObjectId,
|
||||
Timestamp
|
||||
} from 'bson';
|
||||
|
||||
/** @internal */
|
||||
export { BSON };
|
||||
|
||||
/**
|
||||
* BSON Serialization options.
|
||||
* @public
|
||||
*/
|
||||
export interface BSONSerializeOptions
|
||||
extends Omit<SerializeOptions, 'index'>,
|
||||
Omit<
|
||||
DeserializeOptions,
|
||||
| 'evalFunctions'
|
||||
| 'cacheFunctions'
|
||||
| 'cacheFunctionsCrc32'
|
||||
| 'allowObjectSmallerThanBufferSize'
|
||||
| 'index'
|
||||
| 'validation'
|
||||
> {
|
||||
/** Return BSON filled buffers from operations */
|
||||
raw?: boolean;
|
||||
|
||||
/** Enable utf8 validation when deserializing BSON documents. Defaults to true. */
|
||||
enableUtf8Validation?: boolean;
|
||||
}
|
||||
|
||||
export function pluckBSONSerializeOptions(options: BSONSerializeOptions): BSONSerializeOptions {
|
||||
const {
|
||||
fieldsAsRaw,
|
||||
promoteValues,
|
||||
promoteBuffers,
|
||||
promoteLongs,
|
||||
serializeFunctions,
|
||||
ignoreUndefined,
|
||||
bsonRegExp,
|
||||
raw,
|
||||
enableUtf8Validation
|
||||
} = options;
|
||||
return {
|
||||
fieldsAsRaw,
|
||||
promoteValues,
|
||||
promoteBuffers,
|
||||
promoteLongs,
|
||||
serializeFunctions,
|
||||
ignoreUndefined,
|
||||
bsonRegExp,
|
||||
raw,
|
||||
enableUtf8Validation
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given BSONSerializeOptions, preferring options over the parent's options, and
|
||||
* substituting defaults for values not set.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function resolveBSONOptions(
|
||||
options?: BSONSerializeOptions,
|
||||
parent?: { bsonOptions?: BSONSerializeOptions }
|
||||
): BSONSerializeOptions {
|
||||
const parentOptions = parent?.bsonOptions;
|
||||
return {
|
||||
raw: options?.raw ?? parentOptions?.raw ?? false,
|
||||
promoteLongs: options?.promoteLongs ?? parentOptions?.promoteLongs ?? true,
|
||||
promoteValues: options?.promoteValues ?? parentOptions?.promoteValues ?? true,
|
||||
promoteBuffers: options?.promoteBuffers ?? parentOptions?.promoteBuffers ?? false,
|
||||
ignoreUndefined: options?.ignoreUndefined ?? parentOptions?.ignoreUndefined ?? false,
|
||||
bsonRegExp: options?.bsonRegExp ?? parentOptions?.bsonRegExp ?? false,
|
||||
serializeFunctions: options?.serializeFunctions ?? parentOptions?.serializeFunctions ?? false,
|
||||
fieldsAsRaw: options?.fieldsAsRaw ?? parentOptions?.fieldsAsRaw ?? {},
|
||||
enableUtf8Validation:
|
||||
options?.enableUtf8Validation ?? parentOptions?.enableUtf8Validation ?? true
|
||||
};
|
||||
}
|
||||
+1384
File diff suppressed because it is too large
Load Diff
+82
@@ -0,0 +1,82 @@
|
||||
import type { Document } from '../bson';
|
||||
import * as BSON from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoInvalidArgumentError } from '../error';
|
||||
import type { DeleteStatement } from '../operations/delete';
|
||||
import type { UpdateStatement } from '../operations/update';
|
||||
import { Batch, BatchType, BulkOperationBase, BulkWriteOptions } from './common';
|
||||
|
||||
/** @public */
|
||||
export class OrderedBulkOperation extends BulkOperationBase {
|
||||
constructor(collection: Collection, options: BulkWriteOptions) {
|
||||
super(collection, options, true);
|
||||
}
|
||||
|
||||
addToOperationsList(
|
||||
batchType: BatchType,
|
||||
document: Document | UpdateStatement | DeleteStatement
|
||||
): this {
|
||||
// Get the bsonSize
|
||||
const bsonSize = BSON.calculateObjectSize(document, {
|
||||
checkKeys: false,
|
||||
// Since we don't know what the user selected for BSON options here,
|
||||
// err on the safe side, and check the size with ignoreUndefined: false.
|
||||
ignoreUndefined: false
|
||||
} as any);
|
||||
|
||||
// Throw error if the doc is bigger than the max BSON size
|
||||
if (bsonSize >= this.s.maxBsonObjectSize)
|
||||
// TODO(NODE-3483): Change this to MongoBSONError
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Document is larger than the maximum size ${this.s.maxBsonObjectSize}`
|
||||
);
|
||||
|
||||
// Create a new batch object if we don't have a current one
|
||||
if (this.s.currentBatch == null) {
|
||||
this.s.currentBatch = new Batch(batchType, this.s.currentIndex);
|
||||
}
|
||||
|
||||
const maxKeySize = this.s.maxKeySize;
|
||||
|
||||
// Check if we need to create a new batch
|
||||
if (
|
||||
// New batch if we exceed the max batch op size
|
||||
this.s.currentBatchSize + 1 >= this.s.maxWriteBatchSize ||
|
||||
// New batch if we exceed the maxBatchSizeBytes. Only matters if batch already has a doc,
|
||||
// since we can't sent an empty batch
|
||||
(this.s.currentBatchSize > 0 &&
|
||||
this.s.currentBatchSizeBytes + maxKeySize + bsonSize >= this.s.maxBatchSizeBytes) ||
|
||||
// New batch if the new op does not have the same op type as the current batch
|
||||
this.s.currentBatch.batchType !== batchType
|
||||
) {
|
||||
// Save the batch to the execution stack
|
||||
this.s.batches.push(this.s.currentBatch);
|
||||
|
||||
// Create a new batch
|
||||
this.s.currentBatch = new Batch(batchType, this.s.currentIndex);
|
||||
|
||||
// Reset the current size trackers
|
||||
this.s.currentBatchSize = 0;
|
||||
this.s.currentBatchSizeBytes = 0;
|
||||
}
|
||||
|
||||
if (batchType === BatchType.INSERT) {
|
||||
this.s.bulkResult.insertedIds.push({
|
||||
index: this.s.currentIndex,
|
||||
_id: (document as Document)._id
|
||||
});
|
||||
}
|
||||
|
||||
// We have an array of documents
|
||||
if (Array.isArray(document)) {
|
||||
throw new MongoInvalidArgumentError('Operation passed in cannot be an Array');
|
||||
}
|
||||
|
||||
this.s.currentBatch.originalIndexes.push(this.s.currentIndex);
|
||||
this.s.currentBatch.operations.push(document);
|
||||
this.s.currentBatchSize += 1;
|
||||
this.s.currentBatchSizeBytes += maxKeySize + bsonSize;
|
||||
this.s.currentIndex += 1;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import type { Document } from '../bson';
|
||||
import * as BSON from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoInvalidArgumentError } from '../error';
|
||||
import type { DeleteStatement } from '../operations/delete';
|
||||
import type { UpdateStatement } from '../operations/update';
|
||||
import type { Callback } from '../utils';
|
||||
import { Batch, BatchType, BulkOperationBase, BulkWriteOptions, BulkWriteResult } from './common';
|
||||
|
||||
/** @public */
|
||||
export class UnorderedBulkOperation extends BulkOperationBase {
|
||||
constructor(collection: Collection, options: BulkWriteOptions) {
|
||||
super(collection, options, false);
|
||||
}
|
||||
|
||||
override handleWriteError(callback: Callback, writeResult: BulkWriteResult): boolean {
|
||||
if (this.s.batches.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.handleWriteError(callback, writeResult);
|
||||
}
|
||||
|
||||
addToOperationsList(
|
||||
batchType: BatchType,
|
||||
document: Document | UpdateStatement | DeleteStatement
|
||||
): this {
|
||||
// Get the bsonSize
|
||||
const bsonSize = BSON.calculateObjectSize(document, {
|
||||
checkKeys: false,
|
||||
|
||||
// Since we don't know what the user selected for BSON options here,
|
||||
// err on the safe side, and check the size with ignoreUndefined: false.
|
||||
ignoreUndefined: false
|
||||
} as any);
|
||||
|
||||
// Throw error if the doc is bigger than the max BSON size
|
||||
if (bsonSize >= this.s.maxBsonObjectSize) {
|
||||
// TODO(NODE-3483): Change this to MongoBSONError
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Document is larger than the maximum size ${this.s.maxBsonObjectSize}`
|
||||
);
|
||||
}
|
||||
|
||||
// Holds the current batch
|
||||
this.s.currentBatch = undefined;
|
||||
// Get the right type of batch
|
||||
if (batchType === BatchType.INSERT) {
|
||||
this.s.currentBatch = this.s.currentInsertBatch;
|
||||
} else if (batchType === BatchType.UPDATE) {
|
||||
this.s.currentBatch = this.s.currentUpdateBatch;
|
||||
} else if (batchType === BatchType.DELETE) {
|
||||
this.s.currentBatch = this.s.currentRemoveBatch;
|
||||
}
|
||||
|
||||
const maxKeySize = this.s.maxKeySize;
|
||||
|
||||
// Create a new batch object if we don't have a current one
|
||||
if (this.s.currentBatch == null) {
|
||||
this.s.currentBatch = new Batch(batchType, this.s.currentIndex);
|
||||
}
|
||||
|
||||
// Check if we need to create a new batch
|
||||
if (
|
||||
// New batch if we exceed the max batch op size
|
||||
this.s.currentBatch.size + 1 >= this.s.maxWriteBatchSize ||
|
||||
// New batch if we exceed the maxBatchSizeBytes. Only matters if batch already has a doc,
|
||||
// since we can't sent an empty batch
|
||||
(this.s.currentBatch.size > 0 &&
|
||||
this.s.currentBatch.sizeBytes + maxKeySize + bsonSize >= this.s.maxBatchSizeBytes) ||
|
||||
// New batch if the new op does not have the same op type as the current batch
|
||||
this.s.currentBatch.batchType !== batchType
|
||||
) {
|
||||
// Save the batch to the execution stack
|
||||
this.s.batches.push(this.s.currentBatch);
|
||||
|
||||
// Create a new batch
|
||||
this.s.currentBatch = new Batch(batchType, this.s.currentIndex);
|
||||
}
|
||||
|
||||
// We have an array of documents
|
||||
if (Array.isArray(document)) {
|
||||
throw new MongoInvalidArgumentError('Operation passed in cannot be an Array');
|
||||
}
|
||||
|
||||
this.s.currentBatch.operations.push(document);
|
||||
this.s.currentBatch.originalIndexes.push(this.s.currentIndex);
|
||||
this.s.currentIndex = this.s.currentIndex + 1;
|
||||
|
||||
// Save back the current Batch to the right type
|
||||
if (batchType === BatchType.INSERT) {
|
||||
this.s.currentInsertBatch = this.s.currentBatch;
|
||||
this.s.bulkResult.insertedIds.push({
|
||||
index: this.s.bulkResult.insertedIds.length,
|
||||
_id: (document as Document)._id
|
||||
});
|
||||
} else if (batchType === BatchType.UPDATE) {
|
||||
this.s.currentUpdateBatch = this.s.currentBatch;
|
||||
} else if (batchType === BatchType.DELETE) {
|
||||
this.s.currentRemoveBatch = this.s.currentBatch;
|
||||
}
|
||||
|
||||
// Update current batch size
|
||||
this.s.currentBatch.size += 1;
|
||||
this.s.currentBatch.sizeBytes += maxKeySize + bsonSize;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+941
@@ -0,0 +1,941 @@
|
||||
import type { Readable } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import type { Binary, Document, Timestamp } from './bson';
|
||||
import { Collection } from './collection';
|
||||
import { CHANGE, CLOSE, END, ERROR, INIT, MORE, RESPONSE, RESUME_TOKEN_CHANGED } from './constants';
|
||||
import type { AbstractCursorEvents, CursorStreamOptions } from './cursor/abstract_cursor';
|
||||
import { ChangeStreamCursor, ChangeStreamCursorOptions } from './cursor/change_stream_cursor';
|
||||
import { Db } from './db';
|
||||
import {
|
||||
AnyError,
|
||||
isResumableError,
|
||||
MongoAPIError,
|
||||
MongoChangeStreamError,
|
||||
MongoRuntimeError
|
||||
} from './error';
|
||||
import { MongoClient } from './mongo_client';
|
||||
import { InferIdType, TypedEventEmitter } from './mongo_types';
|
||||
import type { AggregateOptions } from './operations/aggregate';
|
||||
import type { CollationOptions, OperationParent } from './operations/command';
|
||||
import type { ReadPreference } from './read_preference';
|
||||
import type { ServerSessionId } from './sessions';
|
||||
import { Callback, filterOptions, getTopology, maybePromise, MongoDBNamespace } from './utils';
|
||||
|
||||
/** @internal */
|
||||
const kCursorStream = Symbol('cursorStream');
|
||||
/** @internal */
|
||||
const kClosed = Symbol('closed');
|
||||
/** @internal */
|
||||
const kMode = Symbol('mode');
|
||||
|
||||
const CHANGE_STREAM_OPTIONS = [
|
||||
'resumeAfter',
|
||||
'startAfter',
|
||||
'startAtOperationTime',
|
||||
'fullDocument',
|
||||
'fullDocumentBeforeChange',
|
||||
'showExpandedEvents'
|
||||
] as const;
|
||||
|
||||
const CHANGE_DOMAIN_TYPES = {
|
||||
COLLECTION: Symbol('Collection'),
|
||||
DATABASE: Symbol('Database'),
|
||||
CLUSTER: Symbol('Cluster')
|
||||
};
|
||||
|
||||
const CHANGE_STREAM_EVENTS = [RESUME_TOKEN_CHANGED, END, CLOSE];
|
||||
|
||||
const NO_RESUME_TOKEN_ERROR =
|
||||
'A change stream document has been received that lacks a resume token (_id).';
|
||||
const CHANGESTREAM_CLOSED_ERROR = 'ChangeStream is closed';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please use the ChangeStreamCursorOptions type instead.
|
||||
*/
|
||||
export interface ResumeOptions {
|
||||
startAtOperationTime?: Timestamp;
|
||||
batchSize?: number;
|
||||
maxAwaitTimeMS?: number;
|
||||
collation?: CollationOptions;
|
||||
readPreference?: ReadPreference;
|
||||
resumeAfter?: ResumeToken;
|
||||
startAfter?: ResumeToken;
|
||||
fullDocument?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the logical starting point for a new ChangeStream or resuming a ChangeStream on the server.
|
||||
* @see https://www.mongodb.com/docs/manual/changeStreams/#std-label-change-stream-resume
|
||||
* @public
|
||||
*/
|
||||
export type ResumeToken = unknown;
|
||||
|
||||
/**
|
||||
* Represents a specific point in time on a server. Can be retrieved by using `db.command()`
|
||||
* @public
|
||||
* @see https://docs.mongodb.com/manual/reference/method/db.runCommand/#response
|
||||
*/
|
||||
export type OperationTime = Timestamp;
|
||||
|
||||
/** @public */
|
||||
export interface PipeOptions {
|
||||
end?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be passed to a ChangeStream. Note that startAfter, resumeAfter, and startAtOperationTime are all mutually exclusive, and the server will error if more than one is specified.
|
||||
* @public
|
||||
*/
|
||||
export interface ChangeStreamOptions extends AggregateOptions {
|
||||
/**
|
||||
* Allowed values: 'updateLookup', 'whenAvailable', 'required'.
|
||||
*
|
||||
* When set to 'updateLookup', the change notification for partial updates
|
||||
* will include both a delta describing the changes to the document as well
|
||||
* as a copy of the entire document that was changed from some time after
|
||||
* the change occurred.
|
||||
*
|
||||
* When set to 'whenAvailable', configures the change stream to return the
|
||||
* post-image of the modified document for replace and update change events
|
||||
* if the post-image for this event is available.
|
||||
*
|
||||
* When set to 'required', the same behavior as 'whenAvailable' except that
|
||||
* an error is raised if the post-image is not available.
|
||||
*/
|
||||
fullDocument?: string;
|
||||
|
||||
/**
|
||||
* Allowed values: 'whenAvailable', 'required', 'off'.
|
||||
*
|
||||
* The default is to not send a value, which is equivalent to 'off'.
|
||||
*
|
||||
* When set to 'whenAvailable', configures the change stream to return the
|
||||
* pre-image of the modified document for replace, update, and delete change
|
||||
* events if it is available.
|
||||
*
|
||||
* When set to 'required', the same behavior as 'whenAvailable' except that
|
||||
* an error is raised if the pre-image is not available.
|
||||
*/
|
||||
fullDocumentBeforeChange?: string;
|
||||
/** The maximum amount of time for the server to wait on new documents to satisfy a change stream query. */
|
||||
maxAwaitTimeMS?: number;
|
||||
/**
|
||||
* Allows you to start a changeStream after a specified event.
|
||||
* @see https://docs.mongodb.com/manual/changeStreams/#resumeafter-for-change-streams
|
||||
*/
|
||||
resumeAfter?: ResumeToken;
|
||||
/**
|
||||
* Similar to resumeAfter, but will allow you to start after an invalidated event.
|
||||
* @see https://docs.mongodb.com/manual/changeStreams/#startafter-for-change-streams
|
||||
*/
|
||||
startAfter?: ResumeToken;
|
||||
/** Will start the changeStream after the specified operationTime. */
|
||||
startAtOperationTime?: OperationTime;
|
||||
/**
|
||||
* The number of documents to return per batch.
|
||||
* @see https://docs.mongodb.com/manual/reference/command/aggregate
|
||||
*/
|
||||
batchSize?: number;
|
||||
|
||||
/**
|
||||
* When enabled, configures the change stream to include extra change events.
|
||||
*
|
||||
* - createIndexes
|
||||
* - dropIndexes
|
||||
* - modify
|
||||
* - create
|
||||
* - shardCollection
|
||||
* - reshardCollection
|
||||
* - refineCollectionShardKey
|
||||
*/
|
||||
showExpandedEvents?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChangeStreamNameSpace {
|
||||
db: string;
|
||||
coll: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChangeStreamDocumentKey<TSchema extends Document = Document> {
|
||||
/**
|
||||
* For unsharded collections this contains a single field `_id`.
|
||||
* For sharded collections, this will contain all the components of the shard key
|
||||
*/
|
||||
documentKey: { _id: InferIdType<TSchema>; [shardKey: string]: any };
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChangeStreamDocumentCommon {
|
||||
/**
|
||||
* The id functions as an opaque token for use when resuming an interrupted
|
||||
* change stream.
|
||||
*/
|
||||
_id: ResumeToken;
|
||||
/**
|
||||
* The timestamp from the oplog entry associated with the event.
|
||||
* For events that happened as part of a multi-document transaction, the associated change stream
|
||||
* notifications will have the same clusterTime value, namely the time when the transaction was committed.
|
||||
* On a sharded cluster, events that occur on different shards can have the same clusterTime but be
|
||||
* associated with different transactions or even not be associated with any transaction.
|
||||
* To identify events for a single transaction, you can use the combination of lsid and txnNumber in the change stream event document.
|
||||
*/
|
||||
clusterTime?: Timestamp;
|
||||
|
||||
/**
|
||||
* The transaction number.
|
||||
* Only present if the operation is part of a multi-document transaction.
|
||||
*
|
||||
* **NOTE:** txnNumber can be a Long if promoteLongs is set to false
|
||||
*/
|
||||
txnNumber?: number;
|
||||
|
||||
/**
|
||||
* The identifier for the session associated with the transaction.
|
||||
* Only present if the operation is part of a multi-document transaction.
|
||||
*/
|
||||
lsid?: ServerSessionId;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChangeStreamDocumentCollectionUUID {
|
||||
/**
|
||||
* The UUID (Binary subtype 4) of the collection that the operation was performed on.
|
||||
*
|
||||
* Only present when the `showExpandedEvents` flag is enabled.
|
||||
*
|
||||
* **NOTE:** collectionUUID will be converted to a NodeJS Buffer if the promoteBuffers
|
||||
* flag is enabled.
|
||||
*
|
||||
* @since 6.1.0
|
||||
*/
|
||||
collectionUUID: Binary;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChangeStreamDocumentOperationDescription {
|
||||
/**
|
||||
* An description of the operation.
|
||||
*
|
||||
* Only present when the `showExpandedEvents` flag is enabled.
|
||||
*
|
||||
* @since 6.1.0
|
||||
*/
|
||||
operationDescription?: Document;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#insert-event
|
||||
*/
|
||||
export interface ChangeStreamInsertDocument<TSchema extends Document = Document>
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentKey<TSchema>,
|
||||
ChangeStreamDocumentCollectionUUID {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'insert';
|
||||
/** This key will contain the document being inserted */
|
||||
fullDocument: TSchema;
|
||||
/** Namespace the insert event occured on */
|
||||
ns: ChangeStreamNameSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#update-event
|
||||
*/
|
||||
export interface ChangeStreamUpdateDocument<TSchema extends Document = Document>
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentKey<TSchema>,
|
||||
ChangeStreamDocumentCollectionUUID {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'update';
|
||||
/**
|
||||
* This is only set if `fullDocument` is set to `'updateLookup'`
|
||||
* Contains the point-in-time post-image of the modified document if the
|
||||
* post-image is available and either 'required' or 'whenAvailable' was
|
||||
* specified for the 'fullDocument' option when creating the change stream.
|
||||
*/
|
||||
fullDocument?: TSchema;
|
||||
/** Contains a description of updated and removed fields in this operation */
|
||||
updateDescription: UpdateDescription<TSchema>;
|
||||
/** Namespace the update event occured on */
|
||||
ns: ChangeStreamNameSpace;
|
||||
/**
|
||||
* Contains the pre-image of the modified or deleted document if the
|
||||
* pre-image is available for the change event and either 'required' or
|
||||
* 'whenAvailable' was specified for the 'fullDocumentBeforeChange' option
|
||||
* when creating the change stream. If 'whenAvailable' was specified but the
|
||||
* pre-image is unavailable, this will be explicitly set to null.
|
||||
*/
|
||||
fullDocumentBeforeChange?: TSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#replace-event
|
||||
*/
|
||||
export interface ChangeStreamReplaceDocument<TSchema extends Document = Document>
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentKey<TSchema> {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'replace';
|
||||
/** The fullDocument of a replace event represents the document after the insert of the replacement document */
|
||||
fullDocument: TSchema;
|
||||
/** Namespace the replace event occured on */
|
||||
ns: ChangeStreamNameSpace;
|
||||
/**
|
||||
* Contains the pre-image of the modified or deleted document if the
|
||||
* pre-image is available for the change event and either 'required' or
|
||||
* 'whenAvailable' was specified for the 'fullDocumentBeforeChange' option
|
||||
* when creating the change stream. If 'whenAvailable' was specified but the
|
||||
* pre-image is unavailable, this will be explicitly set to null.
|
||||
*/
|
||||
fullDocumentBeforeChange?: TSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#delete-event
|
||||
*/
|
||||
export interface ChangeStreamDeleteDocument<TSchema extends Document = Document>
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentKey<TSchema>,
|
||||
ChangeStreamDocumentCollectionUUID {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'delete';
|
||||
/** Namespace the delete event occured on */
|
||||
ns: ChangeStreamNameSpace;
|
||||
/**
|
||||
* Contains the pre-image of the modified or deleted document if the
|
||||
* pre-image is available for the change event and either 'required' or
|
||||
* 'whenAvailable' was specified for the 'fullDocumentBeforeChange' option
|
||||
* when creating the change stream. If 'whenAvailable' was specified but the
|
||||
* pre-image is unavailable, this will be explicitly set to null.
|
||||
*/
|
||||
fullDocumentBeforeChange?: TSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#drop-event
|
||||
*/
|
||||
export interface ChangeStreamDropDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'drop';
|
||||
/** Namespace the drop event occured on */
|
||||
ns: ChangeStreamNameSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#rename-event
|
||||
*/
|
||||
export interface ChangeStreamRenameDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'rename';
|
||||
/** The new name for the `ns.coll` collection */
|
||||
to: { db: string; coll: string };
|
||||
/** The "from" namespace that the rename occured on */
|
||||
ns: ChangeStreamNameSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#dropdatabase-event
|
||||
*/
|
||||
export interface ChangeStreamDropDatabaseDocument extends ChangeStreamDocumentCommon {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'dropDatabase';
|
||||
/** The database dropped */
|
||||
ns: { db: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/#invalidate-event
|
||||
*/
|
||||
export interface ChangeStreamInvalidateDocument extends ChangeStreamDocumentCommon {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'invalidate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Only present when the `showExpandedEvents` flag is enabled.
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/
|
||||
*/
|
||||
export interface ChangeStreamCreateIndexDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID,
|
||||
ChangeStreamDocumentOperationDescription {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'createIndexes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Only present when the `showExpandedEvents` flag is enabled.
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/
|
||||
*/
|
||||
export interface ChangeStreamDropIndexDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID,
|
||||
ChangeStreamDocumentOperationDescription {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'dropIndexes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Only present when the `showExpandedEvents` flag is enabled.
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/
|
||||
*/
|
||||
export interface ChangeStreamCollModDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'modify';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/
|
||||
*/
|
||||
export interface ChangeStreamCreateDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'create';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/
|
||||
*/
|
||||
export interface ChangeStreamShardCollectionDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID,
|
||||
ChangeStreamDocumentOperationDescription {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'shardCollection';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/
|
||||
*/
|
||||
export interface ChangeStreamReshardCollectionDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID,
|
||||
ChangeStreamDocumentOperationDescription {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'reshardCollection';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://www.mongodb.com/docs/manual/reference/change-events/
|
||||
*/
|
||||
export interface ChangeStreamRefineCollectionShardKeyDocument
|
||||
extends ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentCollectionUUID,
|
||||
ChangeStreamDocumentOperationDescription {
|
||||
/** Describes the type of operation represented in this change notification */
|
||||
operationType: 'refineCollectionShardKey';
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ChangeStreamDocument<TSchema extends Document = Document> =
|
||||
| ChangeStreamInsertDocument<TSchema>
|
||||
| ChangeStreamUpdateDocument<TSchema>
|
||||
| ChangeStreamReplaceDocument<TSchema>
|
||||
| ChangeStreamDeleteDocument<TSchema>
|
||||
| ChangeStreamDropDocument
|
||||
| ChangeStreamRenameDocument
|
||||
| ChangeStreamDropDatabaseDocument
|
||||
| ChangeStreamInvalidateDocument
|
||||
| ChangeStreamCreateIndexDocument
|
||||
| ChangeStreamCreateDocument
|
||||
| ChangeStreamCollModDocument
|
||||
| ChangeStreamDropIndexDocument
|
||||
| ChangeStreamShardCollectionDocument
|
||||
| ChangeStreamReshardCollectionDocument
|
||||
| ChangeStreamRefineCollectionShardKeyDocument;
|
||||
|
||||
/** @public */
|
||||
export interface UpdateDescription<TSchema extends Document = Document> {
|
||||
/**
|
||||
* A document containing key:value pairs of names of the fields that were
|
||||
* changed, and the new value for those fields.
|
||||
*/
|
||||
updatedFields?: Partial<TSchema>;
|
||||
|
||||
/**
|
||||
* An array of field names that were removed from the document.
|
||||
*/
|
||||
removedFields?: string[];
|
||||
|
||||
/**
|
||||
* An array of documents which record array truncations performed with pipeline-based updates using one or more of the following stages:
|
||||
* - $addFields
|
||||
* - $set
|
||||
* - $replaceRoot
|
||||
* - $replaceWith
|
||||
*/
|
||||
truncatedArrays?: Array<{
|
||||
/** The name of the truncated field. */
|
||||
field: string;
|
||||
/** The number of elements in the truncated array. */
|
||||
newSize: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ChangeStreamEvents<
|
||||
TSchema extends Document = Document,
|
||||
TChange extends Document = ChangeStreamDocument<TSchema>
|
||||
> = {
|
||||
resumeTokenChanged(token: ResumeToken): void;
|
||||
init(response: any): void;
|
||||
more(response?: any): void;
|
||||
response(): void;
|
||||
end(): void;
|
||||
error(error: Error): void;
|
||||
change(change: TChange): void;
|
||||
} & AbstractCursorEvents;
|
||||
|
||||
/**
|
||||
* Creates a new Change Stream instance. Normally created using {@link Collection#watch|Collection.watch()}.
|
||||
* @public
|
||||
*/
|
||||
export class ChangeStream<
|
||||
TSchema extends Document = Document,
|
||||
TChange extends Document = ChangeStreamDocument<TSchema>
|
||||
> extends TypedEventEmitter<ChangeStreamEvents<TSchema, TChange>> {
|
||||
pipeline: Document[];
|
||||
options: ChangeStreamOptions;
|
||||
parent: MongoClient | Db | Collection;
|
||||
namespace: MongoDBNamespace;
|
||||
type: symbol;
|
||||
/** @internal */
|
||||
cursor: ChangeStreamCursor<TSchema, TChange>;
|
||||
streamOptions?: CursorStreamOptions;
|
||||
/** @internal */
|
||||
[kCursorStream]?: Readable & AsyncIterable<TChange>;
|
||||
/** @internal */
|
||||
[kClosed]: boolean;
|
||||
/** @internal */
|
||||
[kMode]: false | 'iterator' | 'emitter';
|
||||
|
||||
/** @event */
|
||||
static readonly RESPONSE = RESPONSE;
|
||||
/** @event */
|
||||
static readonly MORE = MORE;
|
||||
/** @event */
|
||||
static readonly INIT = INIT;
|
||||
/** @event */
|
||||
static readonly CLOSE = CLOSE;
|
||||
/**
|
||||
* Fired for each new matching change in the specified namespace. Attaching a `change`
|
||||
* event listener to a Change Stream will switch the stream into flowing mode. Data will
|
||||
* then be passed as soon as it is available.
|
||||
* @event
|
||||
*/
|
||||
static readonly CHANGE = CHANGE;
|
||||
/** @event */
|
||||
static readonly END = END;
|
||||
/** @event */
|
||||
static readonly ERROR = ERROR;
|
||||
/**
|
||||
* Emitted each time the change stream stores a new resume token.
|
||||
* @event
|
||||
*/
|
||||
static readonly RESUME_TOKEN_CHANGED = RESUME_TOKEN_CHANGED;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @param parent - The parent object that created this change stream
|
||||
* @param pipeline - An array of {@link https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents
|
||||
*/
|
||||
constructor(
|
||||
parent: OperationParent,
|
||||
pipeline: Document[] = [],
|
||||
options: ChangeStreamOptions = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
this.pipeline = pipeline;
|
||||
this.options = options;
|
||||
|
||||
if (parent instanceof Collection) {
|
||||
this.type = CHANGE_DOMAIN_TYPES.COLLECTION;
|
||||
} else if (parent instanceof Db) {
|
||||
this.type = CHANGE_DOMAIN_TYPES.DATABASE;
|
||||
} else if (parent instanceof MongoClient) {
|
||||
this.type = CHANGE_DOMAIN_TYPES.CLUSTER;
|
||||
} else {
|
||||
throw new MongoChangeStreamError(
|
||||
'Parent provided to ChangeStream constructor must be an instance of Collection, Db, or MongoClient'
|
||||
);
|
||||
}
|
||||
|
||||
this.parent = parent;
|
||||
this.namespace = parent.s.namespace;
|
||||
if (!this.options.readPreference && parent.readPreference) {
|
||||
this.options.readPreference = parent.readPreference;
|
||||
}
|
||||
|
||||
// Create contained Change Stream cursor
|
||||
this.cursor = this._createChangeStreamCursor(options);
|
||||
|
||||
this[kClosed] = false;
|
||||
this[kMode] = false;
|
||||
|
||||
// Listen for any `change` listeners being added to ChangeStream
|
||||
this.on('newListener', eventName => {
|
||||
if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
|
||||
this._streamEvents(this.cursor);
|
||||
}
|
||||
});
|
||||
|
||||
this.on('removeListener', eventName => {
|
||||
if (eventName === 'change' && this.listenerCount('change') === 0 && this.cursor) {
|
||||
this[kCursorStream]?.removeAllListeners('data');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
get cursorStream(): (Readable & AsyncIterable<TChange>) | undefined {
|
||||
return this[kCursorStream];
|
||||
}
|
||||
|
||||
/** The cached resume token that is used to resume after the most recently returned change. */
|
||||
get resumeToken(): ResumeToken {
|
||||
return this.cursor?.resumeToken;
|
||||
}
|
||||
|
||||
/** Check if there is any document still available in the Change Stream */
|
||||
hasNext(): Promise<boolean>;
|
||||
hasNext(callback: Callback<boolean>): void;
|
||||
hasNext(callback?: Callback): Promise<boolean> | void {
|
||||
this._setIsIterator();
|
||||
// TOOD(NODE-4319): Add eslint rule preventing accidental variable shadowing
|
||||
// Shadowing is intentional here. We want to override the `callback` variable
|
||||
// from the outer scope so that the inner scope doesn't accidentally call the wrong callback.
|
||||
return maybePromise(callback, callback => {
|
||||
(async () => {
|
||||
try {
|
||||
const hasNext = await this.cursor.hasNext();
|
||||
return hasNext;
|
||||
} catch (error) {
|
||||
try {
|
||||
await this._processErrorIteratorMode(error);
|
||||
const hasNext = await this.cursor.hasNext();
|
||||
return hasNext;
|
||||
} catch (error) {
|
||||
await this.close().catch(err => err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})().then(
|
||||
hasNext => callback(undefined, hasNext),
|
||||
error => callback(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Get the next available document from the Change Stream. */
|
||||
next(): Promise<TChange>;
|
||||
next(callback: Callback<TChange>): void;
|
||||
next(callback?: Callback<TChange>): Promise<TChange> | void {
|
||||
this._setIsIterator();
|
||||
// TOOD(NODE-4319): Add eslint rule preventing accidental variable shadowing
|
||||
// Shadowing is intentional here. We want to override the `callback` variable
|
||||
// from the outer scope so that the inner scope doesn't accidentally call the wrong callback.
|
||||
return maybePromise(callback, callback => {
|
||||
(async () => {
|
||||
try {
|
||||
const change = await this.cursor.next();
|
||||
const processedChange = this._processChange(change ?? null);
|
||||
return processedChange;
|
||||
} catch (error) {
|
||||
try {
|
||||
await this._processErrorIteratorMode(error);
|
||||
const change = await this.cursor.next();
|
||||
const processedChange = this._processChange(change ?? null);
|
||||
return processedChange;
|
||||
} catch (error) {
|
||||
await this.close().catch(err => err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})().then(
|
||||
change => callback(undefined, change),
|
||||
error => callback(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get the next available document from the Change Stream's cursor or `null` if an empty batch is returned
|
||||
*/
|
||||
tryNext(): Promise<Document | null>;
|
||||
tryNext(callback: Callback<Document | null>): void;
|
||||
tryNext(callback?: Callback<Document | null>): Promise<Document | null> | void {
|
||||
this._setIsIterator();
|
||||
// TOOD(NODE-4319): Add eslint rule preventing accidental variable shadowing
|
||||
// Shadowing is intentional here. We want to override the `callback` variable
|
||||
// from the outer scope so that the inner scope doesn't accidentally call the wrong callback.
|
||||
return maybePromise(callback, callback => {
|
||||
(async () => {
|
||||
try {
|
||||
const change = await this.cursor.tryNext();
|
||||
return change ?? null;
|
||||
} catch (error) {
|
||||
try {
|
||||
await this._processErrorIteratorMode(error);
|
||||
const change = await this.cursor.tryNext();
|
||||
return change ?? null;
|
||||
} catch (error) {
|
||||
await this.close().catch(err => err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})().then(
|
||||
change => callback(undefined, change),
|
||||
error => callback(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Is the cursor closed */
|
||||
get closed(): boolean {
|
||||
return this[kClosed] || this.cursor.closed;
|
||||
}
|
||||
|
||||
/** Close the Change Stream */
|
||||
close(): Promise<void>;
|
||||
close(callback: Callback): void;
|
||||
close(callback?: Callback): Promise<void> | void {
|
||||
this[kClosed] = true;
|
||||
|
||||
return maybePromise(callback, cb => {
|
||||
const cursor = this.cursor;
|
||||
return cursor.close(err => {
|
||||
this._endStream();
|
||||
return cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a modified Readable stream including a possible transform method.
|
||||
*
|
||||
* NOTE: When using a Stream to process change stream events, the stream will
|
||||
* NOT automatically resume in the case a resumable error is encountered.
|
||||
*
|
||||
* @throws MongoChangeStreamError if the underlying cursor or the change stream is closed
|
||||
*/
|
||||
stream(options?: CursorStreamOptions): Readable & AsyncIterable<TChange> {
|
||||
if (this.closed) {
|
||||
throw new MongoChangeStreamError(CHANGESTREAM_CLOSED_ERROR);
|
||||
}
|
||||
|
||||
this.streamOptions = options;
|
||||
return this.cursor.stream(options);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _setIsEmitter(): void {
|
||||
if (this[kMode] === 'iterator') {
|
||||
// TODO(NODE-3485): Replace with MongoChangeStreamModeError
|
||||
throw new MongoAPIError(
|
||||
'ChangeStream cannot be used as an EventEmitter after being used as an iterator'
|
||||
);
|
||||
}
|
||||
this[kMode] = 'emitter';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _setIsIterator(): void {
|
||||
if (this[kMode] === 'emitter') {
|
||||
// TODO(NODE-3485): Replace with MongoChangeStreamModeError
|
||||
throw new MongoAPIError(
|
||||
'ChangeStream cannot be used as an iterator after being used as an EventEmitter'
|
||||
);
|
||||
}
|
||||
this[kMode] = 'iterator';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new change stream cursor based on self's configuration
|
||||
* @internal
|
||||
*/
|
||||
private _createChangeStreamCursor(
|
||||
options: ChangeStreamOptions | ChangeStreamCursorOptions
|
||||
): ChangeStreamCursor<TSchema, TChange> {
|
||||
const changeStreamStageOptions = filterOptions(options, CHANGE_STREAM_OPTIONS);
|
||||
if (this.type === CHANGE_DOMAIN_TYPES.CLUSTER) {
|
||||
changeStreamStageOptions.allChangesForCluster = true;
|
||||
}
|
||||
const pipeline = [{ $changeStream: changeStreamStageOptions }, ...this.pipeline];
|
||||
|
||||
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<TSchema, TChange>(
|
||||
client,
|
||||
this.namespace,
|
||||
pipeline,
|
||||
options
|
||||
);
|
||||
|
||||
for (const event of CHANGE_STREAM_EVENTS) {
|
||||
changeStreamCursor.on(event, e => this.emit(event, e));
|
||||
}
|
||||
|
||||
if (this.listenerCount(ChangeStream.CHANGE) > 0) {
|
||||
this._streamEvents(changeStreamCursor);
|
||||
}
|
||||
|
||||
return changeStreamCursor;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _closeEmitterModeWithError(error: AnyError): void {
|
||||
this.emit(ChangeStream.ERROR, error);
|
||||
|
||||
this.close(() => {
|
||||
// nothing to do
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _streamEvents(cursor: ChangeStreamCursor<TSchema, TChange>): void {
|
||||
this._setIsEmitter();
|
||||
const stream = this[kCursorStream] ?? cursor.stream();
|
||||
this[kCursorStream] = stream;
|
||||
stream.on('data', change => {
|
||||
try {
|
||||
const processedChange = this._processChange(change);
|
||||
this.emit(ChangeStream.CHANGE, processedChange);
|
||||
} catch (error) {
|
||||
this.emit(ChangeStream.ERROR, error);
|
||||
}
|
||||
});
|
||||
stream.on('error', error => this._processErrorStreamMode(error));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _endStream(): void {
|
||||
const cursorStream = this[kCursorStream];
|
||||
if (cursorStream) {
|
||||
['data', 'close', 'end', 'error'].forEach(event => cursorStream.removeAllListeners(event));
|
||||
cursorStream.destroy();
|
||||
}
|
||||
|
||||
this[kCursorStream] = undefined;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _processChange(change: TChange | null): TChange {
|
||||
if (this[kClosed]) {
|
||||
// TODO(NODE-3485): Replace with MongoChangeStreamClosedError
|
||||
throw new MongoAPIError(CHANGESTREAM_CLOSED_ERROR);
|
||||
}
|
||||
|
||||
// a null change means the cursor has been notified, implicitly closing the change stream
|
||||
if (change == null) {
|
||||
// TODO(NODE-3485): Replace with MongoChangeStreamClosedError
|
||||
throw new MongoRuntimeError(CHANGESTREAM_CLOSED_ERROR);
|
||||
}
|
||||
|
||||
if (change && !change._id) {
|
||||
throw new MongoChangeStreamError(NO_RESUME_TOKEN_ERROR);
|
||||
}
|
||||
|
||||
// cache the resume token
|
||||
this.cursor.cacheResumeToken(change._id);
|
||||
|
||||
// wipe the startAtOperationTime if there was one so that there won't be a conflict
|
||||
// between resumeToken and startAtOperationTime if we need to reconnect the cursor
|
||||
this.options.startAtOperationTime = undefined;
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _processErrorStreamMode(changeStreamError: AnyError) {
|
||||
// If the change stream has been closed explicitly, do not process error.
|
||||
if (this[kClosed]) return;
|
||||
|
||||
if (isResumableError(changeStreamError, this.cursor.maxWireVersion)) {
|
||||
this._endStream();
|
||||
this.cursor.close().catch(() => null);
|
||||
|
||||
const topology = getTopology(this.parent);
|
||||
topology.selectServer(this.cursor.readPreference, {}, serverSelectionError => {
|
||||
if (serverSelectionError) return this._closeEmitterModeWithError(changeStreamError);
|
||||
this.cursor = this._createChangeStreamCursor(this.cursor.resumeOptions);
|
||||
});
|
||||
} else {
|
||||
this._closeEmitterModeWithError(changeStreamError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* TODO(NODE-4320): promisify selectServer and refactor this code to be async
|
||||
*
|
||||
* we promisify _processErrorIteratorModeCallback until we have a promisifed version of selectServer.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
private _processErrorIteratorMode = promisify(this._processErrorIteratorModeCallback);
|
||||
|
||||
/** @internal */
|
||||
private _processErrorIteratorModeCallback(changeStreamError: AnyError, callback: Callback) {
|
||||
if (this[kClosed]) {
|
||||
// TODO(NODE-3485): Replace with MongoChangeStreamClosedError
|
||||
return callback(new MongoAPIError(CHANGESTREAM_CLOSED_ERROR));
|
||||
}
|
||||
|
||||
if (isResumableError(changeStreamError, this.cursor.maxWireVersion)) {
|
||||
this.cursor.close().catch(() => null);
|
||||
|
||||
const topology = getTopology(this.parent);
|
||||
topology.selectServer(this.cursor.readPreference, {}, serverSelectionError => {
|
||||
// if the topology can't reconnect, close the stream
|
||||
if (serverSelectionError) return this.close(() => callback(changeStreamError));
|
||||
|
||||
this.cursor = this._createChangeStreamCursor(this.cursor.resumeOptions);
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
this.close(() => callback(changeStreamError));
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import type { Document } from '../../bson';
|
||||
import { MongoRuntimeError } from '../../error';
|
||||
import type { Callback, ClientMetadataOptions } from '../../utils';
|
||||
import type { HandshakeDocument } from '../connect';
|
||||
import type { Connection, ConnectionOptions } from '../connection';
|
||||
import type { MongoCredentials } from './mongo_credentials';
|
||||
|
||||
export type AuthContextOptions = ConnectionOptions & ClientMetadataOptions;
|
||||
|
||||
/** Context used during authentication */
|
||||
export class AuthContext {
|
||||
/** The connection to authenticate */
|
||||
connection: Connection;
|
||||
/** The credentials to use for authentication */
|
||||
credentials?: MongoCredentials;
|
||||
/** The options passed to the `connect` method */
|
||||
options: AuthContextOptions;
|
||||
|
||||
/** A response from an initial auth attempt, only some mechanisms use this (e.g, SCRAM) */
|
||||
response?: Document;
|
||||
/** A random nonce generated for use in an authentication conversation */
|
||||
nonce?: Buffer;
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
credentials: MongoCredentials | undefined,
|
||||
options: AuthContextOptions
|
||||
) {
|
||||
this.connection = connection;
|
||||
this.credentials = credentials;
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthProvider {
|
||||
/**
|
||||
* Prepare the handshake document before the initial handshake.
|
||||
*
|
||||
* @param handshakeDoc - The document used for the initial handshake on a connection
|
||||
* @param authContext - Context for authentication flow
|
||||
*/
|
||||
prepare(
|
||||
handshakeDoc: HandshakeDocument,
|
||||
authContext: AuthContext,
|
||||
callback: Callback<HandshakeDocument>
|
||||
): void {
|
||||
callback(undefined, handshakeDoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param context - A shared context for authentication flow
|
||||
* @param callback - The callback to return the result from the authentication
|
||||
*/
|
||||
auth(context: AuthContext, callback: Callback): void {
|
||||
// TODO(NODE-3483): Replace this with MongoMethodOverrideError
|
||||
callback(new MongoRuntimeError('`auth` method must be overridden by subclass'));
|
||||
}
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
import * as dns from 'dns';
|
||||
|
||||
import type { Document } from '../../bson';
|
||||
import { Kerberos, KerberosClient } from '../../deps';
|
||||
import {
|
||||
MongoError,
|
||||
MongoInvalidArgumentError,
|
||||
MongoMissingCredentialsError,
|
||||
MongoMissingDependencyError,
|
||||
MongoRuntimeError
|
||||
} from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
|
||||
/** @public */
|
||||
export const GSSAPICanonicalizationValue = Object.freeze({
|
||||
on: true,
|
||||
off: false,
|
||||
none: 'none',
|
||||
forward: 'forward',
|
||||
forwardAndReverse: 'forwardAndReverse'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type GSSAPICanonicalizationValue =
|
||||
typeof GSSAPICanonicalizationValue[keyof typeof GSSAPICanonicalizationValue];
|
||||
|
||||
type MechanismProperties = {
|
||||
/** @deprecated use `CANONICALIZE_HOST_NAME` instead */
|
||||
gssapiCanonicalizeHostName?: boolean;
|
||||
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
|
||||
SERVICE_HOST?: string;
|
||||
SERVICE_NAME?: string;
|
||||
SERVICE_REALM?: string;
|
||||
};
|
||||
|
||||
export class GSSAPI extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (credentials == null)
|
||||
return callback(
|
||||
new MongoMissingCredentialsError('Credentials required for GSSAPI authentication')
|
||||
);
|
||||
const { username } = credentials;
|
||||
function externalCommand(
|
||||
command: Document,
|
||||
cb: Callback<{ payload: string; conversationId: any }>
|
||||
) {
|
||||
return connection.command(ns('$external.$cmd'), command, undefined, cb);
|
||||
}
|
||||
makeKerberosClient(authContext, (err, client) => {
|
||||
if (err) return callback(err);
|
||||
if (client == null) return callback(new MongoMissingDependencyError('GSSAPI client missing'));
|
||||
client.step('', (err, payload) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
externalCommand(saslStart(payload), (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result == null) return callback();
|
||||
negotiate(client, 10, result.payload, (err, payload) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
externalCommand(saslContinue(payload, result.conversationId), (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result == null) return callback();
|
||||
finalize(client, username, result.payload, (err, payload) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
externalCommand(
|
||||
{
|
||||
saslContinue: 1,
|
||||
conversationId: result.conversationId,
|
||||
payload
|
||||
},
|
||||
(err, result) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
callback(undefined, result);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function makeKerberosClient(authContext: AuthContext, callback: Callback<KerberosClient>): void {
|
||||
const { hostAddress } = authContext.options;
|
||||
const { credentials } = authContext;
|
||||
if (!hostAddress || typeof hostAddress.host !== 'string' || !credentials) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError('Connection must have host and port and credentials defined.')
|
||||
);
|
||||
}
|
||||
|
||||
if ('kModuleError' in Kerberos) {
|
||||
return callback(Kerberos['kModuleError']);
|
||||
}
|
||||
const { initializeClient } = Kerberos;
|
||||
|
||||
const { username, password } = credentials;
|
||||
const mechanismProperties = credentials.mechanismProperties as MechanismProperties;
|
||||
|
||||
const serviceName = mechanismProperties.SERVICE_NAME ?? 'mongodb';
|
||||
|
||||
performGSSAPICanonicalizeHostName(
|
||||
hostAddress.host,
|
||||
mechanismProperties,
|
||||
(err?: Error | MongoError, host?: string) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const initOptions = {};
|
||||
if (password != null) {
|
||||
Object.assign(initOptions, { user: username, password: password });
|
||||
}
|
||||
|
||||
const spnHost = mechanismProperties.SERVICE_HOST ?? host;
|
||||
let spn = `${serviceName}${process.platform === 'win32' ? '/' : '@'}${spnHost}`;
|
||||
if ('SERVICE_REALM' in mechanismProperties) {
|
||||
spn = `${spn}@${mechanismProperties.SERVICE_REALM}`;
|
||||
}
|
||||
|
||||
initializeClient(spn, initOptions, (err: string, client: KerberosClient): void => {
|
||||
// TODO(NODE-3483)
|
||||
if (err) return callback(new MongoRuntimeError(err));
|
||||
callback(undefined, client);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function saslStart(payload?: string): Document {
|
||||
return {
|
||||
saslStart: 1,
|
||||
mechanism: 'GSSAPI',
|
||||
payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
}
|
||||
|
||||
function saslContinue(payload?: string, conversationId?: number): Document {
|
||||
return {
|
||||
saslContinue: 1,
|
||||
conversationId,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
function negotiate(
|
||||
client: KerberosClient,
|
||||
retries: number,
|
||||
payload: string,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
client.step(payload, (err, response) => {
|
||||
// Retries exhausted, raise error
|
||||
if (err && retries === 0) return callback(err);
|
||||
|
||||
// Adjust number of retries and call step again
|
||||
if (err) return negotiate(client, retries - 1, payload, callback);
|
||||
|
||||
// Return the payload
|
||||
callback(undefined, response || '');
|
||||
});
|
||||
}
|
||||
|
||||
function finalize(
|
||||
client: KerberosClient,
|
||||
user: string,
|
||||
payload: string,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
// GSS Client Unwrap
|
||||
client.unwrap(payload, (err, response) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Wrap the response
|
||||
client.wrap(response || '', { user }, (err, wrapped) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Return the payload
|
||||
callback(undefined, wrapped);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function performGSSAPICanonicalizeHostName(
|
||||
host: string,
|
||||
mechanismProperties: MechanismProperties,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
const mode = mechanismProperties.CANONICALIZE_HOST_NAME;
|
||||
if (!mode || mode === GSSAPICanonicalizationValue.none) {
|
||||
return callback(undefined, host);
|
||||
}
|
||||
|
||||
// If forward and reverse or true
|
||||
if (
|
||||
mode === GSSAPICanonicalizationValue.on ||
|
||||
mode === GSSAPICanonicalizationValue.forwardAndReverse
|
||||
) {
|
||||
// Perform the lookup of the ip address.
|
||||
dns.lookup(host, (error, address) => {
|
||||
// No ip found, return the error.
|
||||
if (error) return callback(error);
|
||||
|
||||
// Perform a reverse ptr lookup on the ip address.
|
||||
dns.resolvePtr(address, (err, results) => {
|
||||
// This can error as ptr records may not exist for all ips. In this case
|
||||
// fallback to a cname lookup as dns.lookup() does not return the
|
||||
// cname.
|
||||
if (err) {
|
||||
return resolveCname(host, callback);
|
||||
}
|
||||
// If the ptr did not error but had no results, return the host.
|
||||
callback(undefined, results.length > 0 ? results[0] : host);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// The case for forward is just to resolve the cname as dns.lookup()
|
||||
// will not return it.
|
||||
resolveCname(host, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCname(host: string, callback: Callback<string>): void {
|
||||
// Attempt to resolve the host name
|
||||
dns.resolveCname(host, (err, r) => {
|
||||
if (err) return callback(undefined, host);
|
||||
|
||||
// Get the first resolve host id
|
||||
if (r.length > 0) {
|
||||
return callback(undefined, r[0]);
|
||||
}
|
||||
|
||||
callback(undefined, host);
|
||||
});
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
// Resolves the default auth mechanism according to
|
||||
import type { Document } from '../../bson';
|
||||
import { MongoAPIError, MongoMissingCredentialsError } from '../../error';
|
||||
import { emitWarningOnce } from '../../utils';
|
||||
import { GSSAPICanonicalizationValue } from './gssapi';
|
||||
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';
|
||||
|
||||
// https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst
|
||||
function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
|
||||
if (hello) {
|
||||
// If hello contains saslSupportedMechs, use scram-sha-256
|
||||
// if it is available, else scram-sha-1
|
||||
if (Array.isArray(hello.saslSupportedMechs)) {
|
||||
return hello.saslSupportedMechs.includes(AuthMechanism.MONGODB_SCRAM_SHA256)
|
||||
? AuthMechanism.MONGODB_SCRAM_SHA256
|
||||
: AuthMechanism.MONGODB_SCRAM_SHA1;
|
||||
}
|
||||
|
||||
// Fallback to legacy selection method. If wire version >= 3, use scram-sha-1
|
||||
if (hello.maxWireVersion >= 3) {
|
||||
return AuthMechanism.MONGODB_SCRAM_SHA1;
|
||||
}
|
||||
}
|
||||
|
||||
// Default for wireprotocol < 3
|
||||
return AuthMechanism.MONGODB_CR;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface AuthMechanismProperties extends Document {
|
||||
SERVICE_HOST?: string;
|
||||
SERVICE_NAME?: string;
|
||||
SERVICE_REALM?: string;
|
||||
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
|
||||
AWS_SESSION_TOKEN?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface MongoCredentialsOptions {
|
||||
username: string;
|
||||
password: string;
|
||||
source: string;
|
||||
db?: string;
|
||||
mechanism?: AuthMechanism;
|
||||
mechanismProperties: AuthMechanismProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the credentials used by MongoDB
|
||||
* @public
|
||||
*/
|
||||
export class MongoCredentials {
|
||||
/** The username used for authentication */
|
||||
readonly username: string;
|
||||
/** The password used for authentication */
|
||||
readonly password: string;
|
||||
/** The database that the user should authenticate against */
|
||||
readonly source: string;
|
||||
/** The method used to authenticate */
|
||||
readonly mechanism: AuthMechanism;
|
||||
/** Special properties used by some types of auth mechanisms */
|
||||
readonly mechanismProperties: AuthMechanismProperties;
|
||||
|
||||
constructor(options: MongoCredentialsOptions) {
|
||||
this.username = options.username;
|
||||
this.password = options.password;
|
||||
this.source = options.source;
|
||||
if (!this.source && options.db) {
|
||||
this.source = options.db;
|
||||
}
|
||||
this.mechanism = options.mechanism || AuthMechanism.MONGODB_DEFAULT;
|
||||
this.mechanismProperties = options.mechanismProperties || {};
|
||||
|
||||
if (this.mechanism.match(/MONGODB-AWS/i)) {
|
||||
if (!this.username && process.env.AWS_ACCESS_KEY_ID) {
|
||||
this.username = process.env.AWS_ACCESS_KEY_ID;
|
||||
}
|
||||
|
||||
if (!this.password && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
this.password = process.env.AWS_SECRET_ACCESS_KEY;
|
||||
}
|
||||
|
||||
if (
|
||||
this.mechanismProperties.AWS_SESSION_TOKEN == null &&
|
||||
process.env.AWS_SESSION_TOKEN != null
|
||||
) {
|
||||
this.mechanismProperties = {
|
||||
...this.mechanismProperties,
|
||||
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ('gssapiCanonicalizeHostName' in this.mechanismProperties) {
|
||||
emitWarningOnce(
|
||||
'gssapiCanonicalizeHostName is deprecated. Please use CANONICALIZE_HOST_NAME instead.'
|
||||
);
|
||||
this.mechanismProperties.CANONICALIZE_HOST_NAME =
|
||||
this.mechanismProperties.gssapiCanonicalizeHostName;
|
||||
}
|
||||
|
||||
Object.freeze(this.mechanismProperties);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/** Determines if two MongoCredentials objects are equivalent */
|
||||
equals(other: MongoCredentials): boolean {
|
||||
return (
|
||||
this.mechanism === other.mechanism &&
|
||||
this.username === other.username &&
|
||||
this.password === other.password &&
|
||||
this.source === other.source
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the authentication mechanism is set to "default", resolves the authMechanism
|
||||
* based on the server version and server supported sasl mechanisms.
|
||||
*
|
||||
* @param hello - A hello response from the server
|
||||
*/
|
||||
resolveAuthMechanism(hello?: Document): MongoCredentials {
|
||||
// If the mechanism is not "default", then it does not need to be resolved
|
||||
if (this.mechanism.match(/DEFAULT/i)) {
|
||||
return new MongoCredentials({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
source: this.source,
|
||||
mechanism: getDefaultAuthMechanism(hello),
|
||||
mechanismProperties: this.mechanismProperties
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
if (
|
||||
(this.mechanism === AuthMechanism.MONGODB_GSSAPI ||
|
||||
this.mechanism === AuthMechanism.MONGODB_CR ||
|
||||
this.mechanism === AuthMechanism.MONGODB_PLAIN ||
|
||||
this.mechanism === AuthMechanism.MONGODB_SCRAM_SHA1 ||
|
||||
this.mechanism === AuthMechanism.MONGODB_SCRAM_SHA256) &&
|
||||
!this.username
|
||||
) {
|
||||
throw new MongoMissingCredentialsError(`Username required for mechanism '${this.mechanism}'`);
|
||||
}
|
||||
|
||||
if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) {
|
||||
if (this.source != null && this.source !== '$external') {
|
||||
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
|
||||
throw new MongoAPIError(
|
||||
`Invalid source '${this.source}' for mechanism '${this.mechanism}' specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mechanism === AuthMechanism.MONGODB_PLAIN && this.source == null) {
|
||||
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
|
||||
throw new MongoAPIError('PLAIN Authentication Mechanism needs an auth source');
|
||||
}
|
||||
|
||||
if (this.mechanism === AuthMechanism.MONGODB_X509 && this.password != null) {
|
||||
if (this.password === '') {
|
||||
Reflect.set(this, 'password', undefined);
|
||||
return;
|
||||
}
|
||||
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
|
||||
throw new MongoAPIError(`Password not allowed for mechanism MONGODB-X509`);
|
||||
}
|
||||
|
||||
const canonicalization = this.mechanismProperties.CANONICALIZE_HOST_NAME ?? false;
|
||||
if (!Object.values(GSSAPICanonicalizationValue).includes(canonicalization)) {
|
||||
throw new MongoAPIError(`Invalid CANONICALIZE_HOST_NAME value: ${canonicalization}`);
|
||||
}
|
||||
}
|
||||
|
||||
static merge(
|
||||
creds: MongoCredentials | undefined,
|
||||
options: Partial<MongoCredentialsOptions>
|
||||
): MongoCredentials {
|
||||
return new MongoCredentials({
|
||||
username: options.username ?? creds?.username ?? '',
|
||||
password: options.password ?? creds?.password ?? '',
|
||||
mechanism: options.mechanism ?? creds?.mechanism ?? AuthMechanism.MONGODB_DEFAULT,
|
||||
mechanismProperties: options.mechanismProperties ?? creds?.mechanismProperties ?? {},
|
||||
source: options.source ?? options.db ?? creds?.source ?? 'admin'
|
||||
});
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { MongoMissingCredentialsError } from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
|
||||
export class MongoCR extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
const username = credentials.username;
|
||||
const password = credentials.password;
|
||||
const source = credentials.source;
|
||||
connection.command(ns(`${source}.$cmd`), { getnonce: 1 }, undefined, (err, r) => {
|
||||
let nonce = null;
|
||||
let key = null;
|
||||
|
||||
// Get nonce
|
||||
if (err == null) {
|
||||
nonce = r.nonce;
|
||||
|
||||
// Use node md5 generator
|
||||
let md5 = crypto.createHash('md5');
|
||||
|
||||
// Generate keys used for authentication
|
||||
md5.update(`${username}:mongo:${password}`, 'utf8');
|
||||
const hash_password = md5.digest('hex');
|
||||
|
||||
// Final key
|
||||
md5 = crypto.createHash('md5');
|
||||
md5.update(nonce + username + hash_password, 'utf8');
|
||||
key = md5.digest('hex');
|
||||
}
|
||||
|
||||
const authenticateCommand = {
|
||||
authenticate: 1,
|
||||
user: username,
|
||||
nonce,
|
||||
key
|
||||
};
|
||||
|
||||
connection.command(ns(`${source}.$cmd`), authenticateCommand, undefined, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
|
||||
import type { Binary, BSONSerializeOptions } from '../../bson';
|
||||
import * as BSON from '../../bson';
|
||||
import { aws4 } from '../../deps';
|
||||
import {
|
||||
MongoAWSError,
|
||||
MongoCompatibilityError,
|
||||
MongoMissingCredentialsError,
|
||||
MongoRuntimeError
|
||||
} from '../../error';
|
||||
import { Callback, maxWireVersion, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
import { MongoCredentials } from './mongo_credentials';
|
||||
import { AuthMechanism } from './providers';
|
||||
|
||||
const ASCII_N = 110;
|
||||
const AWS_RELATIVE_URI = 'http://169.254.170.2';
|
||||
const AWS_EC2_URI = 'http://169.254.169.254';
|
||||
const AWS_EC2_PATH = '/latest/meta-data/iam/security-credentials';
|
||||
const bsonOptions: BSONSerializeOptions = {
|
||||
promoteLongs: true,
|
||||
promoteValues: true,
|
||||
promoteBuffers: false,
|
||||
bsonRegExp: false
|
||||
};
|
||||
|
||||
interface AWSSaslContinuePayload {
|
||||
a: string;
|
||||
d: string;
|
||||
t?: string;
|
||||
}
|
||||
|
||||
export class MongoDBAWS extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
|
||||
if ('kModuleError' in aws4) {
|
||||
return callback(aws4['kModuleError']);
|
||||
}
|
||||
const { sign } = aws4;
|
||||
|
||||
if (maxWireVersion(connection) < 9) {
|
||||
callback(
|
||||
new MongoCompatibilityError(
|
||||
'MONGODB-AWS authentication requires MongoDB version 4.4 or later'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!credentials.username) {
|
||||
makeTempCredentials(credentials, (err, tempCredentials) => {
|
||||
if (err || !tempCredentials) return callback(err);
|
||||
|
||||
authContext.credentials = tempCredentials;
|
||||
this.auth(authContext, callback);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const accessKeyId = credentials.username;
|
||||
const secretAccessKey = credentials.password;
|
||||
const sessionToken = credentials.mechanismProperties.AWS_SESSION_TOKEN;
|
||||
|
||||
// If all three defined, include sessionToken, else include username and pass, else no credentials
|
||||
const awsCredentials =
|
||||
accessKeyId && secretAccessKey && sessionToken
|
||||
? { accessKeyId, secretAccessKey, sessionToken }
|
||||
: accessKeyId && secretAccessKey
|
||||
? { accessKeyId, secretAccessKey }
|
||||
: undefined;
|
||||
|
||||
const db = credentials.source;
|
||||
crypto.randomBytes(32, (err, nonce) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const saslStart = {
|
||||
saslStart: 1,
|
||||
mechanism: 'MONGODB-AWS',
|
||||
payload: BSON.serialize({ r: nonce, p: ASCII_N }, bsonOptions)
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), saslStart, undefined, (err, res) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const serverResponse = BSON.deserialize(res.payload.buffer, bsonOptions) as {
|
||||
s: Binary;
|
||||
h: string;
|
||||
};
|
||||
const host = serverResponse.h;
|
||||
const serverNonce = serverResponse.s.buffer;
|
||||
if (serverNonce.length !== 64) {
|
||||
callback(
|
||||
// TODO(NODE-3483)
|
||||
new MongoRuntimeError(`Invalid server nonce length ${serverNonce.length}, expected 64`)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverNonce.compare(nonce, 0, nonce.length, 0, nonce.length) !== 0) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError('Server nonce does not begin with client nonce'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (host.length < 1 || host.length > 255 || host.indexOf('..') !== -1) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError(`Server returned an invalid host: "${host}"`));
|
||||
return;
|
||||
}
|
||||
|
||||
const body = 'Action=GetCallerIdentity&Version=2011-06-15';
|
||||
const options = sign(
|
||||
{
|
||||
method: 'POST',
|
||||
host,
|
||||
region: deriveRegion(serverResponse.h),
|
||||
service: 'sts',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': body.length,
|
||||
'X-MongoDB-Server-Nonce': serverNonce.toString('base64'),
|
||||
'X-MongoDB-GS2-CB-Flag': 'n'
|
||||
},
|
||||
path: '/',
|
||||
body
|
||||
},
|
||||
awsCredentials
|
||||
);
|
||||
|
||||
const payload: AWSSaslContinuePayload = {
|
||||
a: options.headers.Authorization,
|
||||
d: options.headers['X-Amz-Date']
|
||||
};
|
||||
if (sessionToken) {
|
||||
payload.t = sessionToken;
|
||||
}
|
||||
|
||||
const saslContinue = {
|
||||
saslContinue: 1,
|
||||
conversationId: 1,
|
||||
payload: BSON.serialize(payload, bsonOptions)
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), saslContinue, undefined, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface AWSTempCredentials {
|
||||
AccessKeyId?: string;
|
||||
SecretAccessKey?: string;
|
||||
Token?: string;
|
||||
RoleArn?: string;
|
||||
Expiration?: Date;
|
||||
}
|
||||
|
||||
function makeTempCredentials(credentials: MongoCredentials, callback: Callback<MongoCredentials>) {
|
||||
function done(creds: AWSTempCredentials) {
|
||||
if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) {
|
||||
callback(
|
||||
new MongoMissingCredentialsError('Could not obtain temporary MONGODB-AWS credentials')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(
|
||||
undefined,
|
||||
new MongoCredentials({
|
||||
username: creds.AccessKeyId,
|
||||
password: creds.SecretAccessKey,
|
||||
source: credentials.source,
|
||||
mechanism: AuthMechanism.MONGODB_AWS,
|
||||
mechanismProperties: {
|
||||
AWS_SESSION_TOKEN: creds.Token
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
|
||||
// is set then drivers MUST assume that it was set by an AWS ECS agent
|
||||
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
|
||||
request(
|
||||
`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`,
|
||||
undefined,
|
||||
(err, res) => {
|
||||
if (err) return callback(err);
|
||||
done(res);
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise assume we are on an EC2 instance
|
||||
|
||||
// get a token
|
||||
request(
|
||||
`${AWS_EC2_URI}/latest/api/token`,
|
||||
{ method: 'PUT', json: false, headers: { 'X-aws-ec2-metadata-token-ttl-seconds': 30 } },
|
||||
(err, token) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// get role name
|
||||
request(
|
||||
`${AWS_EC2_URI}/${AWS_EC2_PATH}`,
|
||||
{ json: false, headers: { 'X-aws-ec2-metadata-token': token } },
|
||||
(err, roleName) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// get temp credentials
|
||||
request(
|
||||
`${AWS_EC2_URI}/${AWS_EC2_PATH}/${roleName}`,
|
||||
{ headers: { 'X-aws-ec2-metadata-token': token } },
|
||||
(err, creds) => {
|
||||
if (err) return callback(err);
|
||||
done(creds);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deriveRegion(host: string) {
|
||||
const parts = host.split('.');
|
||||
if (parts.length === 1 || parts[1] === 'amazonaws') {
|
||||
return 'us-east-1';
|
||||
}
|
||||
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
json?: boolean;
|
||||
method?: string;
|
||||
timeout?: number;
|
||||
headers?: http.OutgoingHttpHeaders;
|
||||
}
|
||||
|
||||
function request(uri: string, _options: RequestOptions | undefined, callback: Callback) {
|
||||
const options = Object.assign(
|
||||
{
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
json: true
|
||||
},
|
||||
url.parse(uri),
|
||||
_options
|
||||
);
|
||||
|
||||
const req = http.request(options, res => {
|
||||
res.setEncoding('utf8');
|
||||
|
||||
let data = '';
|
||||
res.on('data', d => (data += d));
|
||||
res.on('end', () => {
|
||||
if (options.json === false) {
|
||||
callback(undefined, data);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
callback(undefined, parsed);
|
||||
} catch (err) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError(`Invalid JSON response: "${data}"`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy(new MongoAWSError(`AWS request to ${uri} timed out after ${options.timeout} ms`));
|
||||
});
|
||||
|
||||
req.on('error', err => callback(err));
|
||||
req.end();
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Binary } from '../../bson';
|
||||
import { MongoMissingCredentialsError } from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
|
||||
export class Plain extends AuthProvider {
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
const username = credentials.username;
|
||||
const password = credentials.password;
|
||||
|
||||
const payload = new Binary(Buffer.from(`\x00${username}\x00${password}`));
|
||||
const command = {
|
||||
saslStart: 1,
|
||||
mechanism: 'PLAIN',
|
||||
payload: payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
connection.command(ns('$external.$cmd'), command, undefined, callback);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/** @public */
|
||||
export const AuthMechanism = Object.freeze({
|
||||
MONGODB_AWS: 'MONGODB-AWS',
|
||||
MONGODB_CR: 'MONGODB-CR',
|
||||
MONGODB_DEFAULT: 'DEFAULT',
|
||||
MONGODB_GSSAPI: 'GSSAPI',
|
||||
MONGODB_PLAIN: 'PLAIN',
|
||||
MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1',
|
||||
MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256',
|
||||
MONGODB_X509: 'MONGODB-X509'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type AuthMechanism = typeof AuthMechanism[keyof typeof AuthMechanism];
|
||||
|
||||
/** @internal */
|
||||
export const AUTH_MECHS_AUTH_SRC_EXTERNAL = new Set<AuthMechanism>([
|
||||
AuthMechanism.MONGODB_GSSAPI,
|
||||
AuthMechanism.MONGODB_AWS,
|
||||
AuthMechanism.MONGODB_X509
|
||||
]);
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { Binary, Document } from '../../bson';
|
||||
import { saslprep } from '../../deps';
|
||||
import {
|
||||
AnyError,
|
||||
MongoInvalidArgumentError,
|
||||
MongoMissingCredentialsError,
|
||||
MongoRuntimeError,
|
||||
MongoServerError
|
||||
} from '../../error';
|
||||
import { Callback, emitWarning, ns } from '../../utils';
|
||||
import type { HandshakeDocument } from '../connect';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
import type { MongoCredentials } from './mongo_credentials';
|
||||
import { AuthMechanism } from './providers';
|
||||
|
||||
type CryptoMethod = 'sha1' | 'sha256';
|
||||
|
||||
class ScramSHA extends AuthProvider {
|
||||
cryptoMethod: CryptoMethod;
|
||||
constructor(cryptoMethod: CryptoMethod) {
|
||||
super();
|
||||
this.cryptoMethod = cryptoMethod || 'sha1';
|
||||
}
|
||||
|
||||
override prepare(handshakeDoc: HandshakeDocument, authContext: AuthContext, callback: Callback) {
|
||||
const cryptoMethod = this.cryptoMethod;
|
||||
const credentials = authContext.credentials;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
if (cryptoMethod === 'sha256' && saslprep == null) {
|
||||
emitWarning('Warning: no saslprep library specified. Passwords will not be sanitized');
|
||||
}
|
||||
|
||||
crypto.randomBytes(24, (err, nonce) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// store the nonce for later use
|
||||
Object.assign(authContext, { nonce });
|
||||
|
||||
const request = Object.assign({}, handshakeDoc, {
|
||||
speculativeAuthenticate: Object.assign(makeFirstMessage(cryptoMethod, credentials, nonce), {
|
||||
db: credentials.source
|
||||
})
|
||||
});
|
||||
|
||||
callback(undefined, request);
|
||||
});
|
||||
}
|
||||
|
||||
override auth(authContext: AuthContext, callback: Callback) {
|
||||
const response = authContext.response;
|
||||
if (response && response.speculativeAuthenticate) {
|
||||
continueScramConversation(
|
||||
this.cryptoMethod,
|
||||
response.speculativeAuthenticate,
|
||||
authContext,
|
||||
callback
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
executeScram(this.cryptoMethod, authContext, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUsername(username: string) {
|
||||
return username.replace('=', '=3D').replace(',', '=2C');
|
||||
}
|
||||
|
||||
function clientFirstMessageBare(username: string, nonce: Buffer) {
|
||||
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
|
||||
// Since the username is not sasl-prep-d, we need to do this here.
|
||||
return Buffer.concat([
|
||||
Buffer.from('n=', 'utf8'),
|
||||
Buffer.from(username, 'utf8'),
|
||||
Buffer.from(',r=', 'utf8'),
|
||||
Buffer.from(nonce.toString('base64'), 'utf8')
|
||||
]);
|
||||
}
|
||||
|
||||
function makeFirstMessage(
|
||||
cryptoMethod: CryptoMethod,
|
||||
credentials: MongoCredentials,
|
||||
nonce: Buffer
|
||||
) {
|
||||
const username = cleanUsername(credentials.username);
|
||||
const mechanism =
|
||||
cryptoMethod === 'sha1' ? AuthMechanism.MONGODB_SCRAM_SHA1 : AuthMechanism.MONGODB_SCRAM_SHA256;
|
||||
|
||||
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
|
||||
// Since the username is not sasl-prep-d, we need to do this here.
|
||||
return {
|
||||
saslStart: 1,
|
||||
mechanism,
|
||||
payload: new Binary(
|
||||
Buffer.concat([Buffer.from('n,,', 'utf8'), clientFirstMessageBare(username, nonce)])
|
||||
),
|
||||
autoAuthorize: 1,
|
||||
options: { skipEmptyExchange: true }
|
||||
};
|
||||
}
|
||||
|
||||
function executeScram(cryptoMethod: CryptoMethod, authContext: AuthContext, callback: Callback) {
|
||||
const { connection, credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
if (!authContext.nonce) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError('AuthContext must contain a valid nonce property')
|
||||
);
|
||||
}
|
||||
const nonce = authContext.nonce;
|
||||
const db = credentials.source;
|
||||
|
||||
const saslStartCmd = makeFirstMessage(cryptoMethod, credentials, nonce);
|
||||
connection.command(ns(`${db}.$cmd`), saslStartCmd, undefined, (_err, result) => {
|
||||
const err = resolveError(_err, result);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
continueScramConversation(cryptoMethod, result, authContext, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function continueScramConversation(
|
||||
cryptoMethod: CryptoMethod,
|
||||
response: Document,
|
||||
authContext: AuthContext,
|
||||
callback: Callback
|
||||
) {
|
||||
const connection = authContext.connection;
|
||||
const credentials = authContext.credentials;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
if (!authContext.nonce) {
|
||||
return callback(new MongoInvalidArgumentError('Unable to continue SCRAM without valid nonce'));
|
||||
}
|
||||
const nonce = authContext.nonce;
|
||||
|
||||
const db = credentials.source;
|
||||
const username = cleanUsername(credentials.username);
|
||||
const password = credentials.password;
|
||||
|
||||
let processedPassword;
|
||||
if (cryptoMethod === 'sha256') {
|
||||
processedPassword = 'kModuleError' in saslprep ? password : saslprep(password);
|
||||
} else {
|
||||
try {
|
||||
processedPassword = passwordDigest(username, password);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = Buffer.isBuffer(response.payload)
|
||||
? new Binary(response.payload)
|
||||
: response.payload;
|
||||
const dict = parsePayload(payload.value());
|
||||
|
||||
const iterations = parseInt(dict.i, 10);
|
||||
if (iterations && iterations < 4096) {
|
||||
callback(
|
||||
// TODO(NODE-3483)
|
||||
new MongoRuntimeError(`Server returned an invalid iteration count ${iterations}`),
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const salt = dict.s;
|
||||
const rnonce = dict.r;
|
||||
if (rnonce.startsWith('nonce')) {
|
||||
// TODO(NODE-3483)
|
||||
callback(new MongoRuntimeError(`Server returned an invalid nonce: ${rnonce}`), false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up start of proof
|
||||
const withoutProof = `c=biws,r=${rnonce}`;
|
||||
const saltedPassword = HI(
|
||||
processedPassword,
|
||||
Buffer.from(salt, 'base64'),
|
||||
iterations,
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
|
||||
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
|
||||
const storedKey = H(cryptoMethod, clientKey);
|
||||
const authMessage = [clientFirstMessageBare(username, nonce), payload.value(), withoutProof].join(
|
||||
','
|
||||
);
|
||||
|
||||
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
|
||||
const clientProof = `p=${xor(clientKey, clientSignature)}`;
|
||||
const clientFinal = [withoutProof, clientProof].join(',');
|
||||
|
||||
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
|
||||
const saslContinueCmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: response.conversationId,
|
||||
payload: new Binary(Buffer.from(clientFinal))
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), saslContinueCmd, undefined, (_err, r) => {
|
||||
const err = resolveError(_err, r);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const parsedResponse = parsePayload(r.payload.value());
|
||||
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
|
||||
callback(new MongoRuntimeError('Server returned an invalid signature'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!r || r.done !== false) {
|
||||
return callback(err, r);
|
||||
}
|
||||
|
||||
const retrySaslContinueCmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: r.conversationId,
|
||||
payload: Buffer.alloc(0)
|
||||
};
|
||||
|
||||
connection.command(ns(`${db}.$cmd`), retrySaslContinueCmd, undefined, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function parsePayload(payload: string) {
|
||||
const dict: Document = {};
|
||||
const parts = payload.split(',');
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const valueParts = parts[i].split('=');
|
||||
dict[valueParts[0]] = valueParts[1];
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
function passwordDigest(username: string, password: string) {
|
||||
if (typeof username !== 'string') {
|
||||
throw new MongoInvalidArgumentError('Username must be a string');
|
||||
}
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
throw new MongoInvalidArgumentError('Password must be a string');
|
||||
}
|
||||
|
||||
if (password.length === 0) {
|
||||
throw new MongoInvalidArgumentError('Password cannot be empty');
|
||||
}
|
||||
|
||||
let md5: crypto.Hash;
|
||||
try {
|
||||
md5 = crypto.createHash('md5');
|
||||
} catch (err) {
|
||||
if (crypto.getFips()) {
|
||||
// This error is (slightly) more helpful than what comes from OpenSSL directly, e.g.
|
||||
// 'Error: error:060800C8:digital envelope routines:EVP_DigestInit_ex:disabled for FIPS'
|
||||
throw new Error('Auth mechanism SCRAM-SHA-1 is not supported in FIPS mode');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
md5.update(`${username}:mongo:${password}`, 'utf8');
|
||||
return md5.digest('hex');
|
||||
}
|
||||
|
||||
// XOR two buffers
|
||||
function xor(a: Buffer, b: Buffer) {
|
||||
if (!Buffer.isBuffer(a)) {
|
||||
a = Buffer.from(a);
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(b)) {
|
||||
b = Buffer.from(b);
|
||||
}
|
||||
|
||||
const length = Math.max(a.length, b.length);
|
||||
const res = [];
|
||||
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
res.push(a[i] ^ b[i]);
|
||||
}
|
||||
|
||||
return Buffer.from(res).toString('base64');
|
||||
}
|
||||
|
||||
function H(method: CryptoMethod, text: Buffer) {
|
||||
return crypto.createHash(method).update(text).digest();
|
||||
}
|
||||
|
||||
function HMAC(method: CryptoMethod, key: Buffer, text: Buffer | string) {
|
||||
return crypto.createHmac(method, key).update(text).digest();
|
||||
}
|
||||
|
||||
interface HICache {
|
||||
[key: string]: Buffer;
|
||||
}
|
||||
|
||||
let _hiCache: HICache = {};
|
||||
let _hiCacheCount = 0;
|
||||
function _hiCachePurge() {
|
||||
_hiCache = {};
|
||||
_hiCacheCount = 0;
|
||||
}
|
||||
|
||||
const hiLengthMap = {
|
||||
sha256: 32,
|
||||
sha1: 20
|
||||
};
|
||||
|
||||
function HI(data: string, salt: Buffer, iterations: number, cryptoMethod: CryptoMethod) {
|
||||
// omit the work if already generated
|
||||
const key = [data, salt.toString('base64'), iterations].join('_');
|
||||
if (_hiCache[key] != null) {
|
||||
return _hiCache[key];
|
||||
}
|
||||
|
||||
// generate the salt
|
||||
const saltedData = crypto.pbkdf2Sync(
|
||||
data,
|
||||
salt,
|
||||
iterations,
|
||||
hiLengthMap[cryptoMethod],
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
|
||||
if (_hiCacheCount >= 200) {
|
||||
_hiCachePurge();
|
||||
}
|
||||
|
||||
_hiCache[key] = saltedData;
|
||||
_hiCacheCount += 1;
|
||||
return saltedData;
|
||||
}
|
||||
|
||||
function compareDigest(lhs: Buffer, rhs: Uint8Array) {
|
||||
if (lhs.length !== rhs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof crypto.timingSafeEqual === 'function') {
|
||||
return crypto.timingSafeEqual(lhs, rhs);
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
for (let i = 0; i < lhs.length; i++) {
|
||||
result |= lhs[i] ^ rhs[i];
|
||||
}
|
||||
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
function resolveError(err?: AnyError, result?: Document) {
|
||||
if (err) return err;
|
||||
if (result) {
|
||||
if (result.$err || result.errmsg) return new MongoServerError(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export class ScramSHA1 extends ScramSHA {
|
||||
constructor() {
|
||||
super('sha1');
|
||||
}
|
||||
}
|
||||
|
||||
export class ScramSHA256 extends ScramSHA {
|
||||
constructor() {
|
||||
super('sha256');
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import type { Document } from '../../bson';
|
||||
import { MongoMissingCredentialsError } from '../../error';
|
||||
import { Callback, ns } from '../../utils';
|
||||
import type { HandshakeDocument } from '../connect';
|
||||
import { AuthContext, AuthProvider } from './auth_provider';
|
||||
import type { MongoCredentials } from './mongo_credentials';
|
||||
|
||||
export class X509 extends AuthProvider {
|
||||
override prepare(
|
||||
handshakeDoc: HandshakeDocument,
|
||||
authContext: AuthContext,
|
||||
callback: Callback
|
||||
): void {
|
||||
const { credentials } = authContext;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
Object.assign(handshakeDoc, {
|
||||
speculativeAuthenticate: x509AuthenticateCommand(credentials)
|
||||
});
|
||||
|
||||
callback(undefined, handshakeDoc);
|
||||
}
|
||||
|
||||
override auth(authContext: AuthContext, callback: Callback): void {
|
||||
const connection = authContext.connection;
|
||||
const credentials = authContext.credentials;
|
||||
if (!credentials) {
|
||||
return callback(new MongoMissingCredentialsError('AuthContext must provide credentials.'));
|
||||
}
|
||||
const response = authContext.response;
|
||||
|
||||
if (response && response.speculativeAuthenticate) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
connection.command(
|
||||
ns('$external.$cmd'),
|
||||
x509AuthenticateCommand(credentials),
|
||||
undefined,
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function x509AuthenticateCommand(credentials: MongoCredentials) {
|
||||
const command: Document = { authenticate: 1, mechanism: 'MONGODB-X509' };
|
||||
if (credentials.username) {
|
||||
command.user = credentials.username;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
import type { Document, ObjectId } from '../bson';
|
||||
import { LEGACY_HELLO_COMMAND, LEGACY_HELLO_COMMAND_CAMEL_CASE } from '../constants';
|
||||
import { calculateDurationInMs, deepCopy } from '../utils';
|
||||
import { Msg, WriteProtocolMessageType } from './commands';
|
||||
import type { Connection } from './connection';
|
||||
|
||||
/**
|
||||
* An event indicating the start of a given
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class CommandStartedEvent {
|
||||
commandObj?: Document;
|
||||
requestId: number;
|
||||
databaseName: string;
|
||||
commandName: string;
|
||||
command: Document;
|
||||
address: string;
|
||||
connectionId?: string | number;
|
||||
serviceId?: ObjectId;
|
||||
|
||||
/**
|
||||
* Create a started event
|
||||
*
|
||||
* @internal
|
||||
* @param pool - the pool that originated the command
|
||||
* @param command - the command
|
||||
*/
|
||||
constructor(connection: Connection, command: WriteProtocolMessageType) {
|
||||
const cmd = extractCommand(command);
|
||||
const commandName = extractCommandName(cmd);
|
||||
const { address, connectionId, serviceId } = extractConnectionDetails(connection);
|
||||
|
||||
// TODO: remove in major revision, this is not spec behavior
|
||||
if (SENSITIVE_COMMANDS.has(commandName)) {
|
||||
this.commandObj = {};
|
||||
this.commandObj[commandName] = true;
|
||||
}
|
||||
|
||||
this.address = address;
|
||||
this.connectionId = connectionId;
|
||||
this.serviceId = serviceId;
|
||||
this.requestId = command.requestId;
|
||||
this.databaseName = databaseName(command);
|
||||
this.commandName = commandName;
|
||||
this.command = maybeRedact(commandName, cmd, cmd);
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
get hasServiceId(): boolean {
|
||||
return !!this.serviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating the success of a given command
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class CommandSucceededEvent {
|
||||
address: string;
|
||||
connectionId?: string | number;
|
||||
requestId: number;
|
||||
duration: number;
|
||||
commandName: string;
|
||||
reply: unknown;
|
||||
serviceId?: ObjectId;
|
||||
|
||||
/**
|
||||
* Create a succeeded event
|
||||
*
|
||||
* @internal
|
||||
* @param pool - the pool that originated the command
|
||||
* @param command - the command
|
||||
* @param reply - the reply for this command from the server
|
||||
* @param started - a high resolution tuple timestamp of when the command was first sent, to calculate duration
|
||||
*/
|
||||
constructor(
|
||||
connection: Connection,
|
||||
command: WriteProtocolMessageType,
|
||||
reply: Document | undefined,
|
||||
started: number
|
||||
) {
|
||||
const cmd = extractCommand(command);
|
||||
const commandName = extractCommandName(cmd);
|
||||
const { address, connectionId, serviceId } = extractConnectionDetails(connection);
|
||||
|
||||
this.address = address;
|
||||
this.connectionId = connectionId;
|
||||
this.serviceId = serviceId;
|
||||
this.requestId = command.requestId;
|
||||
this.commandName = commandName;
|
||||
this.duration = calculateDurationInMs(started);
|
||||
this.reply = maybeRedact(commandName, cmd, extractReply(command, reply));
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
get hasServiceId(): boolean {
|
||||
return !!this.serviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating the failure of a given command
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class CommandFailedEvent {
|
||||
address: string;
|
||||
connectionId?: string | number;
|
||||
requestId: number;
|
||||
duration: number;
|
||||
commandName: string;
|
||||
failure: Error;
|
||||
serviceId?: ObjectId;
|
||||
|
||||
/**
|
||||
* Create a failure event
|
||||
*
|
||||
* @internal
|
||||
* @param pool - the pool that originated the command
|
||||
* @param command - the command
|
||||
* @param error - the generated error or a server error response
|
||||
* @param started - a high resolution tuple timestamp of when the command was first sent, to calculate duration
|
||||
*/
|
||||
constructor(
|
||||
connection: Connection,
|
||||
command: WriteProtocolMessageType,
|
||||
error: Error | Document,
|
||||
started: number
|
||||
) {
|
||||
const cmd = extractCommand(command);
|
||||
const commandName = extractCommandName(cmd);
|
||||
const { address, connectionId, serviceId } = extractConnectionDetails(connection);
|
||||
|
||||
this.address = address;
|
||||
this.connectionId = connectionId;
|
||||
this.serviceId = serviceId;
|
||||
|
||||
this.requestId = command.requestId;
|
||||
this.commandName = commandName;
|
||||
this.duration = calculateDurationInMs(started);
|
||||
this.failure = maybeRedact(commandName, cmd, error) as Error;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
get hasServiceId(): boolean {
|
||||
return !!this.serviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/** Commands that we want to redact because of the sensitive nature of their contents */
|
||||
const SENSITIVE_COMMANDS = new Set([
|
||||
'authenticate',
|
||||
'saslStart',
|
||||
'saslContinue',
|
||||
'getnonce',
|
||||
'createUser',
|
||||
'updateUser',
|
||||
'copydbgetnonce',
|
||||
'copydbsaslstart',
|
||||
'copydb'
|
||||
]);
|
||||
|
||||
const HELLO_COMMANDS = new Set(['hello', LEGACY_HELLO_COMMAND, LEGACY_HELLO_COMMAND_CAMEL_CASE]);
|
||||
|
||||
// helper methods
|
||||
const extractCommandName = (commandDoc: Document) => Object.keys(commandDoc)[0];
|
||||
const namespace = (command: WriteProtocolMessageType) => command.ns;
|
||||
const databaseName = (command: WriteProtocolMessageType) => command.ns.split('.')[0];
|
||||
const collectionName = (command: WriteProtocolMessageType) => command.ns.split('.')[1];
|
||||
const maybeRedact = (commandName: string, commandDoc: Document, result: Error | Document) =>
|
||||
SENSITIVE_COMMANDS.has(commandName) ||
|
||||
(HELLO_COMMANDS.has(commandName) && commandDoc.speculativeAuthenticate)
|
||||
? {}
|
||||
: result;
|
||||
|
||||
const LEGACY_FIND_QUERY_MAP: { [key: string]: string } = {
|
||||
$query: 'filter',
|
||||
$orderby: 'sort',
|
||||
$hint: 'hint',
|
||||
$comment: 'comment',
|
||||
$maxScan: 'maxScan',
|
||||
$max: 'max',
|
||||
$min: 'min',
|
||||
$returnKey: 'returnKey',
|
||||
$showDiskLoc: 'showRecordId',
|
||||
$maxTimeMS: 'maxTimeMS',
|
||||
$snapshot: 'snapshot'
|
||||
};
|
||||
|
||||
const LEGACY_FIND_OPTIONS_MAP = {
|
||||
numberToSkip: 'skip',
|
||||
numberToReturn: 'batchSize',
|
||||
returnFieldSelector: 'projection'
|
||||
} as const;
|
||||
|
||||
const OP_QUERY_KEYS = [
|
||||
'tailable',
|
||||
'oplogReplay',
|
||||
'noCursorTimeout',
|
||||
'awaitData',
|
||||
'partial',
|
||||
'exhaust'
|
||||
] as const;
|
||||
|
||||
/** Extract the actual command from the query, possibly up-converting if it's a legacy format */
|
||||
function extractCommand(command: WriteProtocolMessageType): Document {
|
||||
if (command instanceof Msg) {
|
||||
return deepCopy(command.command);
|
||||
}
|
||||
|
||||
if (command.query?.$query) {
|
||||
let result: Document;
|
||||
if (command.ns === 'admin.$cmd') {
|
||||
// up-convert legacy command
|
||||
result = Object.assign({}, command.query.$query);
|
||||
} else {
|
||||
// up-convert legacy find command
|
||||
result = { find: collectionName(command) };
|
||||
Object.keys(LEGACY_FIND_QUERY_MAP).forEach(key => {
|
||||
if (command.query[key] != null) {
|
||||
result[LEGACY_FIND_QUERY_MAP[key]] = deepCopy(command.query[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
|
||||
const legacyKey = key as keyof typeof LEGACY_FIND_OPTIONS_MAP;
|
||||
if (command[legacyKey] != null) {
|
||||
result[LEGACY_FIND_OPTIONS_MAP[legacyKey]] = deepCopy(command[legacyKey]);
|
||||
}
|
||||
});
|
||||
|
||||
OP_QUERY_KEYS.forEach(key => {
|
||||
if (command[key]) {
|
||||
result[key] = command[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (command.pre32Limit != null) {
|
||||
result.limit = command.pre32Limit;
|
||||
}
|
||||
|
||||
if (command.query.$explain) {
|
||||
return { explain: result };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const clonedQuery: Record<string, unknown> = {};
|
||||
const clonedCommand: Record<string, unknown> = {};
|
||||
if (command.query) {
|
||||
for (const k in command.query) {
|
||||
clonedQuery[k] = deepCopy(command.query[k]);
|
||||
}
|
||||
clonedCommand.query = clonedQuery;
|
||||
}
|
||||
|
||||
for (const k in command) {
|
||||
if (k === 'query') continue;
|
||||
clonedCommand[k] = deepCopy((command as unknown as Record<string, unknown>)[k]);
|
||||
}
|
||||
return command.query ? clonedQuery : clonedCommand;
|
||||
}
|
||||
|
||||
function extractReply(command: WriteProtocolMessageType, reply?: Document) {
|
||||
if (!reply) {
|
||||
return reply;
|
||||
}
|
||||
|
||||
if (command instanceof Msg) {
|
||||
return deepCopy(reply.result ? reply.result : reply);
|
||||
}
|
||||
|
||||
// is this a legacy find command?
|
||||
if (command.query && command.query.$query != null) {
|
||||
return {
|
||||
ok: 1,
|
||||
cursor: {
|
||||
id: deepCopy(reply.cursorId),
|
||||
ns: namespace(command),
|
||||
firstBatch: deepCopy(reply.documents)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return deepCopy(reply.result ? reply.result : reply);
|
||||
}
|
||||
|
||||
function extractConnectionDetails(connection: Connection) {
|
||||
let connectionId;
|
||||
if ('id' in connection) {
|
||||
connectionId = connection.id;
|
||||
}
|
||||
return {
|
||||
address: connection.address,
|
||||
serviceId: connection.serviceId,
|
||||
connectionId
|
||||
};
|
||||
}
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
import type { BSONSerializeOptions, Document, Long } from '../bson';
|
||||
import * as BSON from '../bson';
|
||||
import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
|
||||
import { ReadPreference } from '../read_preference';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { databaseNamespace } from '../utils';
|
||||
import type { CommandOptions } from './connection';
|
||||
import { OP_MSG, OP_QUERY } from './wire_protocol/constants';
|
||||
|
||||
// Incrementing request id
|
||||
let _requestId = 0;
|
||||
|
||||
// Query flags
|
||||
const OPTS_TAILABLE_CURSOR = 2;
|
||||
const OPTS_SECONDARY = 4;
|
||||
const OPTS_OPLOG_REPLAY = 8;
|
||||
const OPTS_NO_CURSOR_TIMEOUT = 16;
|
||||
const OPTS_AWAIT_DATA = 32;
|
||||
const OPTS_EXHAUST = 64;
|
||||
const OPTS_PARTIAL = 128;
|
||||
|
||||
// Response flags
|
||||
const CURSOR_NOT_FOUND = 1;
|
||||
const QUERY_FAILURE = 2;
|
||||
const SHARD_CONFIG_STALE = 4;
|
||||
const AWAIT_CAPABLE = 8;
|
||||
|
||||
/** @internal */
|
||||
export type WriteProtocolMessageType = Query | Msg;
|
||||
|
||||
/** @internal */
|
||||
export interface OpQueryOptions extends CommandOptions {
|
||||
socketTimeoutMS?: number;
|
||||
session?: ClientSession;
|
||||
documentsReturnedIn?: string;
|
||||
numberToSkip?: number;
|
||||
numberToReturn?: number;
|
||||
returnFieldSelector?: Document;
|
||||
pre32Limit?: number;
|
||||
serializeFunctions?: boolean;
|
||||
ignoreUndefined?: boolean;
|
||||
maxBsonSize?: number;
|
||||
checkKeys?: boolean;
|
||||
secondaryOk?: boolean;
|
||||
|
||||
requestId?: number;
|
||||
moreToCome?: boolean;
|
||||
exhaustAllowed?: boolean;
|
||||
readPreference?: ReadPreference;
|
||||
}
|
||||
|
||||
/**************************************************************
|
||||
* QUERY
|
||||
**************************************************************/
|
||||
/** @internal */
|
||||
export class Query {
|
||||
ns: string;
|
||||
query: Document;
|
||||
numberToSkip: number;
|
||||
numberToReturn: number;
|
||||
returnFieldSelector?: Document;
|
||||
requestId: number;
|
||||
pre32Limit?: number;
|
||||
serializeFunctions: boolean;
|
||||
ignoreUndefined: boolean;
|
||||
maxBsonSize: number;
|
||||
checkKeys: boolean;
|
||||
batchSize: number;
|
||||
tailable: boolean;
|
||||
secondaryOk: boolean;
|
||||
oplogReplay: boolean;
|
||||
noCursorTimeout: boolean;
|
||||
awaitData: boolean;
|
||||
exhaust: boolean;
|
||||
partial: boolean;
|
||||
documentsReturnedIn?: string;
|
||||
|
||||
constructor(ns: string, query: Document, options: OpQueryOptions) {
|
||||
// Basic options needed to be passed in
|
||||
// TODO(NODE-3483): Replace with MongoCommandError
|
||||
if (ns == null) throw new MongoRuntimeError('Namespace must be specified for query');
|
||||
// TODO(NODE-3483): Replace with MongoCommandError
|
||||
if (query == null) throw new MongoRuntimeError('A query document must be specified for query');
|
||||
|
||||
// Validate that we are not passing 0x00 in the collection name
|
||||
if (ns.indexOf('\x00') !== -1) {
|
||||
// TODO(NODE-3483): Use MongoNamespace static method
|
||||
throw new MongoRuntimeError('Namespace cannot contain a null character');
|
||||
}
|
||||
|
||||
// Basic options
|
||||
this.ns = ns;
|
||||
this.query = query;
|
||||
|
||||
// Additional options
|
||||
this.numberToSkip = options.numberToSkip || 0;
|
||||
this.numberToReturn = options.numberToReturn || 0;
|
||||
this.returnFieldSelector = options.returnFieldSelector || undefined;
|
||||
this.requestId = Query.getRequestId();
|
||||
|
||||
// special case for pre-3.2 find commands, delete ASAP
|
||||
this.pre32Limit = options.pre32Limit;
|
||||
|
||||
// Serialization option
|
||||
this.serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
this.ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
this.maxBsonSize = options.maxBsonSize || 1024 * 1024 * 16;
|
||||
this.checkKeys = typeof options.checkKeys === 'boolean' ? options.checkKeys : false;
|
||||
this.batchSize = this.numberToReturn;
|
||||
|
||||
// Flags
|
||||
this.tailable = false;
|
||||
this.secondaryOk = typeof options.secondaryOk === 'boolean' ? options.secondaryOk : false;
|
||||
this.oplogReplay = false;
|
||||
this.noCursorTimeout = false;
|
||||
this.awaitData = false;
|
||||
this.exhaust = false;
|
||||
this.partial = false;
|
||||
}
|
||||
|
||||
/** Assign next request Id. */
|
||||
incRequestId(): void {
|
||||
this.requestId = _requestId++;
|
||||
}
|
||||
|
||||
/** Peek next request Id. */
|
||||
nextRequestId(): number {
|
||||
return _requestId + 1;
|
||||
}
|
||||
|
||||
/** Increment then return next request Id. */
|
||||
static getRequestId(): number {
|
||||
return ++_requestId;
|
||||
}
|
||||
|
||||
// Uses a single allocated buffer for the process, avoiding multiple memory allocations
|
||||
toBin(): Buffer[] {
|
||||
const buffers = [];
|
||||
let projection = null;
|
||||
|
||||
// Set up the flags
|
||||
let flags = 0;
|
||||
if (this.tailable) {
|
||||
flags |= OPTS_TAILABLE_CURSOR;
|
||||
}
|
||||
|
||||
if (this.secondaryOk) {
|
||||
flags |= OPTS_SECONDARY;
|
||||
}
|
||||
|
||||
if (this.oplogReplay) {
|
||||
flags |= OPTS_OPLOG_REPLAY;
|
||||
}
|
||||
|
||||
if (this.noCursorTimeout) {
|
||||
flags |= OPTS_NO_CURSOR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (this.awaitData) {
|
||||
flags |= OPTS_AWAIT_DATA;
|
||||
}
|
||||
|
||||
if (this.exhaust) {
|
||||
flags |= OPTS_EXHAUST;
|
||||
}
|
||||
|
||||
if (this.partial) {
|
||||
flags |= OPTS_PARTIAL;
|
||||
}
|
||||
|
||||
// If batchSize is different to this.numberToReturn
|
||||
if (this.batchSize !== this.numberToReturn) this.numberToReturn = this.batchSize;
|
||||
|
||||
// Allocate write protocol header buffer
|
||||
const header = Buffer.alloc(
|
||||
4 * 4 + // Header
|
||||
4 + // Flags
|
||||
Buffer.byteLength(this.ns) +
|
||||
1 + // namespace
|
||||
4 + // numberToSkip
|
||||
4 // numberToReturn
|
||||
);
|
||||
|
||||
// Add header to buffers
|
||||
buffers.push(header);
|
||||
|
||||
// Serialize the query
|
||||
const query = BSON.serialize(this.query, {
|
||||
checkKeys: this.checkKeys,
|
||||
serializeFunctions: this.serializeFunctions,
|
||||
ignoreUndefined: this.ignoreUndefined
|
||||
});
|
||||
|
||||
// Add query document
|
||||
buffers.push(query);
|
||||
|
||||
if (this.returnFieldSelector && Object.keys(this.returnFieldSelector).length > 0) {
|
||||
// Serialize the projection document
|
||||
projection = BSON.serialize(this.returnFieldSelector, {
|
||||
checkKeys: this.checkKeys,
|
||||
serializeFunctions: this.serializeFunctions,
|
||||
ignoreUndefined: this.ignoreUndefined
|
||||
});
|
||||
// Add projection document
|
||||
buffers.push(projection);
|
||||
}
|
||||
|
||||
// Total message size
|
||||
const totalLength = header.length + query.length + (projection ? projection.length : 0);
|
||||
|
||||
// Set up the index
|
||||
let index = 4;
|
||||
|
||||
// Write total document length
|
||||
header[3] = (totalLength >> 24) & 0xff;
|
||||
header[2] = (totalLength >> 16) & 0xff;
|
||||
header[1] = (totalLength >> 8) & 0xff;
|
||||
header[0] = totalLength & 0xff;
|
||||
|
||||
// Write header information requestId
|
||||
header[index + 3] = (this.requestId >> 24) & 0xff;
|
||||
header[index + 2] = (this.requestId >> 16) & 0xff;
|
||||
header[index + 1] = (this.requestId >> 8) & 0xff;
|
||||
header[index] = this.requestId & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information responseTo
|
||||
header[index + 3] = (0 >> 24) & 0xff;
|
||||
header[index + 2] = (0 >> 16) & 0xff;
|
||||
header[index + 1] = (0 >> 8) & 0xff;
|
||||
header[index] = 0 & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information OP_QUERY
|
||||
header[index + 3] = (OP_QUERY >> 24) & 0xff;
|
||||
header[index + 2] = (OP_QUERY >> 16) & 0xff;
|
||||
header[index + 1] = (OP_QUERY >> 8) & 0xff;
|
||||
header[index] = OP_QUERY & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information flags
|
||||
header[index + 3] = (flags >> 24) & 0xff;
|
||||
header[index + 2] = (flags >> 16) & 0xff;
|
||||
header[index + 1] = (flags >> 8) & 0xff;
|
||||
header[index] = flags & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write collection name
|
||||
index = index + header.write(this.ns, index, 'utf8') + 1;
|
||||
header[index - 1] = 0;
|
||||
|
||||
// Write header information flags numberToSkip
|
||||
header[index + 3] = (this.numberToSkip >> 24) & 0xff;
|
||||
header[index + 2] = (this.numberToSkip >> 16) & 0xff;
|
||||
header[index + 1] = (this.numberToSkip >> 8) & 0xff;
|
||||
header[index] = this.numberToSkip & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information flags numberToReturn
|
||||
header[index + 3] = (this.numberToReturn >> 24) & 0xff;
|
||||
header[index + 2] = (this.numberToReturn >> 16) & 0xff;
|
||||
header[index + 1] = (this.numberToReturn >> 8) & 0xff;
|
||||
header[index] = this.numberToReturn & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Return the buffers
|
||||
return buffers;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface MessageHeader {
|
||||
length: number;
|
||||
requestId: number;
|
||||
responseTo: number;
|
||||
opCode: number;
|
||||
fromCompressed?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface OpResponseOptions extends BSONSerializeOptions {
|
||||
raw?: boolean;
|
||||
documentsReturnedIn?: string | null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class Response {
|
||||
parsed: boolean;
|
||||
raw: Buffer;
|
||||
data: Buffer;
|
||||
opts: OpResponseOptions;
|
||||
length: number;
|
||||
requestId: number;
|
||||
responseTo: number;
|
||||
opCode: number;
|
||||
fromCompressed?: boolean;
|
||||
responseFlags?: number;
|
||||
cursorId?: Long;
|
||||
startingFrom?: number;
|
||||
numberReturned?: number;
|
||||
documents: (Document | Buffer)[] = new Array(0);
|
||||
cursorNotFound?: boolean;
|
||||
queryFailure?: boolean;
|
||||
shardConfigStale?: boolean;
|
||||
awaitCapable?: boolean;
|
||||
promoteLongs: boolean;
|
||||
promoteValues: boolean;
|
||||
promoteBuffers: boolean;
|
||||
bsonRegExp?: boolean;
|
||||
index?: number;
|
||||
|
||||
constructor(
|
||||
message: Buffer,
|
||||
msgHeader: MessageHeader,
|
||||
msgBody: Buffer,
|
||||
opts?: OpResponseOptions
|
||||
) {
|
||||
this.parsed = false;
|
||||
this.raw = message;
|
||||
this.data = msgBody;
|
||||
this.opts = opts ?? {
|
||||
promoteLongs: true,
|
||||
promoteValues: true,
|
||||
promoteBuffers: false,
|
||||
bsonRegExp: false
|
||||
};
|
||||
|
||||
// Read the message header
|
||||
this.length = msgHeader.length;
|
||||
this.requestId = msgHeader.requestId;
|
||||
this.responseTo = msgHeader.responseTo;
|
||||
this.opCode = msgHeader.opCode;
|
||||
this.fromCompressed = msgHeader.fromCompressed;
|
||||
|
||||
// Flag values
|
||||
this.promoteLongs = typeof this.opts.promoteLongs === 'boolean' ? this.opts.promoteLongs : true;
|
||||
this.promoteValues =
|
||||
typeof this.opts.promoteValues === 'boolean' ? this.opts.promoteValues : true;
|
||||
this.promoteBuffers =
|
||||
typeof this.opts.promoteBuffers === 'boolean' ? this.opts.promoteBuffers : false;
|
||||
this.bsonRegExp = typeof this.opts.bsonRegExp === 'boolean' ? this.opts.bsonRegExp : false;
|
||||
}
|
||||
|
||||
isParsed(): boolean {
|
||||
return this.parsed;
|
||||
}
|
||||
|
||||
parse(options: OpResponseOptions): void {
|
||||
// Don't parse again if not needed
|
||||
if (this.parsed) return;
|
||||
options = options ?? {};
|
||||
|
||||
// Allow the return of raw documents instead of parsing
|
||||
const raw = options.raw || false;
|
||||
const documentsReturnedIn = options.documentsReturnedIn || null;
|
||||
const promoteLongs = options.promoteLongs ?? this.opts.promoteLongs;
|
||||
const promoteValues = options.promoteValues ?? this.opts.promoteValues;
|
||||
const promoteBuffers = options.promoteBuffers ?? this.opts.promoteBuffers;
|
||||
const bsonRegExp = options.bsonRegExp ?? this.opts.bsonRegExp;
|
||||
let bsonSize;
|
||||
|
||||
// Set up the options
|
||||
const _options: BSONSerializeOptions = {
|
||||
promoteLongs,
|
||||
promoteValues,
|
||||
promoteBuffers,
|
||||
bsonRegExp
|
||||
};
|
||||
|
||||
// Position within OP_REPLY at which documents start
|
||||
// (See https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#wire-op-reply)
|
||||
this.index = 20;
|
||||
|
||||
// Read the message body
|
||||
this.responseFlags = this.data.readInt32LE(0);
|
||||
this.cursorId = new BSON.Long(this.data.readInt32LE(4), this.data.readInt32LE(8));
|
||||
this.startingFrom = this.data.readInt32LE(12);
|
||||
this.numberReturned = this.data.readInt32LE(16);
|
||||
|
||||
// Preallocate document array
|
||||
this.documents = new Array(this.numberReturned);
|
||||
|
||||
this.cursorNotFound = (this.responseFlags & CURSOR_NOT_FOUND) !== 0;
|
||||
this.queryFailure = (this.responseFlags & QUERY_FAILURE) !== 0;
|
||||
this.shardConfigStale = (this.responseFlags & SHARD_CONFIG_STALE) !== 0;
|
||||
this.awaitCapable = (this.responseFlags & AWAIT_CAPABLE) !== 0;
|
||||
|
||||
// Parse Body
|
||||
for (let i = 0; i < this.numberReturned; i++) {
|
||||
bsonSize =
|
||||
this.data[this.index] |
|
||||
(this.data[this.index + 1] << 8) |
|
||||
(this.data[this.index + 2] << 16) |
|
||||
(this.data[this.index + 3] << 24);
|
||||
|
||||
// If we have raw results specified slice the return document
|
||||
if (raw) {
|
||||
this.documents[i] = this.data.slice(this.index, this.index + bsonSize);
|
||||
} else {
|
||||
this.documents[i] = BSON.deserialize(
|
||||
this.data.slice(this.index, this.index + bsonSize),
|
||||
_options
|
||||
);
|
||||
}
|
||||
|
||||
// Adjust the index
|
||||
this.index = this.index + bsonSize;
|
||||
}
|
||||
|
||||
if (this.documents.length === 1 && documentsReturnedIn != null && raw) {
|
||||
const fieldsAsRaw: Document = {};
|
||||
fieldsAsRaw[documentsReturnedIn] = true;
|
||||
_options.fieldsAsRaw = fieldsAsRaw;
|
||||
|
||||
const doc = BSON.deserialize(this.documents[0] as Buffer, _options);
|
||||
this.documents = [doc];
|
||||
}
|
||||
|
||||
// Set parsed
|
||||
this.parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of OP_MSG spec:
|
||||
// https://github.com/mongodb/specifications/blob/master/source/message/OP_MSG.rst
|
||||
//
|
||||
// struct Section {
|
||||
// uint8 payloadType;
|
||||
// union payload {
|
||||
// document document; // payloadType == 0
|
||||
// struct sequence { // payloadType == 1
|
||||
// int32 size;
|
||||
// cstring identifier;
|
||||
// document* documents;
|
||||
// };
|
||||
// };
|
||||
// };
|
||||
|
||||
// struct OP_MSG {
|
||||
// struct MsgHeader {
|
||||
// int32 messageLength;
|
||||
// int32 requestID;
|
||||
// int32 responseTo;
|
||||
// int32 opCode = 2013;
|
||||
// };
|
||||
// uint32 flagBits;
|
||||
// Section+ sections;
|
||||
// [uint32 checksum;]
|
||||
// };
|
||||
|
||||
// Msg Flags
|
||||
const OPTS_CHECKSUM_PRESENT = 1;
|
||||
const OPTS_MORE_TO_COME = 2;
|
||||
const OPTS_EXHAUST_ALLOWED = 1 << 16;
|
||||
|
||||
/** @internal */
|
||||
export interface OpMsgOptions {
|
||||
requestId: number;
|
||||
serializeFunctions: boolean;
|
||||
ignoreUndefined: boolean;
|
||||
checkKeys: boolean;
|
||||
maxBsonSize: number;
|
||||
moreToCome: boolean;
|
||||
exhaustAllowed: boolean;
|
||||
readPreference: ReadPreference;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class Msg {
|
||||
ns: string;
|
||||
command: Document;
|
||||
options: OpQueryOptions;
|
||||
requestId: number;
|
||||
serializeFunctions: boolean;
|
||||
ignoreUndefined: boolean;
|
||||
checkKeys: boolean;
|
||||
maxBsonSize: number;
|
||||
checksumPresent: boolean;
|
||||
moreToCome: boolean;
|
||||
exhaustAllowed: boolean;
|
||||
|
||||
constructor(ns: string, command: Document, options: OpQueryOptions) {
|
||||
// Basic options needed to be passed in
|
||||
if (command == null)
|
||||
throw new MongoInvalidArgumentError('Query document must be specified for query');
|
||||
|
||||
// Basic options
|
||||
this.ns = ns;
|
||||
this.command = command;
|
||||
this.command.$db = databaseNamespace(ns);
|
||||
|
||||
if (options.readPreference && options.readPreference.mode !== ReadPreference.PRIMARY) {
|
||||
this.command.$readPreference = options.readPreference.toJSON();
|
||||
}
|
||||
|
||||
// Ensure empty options
|
||||
this.options = options ?? {};
|
||||
|
||||
// Additional options
|
||||
this.requestId = options.requestId ? options.requestId : Msg.getRequestId();
|
||||
|
||||
// Serialization option
|
||||
this.serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
this.ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
this.checkKeys = typeof options.checkKeys === 'boolean' ? options.checkKeys : false;
|
||||
this.maxBsonSize = options.maxBsonSize || 1024 * 1024 * 16;
|
||||
|
||||
// flags
|
||||
this.checksumPresent = false;
|
||||
this.moreToCome = options.moreToCome || false;
|
||||
this.exhaustAllowed =
|
||||
typeof options.exhaustAllowed === 'boolean' ? options.exhaustAllowed : false;
|
||||
}
|
||||
|
||||
toBin(): Buffer[] {
|
||||
const buffers: Buffer[] = [];
|
||||
let flags = 0;
|
||||
|
||||
if (this.checksumPresent) {
|
||||
flags |= OPTS_CHECKSUM_PRESENT;
|
||||
}
|
||||
|
||||
if (this.moreToCome) {
|
||||
flags |= OPTS_MORE_TO_COME;
|
||||
}
|
||||
|
||||
if (this.exhaustAllowed) {
|
||||
flags |= OPTS_EXHAUST_ALLOWED;
|
||||
}
|
||||
|
||||
const header = Buffer.alloc(
|
||||
4 * 4 + // Header
|
||||
4 // Flags
|
||||
);
|
||||
|
||||
buffers.push(header);
|
||||
|
||||
let totalLength = header.length;
|
||||
const command = this.command;
|
||||
totalLength += this.makeDocumentSegment(buffers, command);
|
||||
|
||||
header.writeInt32LE(totalLength, 0); // messageLength
|
||||
header.writeInt32LE(this.requestId, 4); // requestID
|
||||
header.writeInt32LE(0, 8); // responseTo
|
||||
header.writeInt32LE(OP_MSG, 12); // opCode
|
||||
header.writeUInt32LE(flags, 16); // flags
|
||||
return buffers;
|
||||
}
|
||||
|
||||
makeDocumentSegment(buffers: Buffer[], document: Document): number {
|
||||
const payloadTypeBuffer = Buffer.alloc(1);
|
||||
payloadTypeBuffer[0] = 0;
|
||||
|
||||
const documentBuffer = this.serializeBson(document);
|
||||
buffers.push(payloadTypeBuffer);
|
||||
buffers.push(documentBuffer);
|
||||
|
||||
return payloadTypeBuffer.length + documentBuffer.length;
|
||||
}
|
||||
|
||||
serializeBson(document: Document): Buffer {
|
||||
return BSON.serialize(document, {
|
||||
checkKeys: this.checkKeys,
|
||||
serializeFunctions: this.serializeFunctions,
|
||||
ignoreUndefined: this.ignoreUndefined
|
||||
});
|
||||
}
|
||||
|
||||
static getRequestId(): number {
|
||||
_requestId = (_requestId + 1) & 0x7fffffff;
|
||||
return _requestId;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class BinMsg {
|
||||
parsed: boolean;
|
||||
raw: Buffer;
|
||||
data: Buffer;
|
||||
opts: OpResponseOptions;
|
||||
length: number;
|
||||
requestId: number;
|
||||
responseTo: number;
|
||||
opCode: number;
|
||||
fromCompressed?: boolean;
|
||||
responseFlags: number;
|
||||
checksumPresent: boolean;
|
||||
moreToCome: boolean;
|
||||
exhaustAllowed: boolean;
|
||||
promoteLongs: boolean;
|
||||
promoteValues: boolean;
|
||||
promoteBuffers: boolean;
|
||||
bsonRegExp: boolean;
|
||||
documents: (Document | Buffer)[];
|
||||
index?: number;
|
||||
|
||||
constructor(
|
||||
message: Buffer,
|
||||
msgHeader: MessageHeader,
|
||||
msgBody: Buffer,
|
||||
opts?: OpResponseOptions
|
||||
) {
|
||||
this.parsed = false;
|
||||
this.raw = message;
|
||||
this.data = msgBody;
|
||||
this.opts = opts ?? {
|
||||
promoteLongs: true,
|
||||
promoteValues: true,
|
||||
promoteBuffers: false,
|
||||
bsonRegExp: false
|
||||
};
|
||||
|
||||
// Read the message header
|
||||
this.length = msgHeader.length;
|
||||
this.requestId = msgHeader.requestId;
|
||||
this.responseTo = msgHeader.responseTo;
|
||||
this.opCode = msgHeader.opCode;
|
||||
this.fromCompressed = msgHeader.fromCompressed;
|
||||
|
||||
// Read response flags
|
||||
this.responseFlags = msgBody.readInt32LE(0);
|
||||
this.checksumPresent = (this.responseFlags & OPTS_CHECKSUM_PRESENT) !== 0;
|
||||
this.moreToCome = (this.responseFlags & OPTS_MORE_TO_COME) !== 0;
|
||||
this.exhaustAllowed = (this.responseFlags & OPTS_EXHAUST_ALLOWED) !== 0;
|
||||
this.promoteLongs = typeof this.opts.promoteLongs === 'boolean' ? this.opts.promoteLongs : true;
|
||||
this.promoteValues =
|
||||
typeof this.opts.promoteValues === 'boolean' ? this.opts.promoteValues : true;
|
||||
this.promoteBuffers =
|
||||
typeof this.opts.promoteBuffers === 'boolean' ? this.opts.promoteBuffers : false;
|
||||
this.bsonRegExp = typeof this.opts.bsonRegExp === 'boolean' ? this.opts.bsonRegExp : false;
|
||||
|
||||
this.documents = [];
|
||||
}
|
||||
|
||||
isParsed(): boolean {
|
||||
return this.parsed;
|
||||
}
|
||||
|
||||
parse(options: OpResponseOptions): void {
|
||||
// Don't parse again if not needed
|
||||
if (this.parsed) return;
|
||||
options = options ?? {};
|
||||
|
||||
this.index = 4;
|
||||
// Allow the return of raw documents instead of parsing
|
||||
const raw = options.raw || false;
|
||||
const documentsReturnedIn = options.documentsReturnedIn || null;
|
||||
const promoteLongs = options.promoteLongs ?? this.opts.promoteLongs;
|
||||
const promoteValues = options.promoteValues ?? this.opts.promoteValues;
|
||||
const promoteBuffers = options.promoteBuffers ?? this.opts.promoteBuffers;
|
||||
const bsonRegExp = options.bsonRegExp ?? this.opts.bsonRegExp;
|
||||
const validation = this.parseBsonSerializationOptions(options);
|
||||
|
||||
// Set up the options
|
||||
const bsonOptions: BSONSerializeOptions = {
|
||||
promoteLongs,
|
||||
promoteValues,
|
||||
promoteBuffers,
|
||||
bsonRegExp,
|
||||
validation
|
||||
// Due to the strictness of the BSON libraries validation option we need this cast
|
||||
} as BSONSerializeOptions & { validation: { utf8: { writeErrors: boolean } } };
|
||||
|
||||
while (this.index < this.data.length) {
|
||||
const payloadType = this.data.readUInt8(this.index++);
|
||||
if (payloadType === 0) {
|
||||
const bsonSize = this.data.readUInt32LE(this.index);
|
||||
const bin = this.data.slice(this.index, this.index + bsonSize);
|
||||
this.documents.push(raw ? bin : BSON.deserialize(bin, bsonOptions));
|
||||
this.index += bsonSize;
|
||||
} else if (payloadType === 1) {
|
||||
// It was decided that no driver makes use of payload type 1
|
||||
|
||||
// TODO(NODE-3483): Replace with MongoDeprecationError
|
||||
throw new MongoRuntimeError('OP_MSG Payload Type 1 detected unsupported protocol');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.documents.length === 1 && documentsReturnedIn != null && raw) {
|
||||
const fieldsAsRaw: Document = {};
|
||||
fieldsAsRaw[documentsReturnedIn] = true;
|
||||
bsonOptions.fieldsAsRaw = fieldsAsRaw;
|
||||
const doc = BSON.deserialize(this.documents[0] as Buffer, bsonOptions);
|
||||
this.documents = [doc];
|
||||
}
|
||||
|
||||
this.parsed = true;
|
||||
}
|
||||
|
||||
parseBsonSerializationOptions({ enableUtf8Validation }: BSONSerializeOptions): {
|
||||
utf8: { writeErrors: false } | false;
|
||||
} {
|
||||
if (enableUtf8Validation === false) {
|
||||
return { utf8: false };
|
||||
}
|
||||
|
||||
return { utf8: { writeErrors: false } };
|
||||
}
|
||||
}
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
import type { Socket, SocketConnectOpts } from 'net';
|
||||
import * as net from 'net';
|
||||
import { SocksClient } from 'socks';
|
||||
import type { ConnectionOptions as TLSConnectionOpts, TLSSocket } from 'tls';
|
||||
import * as tls from 'tls';
|
||||
|
||||
import type { Document } from '../bson';
|
||||
import { Int32 } from '../bson';
|
||||
import { LEGACY_HELLO_COMMAND } from '../constants';
|
||||
import {
|
||||
MongoCompatibilityError,
|
||||
MongoError,
|
||||
MongoErrorLabel,
|
||||
MongoInvalidArgumentError,
|
||||
MongoNetworkError,
|
||||
MongoNetworkTimeoutError,
|
||||
MongoRuntimeError,
|
||||
MongoServerError,
|
||||
needsRetryableWriteLabel
|
||||
} from '../error';
|
||||
import { Callback, ClientMetadata, HostAddress, makeClientMetadata, ns } from '../utils';
|
||||
import { AuthContext, AuthProvider } from './auth/auth_provider';
|
||||
import { GSSAPI } from './auth/gssapi';
|
||||
import { MongoCR } from './auth/mongocr';
|
||||
import { MongoDBAWS } from './auth/mongodb_aws';
|
||||
import { Plain } from './auth/plain';
|
||||
import { AuthMechanism } from './auth/providers';
|
||||
import { ScramSHA1, ScramSHA256 } from './auth/scram';
|
||||
import { X509 } from './auth/x509';
|
||||
import { Connection, ConnectionOptions, CryptoConnection } from './connection';
|
||||
import {
|
||||
MAX_SUPPORTED_SERVER_VERSION,
|
||||
MAX_SUPPORTED_WIRE_VERSION,
|
||||
MIN_SUPPORTED_SERVER_VERSION,
|
||||
MIN_SUPPORTED_WIRE_VERSION
|
||||
} from './wire_protocol/constants';
|
||||
|
||||
const AUTH_PROVIDERS = new Map<AuthMechanism | string, AuthProvider>([
|
||||
[AuthMechanism.MONGODB_AWS, new MongoDBAWS()],
|
||||
[AuthMechanism.MONGODB_CR, new MongoCR()],
|
||||
[AuthMechanism.MONGODB_GSSAPI, new GSSAPI()],
|
||||
[AuthMechanism.MONGODB_PLAIN, new Plain()],
|
||||
[AuthMechanism.MONGODB_SCRAM_SHA1, new ScramSHA1()],
|
||||
[AuthMechanism.MONGODB_SCRAM_SHA256, new ScramSHA256()],
|
||||
[AuthMechanism.MONGODB_X509, new X509()]
|
||||
]);
|
||||
|
||||
/** @public */
|
||||
export type Stream = Socket | TLSSocket;
|
||||
|
||||
export function connect(options: ConnectionOptions, callback: Callback<Connection>): void {
|
||||
makeConnection({ ...options, existingSocket: undefined }, (err, socket) => {
|
||||
if (err || !socket) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let ConnectionType = options.connectionType ?? Connection;
|
||||
if (options.autoEncrypter) {
|
||||
ConnectionType = CryptoConnection;
|
||||
}
|
||||
performInitialHandshake(new ConnectionType(socket, options), options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSupportedServer(hello: Document, options: ConnectionOptions) {
|
||||
const serverVersionHighEnough =
|
||||
hello &&
|
||||
(typeof hello.maxWireVersion === 'number' || hello.maxWireVersion instanceof Int32) &&
|
||||
hello.maxWireVersion >= MIN_SUPPORTED_WIRE_VERSION;
|
||||
const serverVersionLowEnough =
|
||||
hello &&
|
||||
(typeof hello.minWireVersion === 'number' || hello.minWireVersion instanceof Int32) &&
|
||||
hello.minWireVersion <= MAX_SUPPORTED_WIRE_VERSION;
|
||||
|
||||
if (serverVersionHighEnough) {
|
||||
if (serverVersionLowEnough) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = `Server at ${options.hostAddress} reports minimum wire version ${JSON.stringify(
|
||||
hello.minWireVersion
|
||||
)}, but this version of the Node.js Driver requires at most ${MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${MAX_SUPPORTED_SERVER_VERSION})`;
|
||||
return new MongoCompatibilityError(message);
|
||||
}
|
||||
|
||||
const message = `Server at ${options.hostAddress} reports maximum wire version ${
|
||||
JSON.stringify(hello.maxWireVersion) ?? 0
|
||||
}, but this version of the Node.js Driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION})`;
|
||||
return new MongoCompatibilityError(message);
|
||||
}
|
||||
|
||||
function performInitialHandshake(
|
||||
conn: Connection,
|
||||
options: ConnectionOptions,
|
||||
_callback: Callback
|
||||
) {
|
||||
const callback: Callback<Document> = function (err, ret) {
|
||||
if (err && conn) {
|
||||
conn.destroy();
|
||||
}
|
||||
_callback(err, ret);
|
||||
};
|
||||
|
||||
const credentials = options.credentials;
|
||||
if (credentials) {
|
||||
if (
|
||||
!(credentials.mechanism === AuthMechanism.MONGODB_DEFAULT) &&
|
||||
!AUTH_PROVIDERS.get(credentials.mechanism)
|
||||
) {
|
||||
callback(
|
||||
new MongoInvalidArgumentError(`AuthMechanism '${credentials.mechanism}' not supported`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const authContext = new AuthContext(conn, credentials, options);
|
||||
prepareHandshakeDocument(authContext, (err, handshakeDoc) => {
|
||||
if (err || !handshakeDoc) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const handshakeOptions: Document = Object.assign({}, options);
|
||||
if (typeof options.connectTimeoutMS === 'number') {
|
||||
// The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
|
||||
handshakeOptions.socketTimeoutMS = options.connectTimeoutMS;
|
||||
}
|
||||
|
||||
const start = new Date().getTime();
|
||||
conn.command(ns('admin.$cmd'), handshakeDoc, handshakeOptions, (err, response) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response?.ok === 0) {
|
||||
callback(new MongoServerError(response));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('isWritablePrimary' in response)) {
|
||||
// Provide hello-style response document.
|
||||
response.isWritablePrimary = response[LEGACY_HELLO_COMMAND];
|
||||
}
|
||||
|
||||
if (response.helloOk) {
|
||||
conn.helloOk = true;
|
||||
}
|
||||
|
||||
const supportedServerErr = checkSupportedServer(response, options);
|
||||
if (supportedServerErr) {
|
||||
callback(supportedServerErr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.loadBalanced) {
|
||||
if (!response.serviceId) {
|
||||
return callback(
|
||||
new MongoCompatibilityError(
|
||||
'Driver attempted to initialize in load balancing mode, ' +
|
||||
'but the server does not support this mode.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: This is metadata attached to the connection while porting away from
|
||||
// handshake being done in the `Server` class. Likely, it should be
|
||||
// relocated, or at very least restructured.
|
||||
conn.hello = response;
|
||||
conn.lastHelloMS = new Date().getTime() - start;
|
||||
|
||||
if (!response.arbiterOnly && credentials) {
|
||||
// store the response on auth context
|
||||
authContext.response = response;
|
||||
|
||||
const resolvedCredentials = credentials.resolveAuthMechanism(response);
|
||||
const provider = AUTH_PROVIDERS.get(resolvedCredentials.mechanism);
|
||||
if (!provider) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError(
|
||||
`No AuthProvider for ${resolvedCredentials.mechanism} defined.`
|
||||
)
|
||||
);
|
||||
}
|
||||
provider.auth(authContext, err => {
|
||||
if (err) {
|
||||
if (err instanceof MongoError) {
|
||||
err.addErrorLabel(MongoErrorLabel.HandshakeError);
|
||||
if (needsRetryableWriteLabel(err, response.maxWireVersion)) {
|
||||
err.addErrorLabel(MongoErrorLabel.RetryableWriteError);
|
||||
}
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, conn);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
callback(undefined, conn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface HandshakeDocument extends Document {
|
||||
/**
|
||||
* @deprecated Use hello instead
|
||||
*/
|
||||
ismaster?: boolean;
|
||||
hello?: boolean;
|
||||
helloOk?: boolean;
|
||||
client: ClientMetadata;
|
||||
compression: string[];
|
||||
saslSupportedMechs?: string;
|
||||
loadBalanced?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* This function is only exposed for testing purposes.
|
||||
*/
|
||||
export function prepareHandshakeDocument(
|
||||
authContext: AuthContext,
|
||||
callback: Callback<HandshakeDocument>
|
||||
) {
|
||||
const options = authContext.options;
|
||||
const compressors = options.compressors ? options.compressors : [];
|
||||
const { serverApi } = authContext.connection;
|
||||
|
||||
const handshakeDoc: HandshakeDocument = {
|
||||
[serverApi?.version ? 'hello' : LEGACY_HELLO_COMMAND]: true,
|
||||
helloOk: true,
|
||||
client: options.metadata || makeClientMetadata(options),
|
||||
compression: compressors
|
||||
};
|
||||
|
||||
if (options.loadBalanced === true) {
|
||||
handshakeDoc.loadBalanced = true;
|
||||
}
|
||||
|
||||
const credentials = authContext.credentials;
|
||||
if (credentials) {
|
||||
if (credentials.mechanism === AuthMechanism.MONGODB_DEFAULT && credentials.username) {
|
||||
handshakeDoc.saslSupportedMechs = `${credentials.source}.${credentials.username}`;
|
||||
|
||||
const provider = AUTH_PROVIDERS.get(AuthMechanism.MONGODB_SCRAM_SHA256);
|
||||
if (!provider) {
|
||||
// This auth mechanism is always present.
|
||||
return callback(
|
||||
new MongoInvalidArgumentError(
|
||||
`No AuthProvider for ${AuthMechanism.MONGODB_SCRAM_SHA256} defined.`
|
||||
)
|
||||
);
|
||||
}
|
||||
return provider.prepare(handshakeDoc, authContext, callback);
|
||||
}
|
||||
const provider = AUTH_PROVIDERS.get(credentials.mechanism);
|
||||
if (!provider) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError(`No AuthProvider for ${credentials.mechanism} defined.`)
|
||||
);
|
||||
}
|
||||
return provider.prepare(handshakeDoc, authContext, callback);
|
||||
}
|
||||
callback(undefined, handshakeDoc);
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const LEGAL_TLS_SOCKET_OPTIONS = [
|
||||
'ALPNProtocols',
|
||||
'ca',
|
||||
'cert',
|
||||
'checkServerIdentity',
|
||||
'ciphers',
|
||||
'crl',
|
||||
'ecdhCurve',
|
||||
'key',
|
||||
'minDHSize',
|
||||
'passphrase',
|
||||
'pfx',
|
||||
'rejectUnauthorized',
|
||||
'secureContext',
|
||||
'secureProtocol',
|
||||
'servername',
|
||||
'session'
|
||||
] as const;
|
||||
|
||||
/** @public */
|
||||
export const LEGAL_TCP_SOCKET_OPTIONS = [
|
||||
'family',
|
||||
'hints',
|
||||
'localAddress',
|
||||
'localPort',
|
||||
'lookup'
|
||||
] as const;
|
||||
|
||||
function parseConnectOptions(options: ConnectionOptions): SocketConnectOpts {
|
||||
const hostAddress = options.hostAddress;
|
||||
if (!hostAddress) throw new MongoInvalidArgumentError('Option "hostAddress" is required');
|
||||
|
||||
const result: Partial<net.TcpNetConnectOpts & net.IpcNetConnectOpts> = {};
|
||||
for (const name of LEGAL_TCP_SOCKET_OPTIONS) {
|
||||
if (options[name] != null) {
|
||||
(result as Document)[name] = options[name];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof hostAddress.socketPath === 'string') {
|
||||
result.path = hostAddress.socketPath;
|
||||
return result as net.IpcNetConnectOpts;
|
||||
} else if (typeof hostAddress.host === 'string') {
|
||||
result.host = hostAddress.host;
|
||||
result.port = hostAddress.port;
|
||||
return result as net.TcpNetConnectOpts;
|
||||
} else {
|
||||
// This should never happen since we set up HostAddresses
|
||||
// But if we don't throw here the socket could hang until timeout
|
||||
// TODO(NODE-3483)
|
||||
throw new MongoRuntimeError(`Unexpected HostAddress ${JSON.stringify(hostAddress)}`);
|
||||
}
|
||||
}
|
||||
|
||||
type MakeConnectionOptions = ConnectionOptions & { existingSocket?: Stream };
|
||||
|
||||
function parseSslOptions(options: MakeConnectionOptions): TLSConnectionOpts {
|
||||
const result: TLSConnectionOpts = parseConnectOptions(options);
|
||||
// Merge in valid SSL options
|
||||
for (const name of LEGAL_TLS_SOCKET_OPTIONS) {
|
||||
if (options[name] != null) {
|
||||
(result as Document)[name] = options[name];
|
||||
}
|
||||
}
|
||||
|
||||
if (options.existingSocket) {
|
||||
result.socket = options.existingSocket;
|
||||
}
|
||||
|
||||
// Set default sni servername to be the same as host
|
||||
if (result.servername == null && result.host && !net.isIP(result.host)) {
|
||||
result.servername = result.host;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const SOCKET_ERROR_EVENT_LIST = ['error', 'close', 'timeout', 'parseError'] as const;
|
||||
type ErrorHandlerEventName = typeof SOCKET_ERROR_EVENT_LIST[number] | 'cancel';
|
||||
const SOCKET_ERROR_EVENTS = new Set(SOCKET_ERROR_EVENT_LIST);
|
||||
|
||||
function makeConnection(options: MakeConnectionOptions, _callback: Callback<Stream>) {
|
||||
const useTLS = options.tls ?? false;
|
||||
const keepAlive = options.keepAlive ?? true;
|
||||
const socketTimeoutMS = options.socketTimeoutMS ?? Reflect.get(options, 'socketTimeout') ?? 0;
|
||||
const noDelay = options.noDelay ?? true;
|
||||
const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
|
||||
const rejectUnauthorized = options.rejectUnauthorized ?? true;
|
||||
const keepAliveInitialDelay =
|
||||
((options.keepAliveInitialDelay ?? 120000) > socketTimeoutMS
|
||||
? Math.round(socketTimeoutMS / 2)
|
||||
: options.keepAliveInitialDelay) ?? 120000;
|
||||
const existingSocket = options.existingSocket;
|
||||
|
||||
let socket: Stream;
|
||||
const callback: Callback<Stream> = function (err, ret) {
|
||||
if (err && socket) {
|
||||
socket.destroy();
|
||||
}
|
||||
|
||||
_callback(err, ret);
|
||||
};
|
||||
|
||||
if (options.proxyHost != null) {
|
||||
// Currently, only Socks5 is supported.
|
||||
return makeSocks5Connection(
|
||||
{
|
||||
...options,
|
||||
connectTimeoutMS // Should always be present for Socks5
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
if (useTLS) {
|
||||
const tlsSocket = tls.connect(parseSslOptions(options));
|
||||
if (typeof tlsSocket.disableRenegotiation === 'function') {
|
||||
tlsSocket.disableRenegotiation();
|
||||
}
|
||||
socket = tlsSocket;
|
||||
} else if (existingSocket) {
|
||||
// In the TLS case, parseSslOptions() sets options.socket to existingSocket,
|
||||
// so we only need to handle the non-TLS case here (where existingSocket
|
||||
// gives us all we need out of the box).
|
||||
socket = existingSocket;
|
||||
} else {
|
||||
socket = net.createConnection(parseConnectOptions(options));
|
||||
}
|
||||
|
||||
socket.setKeepAlive(keepAlive, keepAliveInitialDelay);
|
||||
socket.setTimeout(connectTimeoutMS);
|
||||
socket.setNoDelay(noDelay);
|
||||
|
||||
const connectEvent = useTLS ? 'secureConnect' : 'connect';
|
||||
let cancellationHandler: (err: Error) => void;
|
||||
function errorHandler(eventName: ErrorHandlerEventName) {
|
||||
return (err: Error) => {
|
||||
SOCKET_ERROR_EVENTS.forEach(event => socket.removeAllListeners(event));
|
||||
if (cancellationHandler && options.cancellationToken) {
|
||||
options.cancellationToken.removeListener('cancel', cancellationHandler);
|
||||
}
|
||||
|
||||
socket.removeListener(connectEvent, connectHandler);
|
||||
callback(connectionFailureError(eventName, err));
|
||||
};
|
||||
}
|
||||
|
||||
function connectHandler() {
|
||||
SOCKET_ERROR_EVENTS.forEach(event => socket.removeAllListeners(event));
|
||||
if (cancellationHandler && options.cancellationToken) {
|
||||
options.cancellationToken.removeListener('cancel', cancellationHandler);
|
||||
}
|
||||
|
||||
if ('authorizationError' in socket) {
|
||||
if (socket.authorizationError && rejectUnauthorized) {
|
||||
return callback(socket.authorizationError);
|
||||
}
|
||||
}
|
||||
|
||||
socket.setTimeout(socketTimeoutMS);
|
||||
callback(undefined, socket);
|
||||
}
|
||||
|
||||
SOCKET_ERROR_EVENTS.forEach(event => socket.once(event, errorHandler(event)));
|
||||
if (options.cancellationToken) {
|
||||
cancellationHandler = errorHandler('cancel');
|
||||
options.cancellationToken.once('cancel', cancellationHandler);
|
||||
}
|
||||
|
||||
if (existingSocket) {
|
||||
process.nextTick(connectHandler);
|
||||
} else {
|
||||
socket.once(connectEvent, connectHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function makeSocks5Connection(options: MakeConnectionOptions, callback: Callback<Stream>) {
|
||||
const hostAddress = HostAddress.fromHostPort(
|
||||
options.proxyHost ?? '', // proxyHost is guaranteed to set here
|
||||
options.proxyPort ?? 1080
|
||||
);
|
||||
|
||||
// First, connect to the proxy server itself:
|
||||
makeConnection(
|
||||
{
|
||||
...options,
|
||||
hostAddress,
|
||||
tls: false,
|
||||
proxyHost: undefined
|
||||
},
|
||||
(err, rawSocket) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const destination = parseConnectOptions(options) as net.TcpNetConnectOpts;
|
||||
if (typeof destination.host !== 'string' || typeof destination.port !== 'number') {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError('Can only make Socks5 connections to TCP hosts')
|
||||
);
|
||||
}
|
||||
|
||||
// Then, establish the Socks5 proxy connection:
|
||||
SocksClient.createConnection({
|
||||
existing_socket: rawSocket,
|
||||
timeout: options.connectTimeoutMS,
|
||||
command: 'connect',
|
||||
destination: {
|
||||
host: destination.host,
|
||||
port: destination.port
|
||||
},
|
||||
proxy: {
|
||||
// host and port are ignored because we pass existing_socket
|
||||
host: 'iLoveJavaScript',
|
||||
port: 0,
|
||||
type: 5,
|
||||
userId: options.proxyUsername || undefined,
|
||||
password: options.proxyPassword || undefined
|
||||
}
|
||||
}).then(
|
||||
({ socket }) => {
|
||||
// Finally, now treat the resulting duplex stream as the
|
||||
// socket over which we send and receive wire protocol messages:
|
||||
makeConnection(
|
||||
{
|
||||
...options,
|
||||
existingSocket: socket,
|
||||
proxyHost: undefined
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
error => callback(connectionFailureError('error', error))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function connectionFailureError(type: ErrorHandlerEventName, err: Error) {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return new MongoNetworkError(err);
|
||||
case 'timeout':
|
||||
return new MongoNetworkTimeoutError('connection timed out');
|
||||
case 'close':
|
||||
return new MongoNetworkError('connection closed');
|
||||
case 'cancel':
|
||||
return new MongoNetworkError('connection establishment was cancelled');
|
||||
default:
|
||||
return new MongoNetworkError('unknown network error');
|
||||
}
|
||||
}
|
||||
+752
@@ -0,0 +1,752 @@
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
|
||||
import type { BSONSerializeOptions, Document, ObjectId } from '../bson';
|
||||
import {
|
||||
CLOSE,
|
||||
CLUSTER_TIME_RECEIVED,
|
||||
COMMAND_FAILED,
|
||||
COMMAND_STARTED,
|
||||
COMMAND_SUCCEEDED,
|
||||
MESSAGE,
|
||||
PINNED,
|
||||
UNPINNED
|
||||
} from '../constants';
|
||||
import type { AutoEncrypter } from '../deps';
|
||||
import {
|
||||
MongoCompatibilityError,
|
||||
MongoMissingDependencyError,
|
||||
MongoNetworkError,
|
||||
MongoNetworkTimeoutError,
|
||||
MongoServerError,
|
||||
MongoWriteConcernError
|
||||
} from '../error';
|
||||
import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client';
|
||||
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
|
||||
import type { ReadPreference, ReadPreferenceLike } from '../read_preference';
|
||||
import { applySession, ClientSession, updateSessionFromResponse } from '../sessions';
|
||||
import {
|
||||
calculateDurationInMs,
|
||||
Callback,
|
||||
ClientMetadata,
|
||||
HostAddress,
|
||||
maxWireVersion,
|
||||
MongoDBNamespace,
|
||||
now,
|
||||
uuidV4
|
||||
} from '../utils';
|
||||
import type { WriteConcern } from '../write_concern';
|
||||
import type { MongoCredentials } from './auth/mongo_credentials';
|
||||
import {
|
||||
CommandFailedEvent,
|
||||
CommandStartedEvent,
|
||||
CommandSucceededEvent
|
||||
} from './command_monitoring_events';
|
||||
import { BinMsg, Msg, Query, Response, WriteProtocolMessageType } from './commands';
|
||||
import type { Stream } from './connect';
|
||||
import { MessageStream, OperationDescription } from './message_stream';
|
||||
import { StreamDescription, StreamDescriptionOptions } from './stream_description';
|
||||
import { getReadPreference, isSharded } from './wire_protocol/shared';
|
||||
|
||||
/** @internal */
|
||||
const kStream = Symbol('stream');
|
||||
/** @internal */
|
||||
const kQueue = Symbol('queue');
|
||||
/** @internal */
|
||||
const kMessageStream = Symbol('messageStream');
|
||||
/** @internal */
|
||||
const kGeneration = Symbol('generation');
|
||||
/** @internal */
|
||||
const kLastUseTime = Symbol('lastUseTime');
|
||||
/** @internal */
|
||||
const kClusterTime = Symbol('clusterTime');
|
||||
/** @internal */
|
||||
const kDescription = Symbol('description');
|
||||
/** @internal */
|
||||
const kHello = Symbol('hello');
|
||||
/** @internal */
|
||||
const kAutoEncrypter = Symbol('autoEncrypter');
|
||||
/** @internal */
|
||||
const kDelayedTimeoutId = Symbol('delayedTimeoutId');
|
||||
|
||||
/** @internal */
|
||||
export interface QueryOptions extends BSONSerializeOptions {
|
||||
readPreference: ReadPreference;
|
||||
documentsReturnedIn?: string;
|
||||
batchSize?: number;
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
projection?: Document;
|
||||
tailable?: boolean;
|
||||
awaitData?: boolean;
|
||||
noCursorTimeout?: boolean;
|
||||
/** @deprecated use `noCursorTimeout` instead */
|
||||
timeout?: boolean;
|
||||
partial?: boolean;
|
||||
oplogReplay?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface CommandOptions extends BSONSerializeOptions {
|
||||
command?: boolean;
|
||||
secondaryOk?: boolean;
|
||||
/** Specify read preference if command supports it */
|
||||
readPreference?: ReadPreferenceLike;
|
||||
raw?: boolean;
|
||||
monitoring?: boolean;
|
||||
socketTimeoutMS?: number;
|
||||
/** Session to use for the operation */
|
||||
session?: ClientSession;
|
||||
documentsReturnedIn?: string;
|
||||
noResponse?: boolean;
|
||||
omitReadPreference?: boolean;
|
||||
|
||||
// TODO(NODE-2802): Currently the CommandOptions take a property willRetryWrite which is a hint
|
||||
// from executeOperation that the txnNum should be applied to this command.
|
||||
// Applying a session to a command should happen as part of command construction,
|
||||
// most likely in the CommandOperation#executeCommand method, where we have access to
|
||||
// the details we need to determine if a txnNum should also be applied.
|
||||
willRetryWrite?: boolean;
|
||||
|
||||
writeConcern?: WriteConcern;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface GetMoreOptions extends CommandOptions {
|
||||
batchSize?: number;
|
||||
maxTimeMS?: number;
|
||||
maxAwaitTimeMS?: number;
|
||||
/**
|
||||
* Comment to apply to the operation.
|
||||
*
|
||||
* In server versions pre-4.4, 'comment' must be string. A server
|
||||
* error will be thrown if any other type is provided.
|
||||
*
|
||||
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
|
||||
*/
|
||||
comment?: unknown;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ProxyOptions {
|
||||
proxyHost?: string;
|
||||
proxyPort?: number;
|
||||
proxyUsername?: string;
|
||||
proxyPassword?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ConnectionOptions
|
||||
extends SupportedNodeConnectionOptions,
|
||||
StreamDescriptionOptions,
|
||||
ProxyOptions {
|
||||
// Internal creation info
|
||||
id: number | '<monitor>';
|
||||
generation: number;
|
||||
hostAddress: HostAddress;
|
||||
// Settings
|
||||
autoEncrypter?: AutoEncrypter;
|
||||
serverApi?: ServerApi;
|
||||
monitorCommands: boolean;
|
||||
/** @internal */
|
||||
connectionType?: typeof Connection;
|
||||
credentials?: MongoCredentials;
|
||||
connectTimeoutMS?: number;
|
||||
tls: boolean;
|
||||
keepAlive?: boolean;
|
||||
keepAliveInitialDelay?: number;
|
||||
noDelay?: boolean;
|
||||
socketTimeoutMS?: number;
|
||||
cancellationToken?: CancellationToken;
|
||||
|
||||
metadata: ClientMetadata;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface DestroyOptions {
|
||||
/** Force the destruction. */
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ConnectionEvents = {
|
||||
commandStarted(event: CommandStartedEvent): void;
|
||||
commandSucceeded(event: CommandSucceededEvent): void;
|
||||
commandFailed(event: CommandFailedEvent): void;
|
||||
clusterTimeReceived(clusterTime: Document): void;
|
||||
close(): void;
|
||||
message(message: any): void;
|
||||
pinned(pinType: string): void;
|
||||
unpinned(pinType: string): void;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export class Connection extends TypedEventEmitter<ConnectionEvents> {
|
||||
id: number | '<monitor>';
|
||||
address: string;
|
||||
socketTimeoutMS: number;
|
||||
monitorCommands: boolean;
|
||||
closed: boolean;
|
||||
destroyed: boolean;
|
||||
lastHelloMS?: number;
|
||||
serverApi?: ServerApi;
|
||||
helloOk?: boolean;
|
||||
|
||||
/**@internal */
|
||||
[kDelayedTimeoutId]: NodeJS.Timeout | null;
|
||||
/** @internal */
|
||||
[kDescription]: StreamDescription;
|
||||
/** @internal */
|
||||
[kGeneration]: number;
|
||||
/** @internal */
|
||||
[kLastUseTime]: number;
|
||||
/** @internal */
|
||||
[kQueue]: Map<number, OperationDescription>;
|
||||
/** @internal */
|
||||
[kMessageStream]: MessageStream;
|
||||
/** @internal */
|
||||
[kStream]: Stream;
|
||||
/** @internal */
|
||||
[kHello]: Document | null;
|
||||
/** @internal */
|
||||
[kClusterTime]: Document | null;
|
||||
|
||||
/** @event */
|
||||
static readonly COMMAND_STARTED = COMMAND_STARTED;
|
||||
/** @event */
|
||||
static readonly COMMAND_SUCCEEDED = COMMAND_SUCCEEDED;
|
||||
/** @event */
|
||||
static readonly COMMAND_FAILED = COMMAND_FAILED;
|
||||
/** @event */
|
||||
static readonly CLUSTER_TIME_RECEIVED = CLUSTER_TIME_RECEIVED;
|
||||
/** @event */
|
||||
static readonly CLOSE = CLOSE;
|
||||
/** @event */
|
||||
static readonly MESSAGE = MESSAGE;
|
||||
/** @event */
|
||||
static readonly PINNED = PINNED;
|
||||
/** @event */
|
||||
static readonly UNPINNED = UNPINNED;
|
||||
|
||||
constructor(stream: Stream, options: ConnectionOptions) {
|
||||
super();
|
||||
this.id = options.id;
|
||||
this.address = streamIdentifier(stream, options);
|
||||
this.socketTimeoutMS = options.socketTimeoutMS ?? 0;
|
||||
this.monitorCommands = options.monitorCommands;
|
||||
this.serverApi = options.serverApi;
|
||||
this.closed = false;
|
||||
this.destroyed = false;
|
||||
this[kHello] = null;
|
||||
this[kClusterTime] = null;
|
||||
|
||||
this[kDescription] = new StreamDescription(this.address, options);
|
||||
this[kGeneration] = options.generation;
|
||||
this[kLastUseTime] = now();
|
||||
|
||||
// setup parser stream and message handling
|
||||
this[kQueue] = new Map();
|
||||
this[kMessageStream] = new MessageStream({
|
||||
...options,
|
||||
maxBsonMessageSize: this.hello?.maxBsonMessageSize
|
||||
});
|
||||
this[kStream] = stream;
|
||||
|
||||
this[kDelayedTimeoutId] = null;
|
||||
|
||||
this[kMessageStream].on('message', message => this.onMessage(message));
|
||||
this[kMessageStream].on('error', error => this.onError(error));
|
||||
this[kStream].on('close', () => this.onClose());
|
||||
this[kStream].on('timeout', () => this.onTimeout());
|
||||
this[kStream].on('error', () => {
|
||||
/* ignore errors, listen to `close` instead */
|
||||
});
|
||||
|
||||
// hook the message stream up to the passed in stream
|
||||
this[kStream].pipe(this[kMessageStream]);
|
||||
this[kMessageStream].pipe(this[kStream]);
|
||||
}
|
||||
|
||||
get description(): StreamDescription {
|
||||
return this[kDescription];
|
||||
}
|
||||
|
||||
get hello(): Document | null {
|
||||
return this[kHello];
|
||||
}
|
||||
|
||||
// the `connect` method stores the result of the handshake hello on the connection
|
||||
set hello(response: Document | null) {
|
||||
this[kDescription].receiveResponse(response);
|
||||
this[kDescription] = Object.freeze(this[kDescription]);
|
||||
|
||||
// TODO: remove this, and only use the `StreamDescription` in the future
|
||||
this[kHello] = response;
|
||||
}
|
||||
|
||||
// Set the whether the message stream is for a monitoring connection.
|
||||
set isMonitoringConnection(value: boolean) {
|
||||
this[kMessageStream].isMonitoringConnection = value;
|
||||
}
|
||||
|
||||
get isMonitoringConnection(): boolean {
|
||||
return this[kMessageStream].isMonitoringConnection;
|
||||
}
|
||||
|
||||
get serviceId(): ObjectId | undefined {
|
||||
return this.hello?.serviceId;
|
||||
}
|
||||
|
||||
get loadBalanced(): boolean {
|
||||
return this.description.loadBalanced;
|
||||
}
|
||||
|
||||
get generation(): number {
|
||||
return this[kGeneration] || 0;
|
||||
}
|
||||
|
||||
set generation(generation: number) {
|
||||
this[kGeneration] = generation;
|
||||
}
|
||||
|
||||
get idleTime(): number {
|
||||
return calculateDurationInMs(this[kLastUseTime]);
|
||||
}
|
||||
|
||||
get clusterTime(): Document | null {
|
||||
return this[kClusterTime];
|
||||
}
|
||||
|
||||
get stream(): Stream {
|
||||
return this[kStream];
|
||||
}
|
||||
|
||||
markAvailable(): void {
|
||||
this[kLastUseTime] = now();
|
||||
}
|
||||
|
||||
onError(error: Error) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[kStream].destroy(error);
|
||||
|
||||
this.closed = true;
|
||||
|
||||
for (const op of this[kQueue].values()) {
|
||||
op.cb(error);
|
||||
}
|
||||
|
||||
this[kQueue].clear();
|
||||
this.emit(Connection.CLOSE);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
|
||||
const message = `connection ${this.id} to ${this.address} closed`;
|
||||
for (const op of this[kQueue].values()) {
|
||||
op.cb(new MongoNetworkError(message));
|
||||
}
|
||||
|
||||
this[kQueue].clear();
|
||||
this.emit(Connection.CLOSE);
|
||||
}
|
||||
|
||||
onTimeout() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[kDelayedTimeoutId] = setTimeout(() => {
|
||||
this[kStream].destroy();
|
||||
|
||||
this.closed = true;
|
||||
|
||||
const message = `connection ${this.id} to ${this.address} timed out`;
|
||||
const beforeHandshake = this.hello == null;
|
||||
for (const op of this[kQueue].values()) {
|
||||
op.cb(new MongoNetworkTimeoutError(message, { beforeHandshake }));
|
||||
}
|
||||
|
||||
this[kQueue].clear();
|
||||
this.emit(Connection.CLOSE);
|
||||
}, 1).unref(); // No need for this timer to hold the event loop open
|
||||
}
|
||||
|
||||
onMessage(message: BinMsg | Response) {
|
||||
const delayedTimeoutId = this[kDelayedTimeoutId];
|
||||
if (delayedTimeoutId != null) {
|
||||
clearTimeout(delayedTimeoutId);
|
||||
this[kDelayedTimeoutId] = null;
|
||||
}
|
||||
|
||||
// always emit the message, in case we are streaming
|
||||
this.emit('message', message);
|
||||
const operationDescription = this[kQueue].get(message.responseTo);
|
||||
if (!operationDescription) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = operationDescription.cb;
|
||||
|
||||
// SERVER-45775: For exhaust responses we should be able to use the same requestId to
|
||||
// track response, however the server currently synthetically produces remote requests
|
||||
// making the `responseTo` change on each response
|
||||
this[kQueue].delete(message.responseTo);
|
||||
if ('moreToCome' in message && message.moreToCome) {
|
||||
// requeue the callback for next synthetic request
|
||||
this[kQueue].set(message.requestId, operationDescription);
|
||||
} else if (operationDescription.socketTimeoutOverride) {
|
||||
this[kStream].setTimeout(this.socketTimeoutMS);
|
||||
}
|
||||
|
||||
try {
|
||||
// Pass in the entire description because it has BSON parsing options
|
||||
message.parse(operationDescription);
|
||||
} catch (err) {
|
||||
// If this error is generated by our own code, it will already have the correct class applied
|
||||
// if it is not, then it is coming from a catastrophic data parse failure or the BSON library
|
||||
// in either case, it should not be wrapped
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.documents[0]) {
|
||||
const document: Document = message.documents[0];
|
||||
const session = operationDescription.session;
|
||||
if (session) {
|
||||
updateSessionFromResponse(session, document);
|
||||
}
|
||||
|
||||
if (document.$clusterTime) {
|
||||
this[kClusterTime] = document.$clusterTime;
|
||||
this.emit(Connection.CLUSTER_TIME_RECEIVED, document.$clusterTime);
|
||||
}
|
||||
|
||||
if (operationDescription.command) {
|
||||
if (document.writeConcernError) {
|
||||
callback(new MongoWriteConcernError(document.writeConcernError, document));
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.ok === 0 || document.$err || document.errmsg || document.code) {
|
||||
callback(new MongoServerError(document));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Pre 3.2 support
|
||||
if (document.ok === 0 || document.$err || document.errmsg) {
|
||||
callback(new MongoServerError(document));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(undefined, message.documents[0]);
|
||||
}
|
||||
|
||||
destroy(options?: DestroyOptions, callback?: Callback): void {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = { force: false };
|
||||
}
|
||||
|
||||
this.removeAllListeners(Connection.PINNED);
|
||||
this.removeAllListeners(Connection.UNPINNED);
|
||||
|
||||
options = Object.assign({ force: false }, options);
|
||||
if (this[kStream] == null || this.destroyed) {
|
||||
this.destroyed = true;
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.force) {
|
||||
this[kStream].destroy();
|
||||
this.destroyed = true;
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this[kStream].end(() => {
|
||||
this.destroyed = true;
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
command(
|
||||
ns: MongoDBNamespace,
|
||||
cmd: Document,
|
||||
options: CommandOptions | undefined,
|
||||
callback: Callback
|
||||
): void {
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
const shouldUseOpMsg = supportsOpMsg(this);
|
||||
const session = options?.session;
|
||||
|
||||
let clusterTime = this.clusterTime;
|
||||
let finalCmd = Object.assign({}, cmd);
|
||||
|
||||
if (this.serverApi) {
|
||||
const { version, strict, deprecationErrors } = this.serverApi;
|
||||
finalCmd.apiVersion = version;
|
||||
if (strict != null) finalCmd.apiStrict = strict;
|
||||
if (deprecationErrors != null) finalCmd.apiDeprecationErrors = deprecationErrors;
|
||||
}
|
||||
|
||||
if (hasSessionSupport(this) && session) {
|
||||
if (
|
||||
session.clusterTime &&
|
||||
clusterTime &&
|
||||
session.clusterTime.clusterTime.greaterThan(clusterTime.clusterTime)
|
||||
) {
|
||||
clusterTime = session.clusterTime;
|
||||
}
|
||||
|
||||
const err = applySession(session, finalCmd, options);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a known cluster time, gossip it
|
||||
if (clusterTime) {
|
||||
finalCmd.$clusterTime = clusterTime;
|
||||
}
|
||||
|
||||
if (isSharded(this) && !shouldUseOpMsg && readPreference && readPreference.mode !== 'primary') {
|
||||
finalCmd = {
|
||||
$query: finalCmd,
|
||||
$readPreference: readPreference.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
const commandOptions: Document = Object.assign(
|
||||
{
|
||||
command: true,
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false,
|
||||
// This value is not overridable
|
||||
secondaryOk: readPreference.secondaryOk()
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
const cmdNs = `${ns.db}.$cmd`;
|
||||
const message = shouldUseOpMsg
|
||||
? new Msg(cmdNs, finalCmd, commandOptions)
|
||||
: new Query(cmdNs, finalCmd, commandOptions);
|
||||
|
||||
try {
|
||||
write(this, message, commandOptions, callback);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class CryptoConnection extends Connection {
|
||||
/** @internal */
|
||||
[kAutoEncrypter]?: AutoEncrypter;
|
||||
|
||||
constructor(stream: Stream, options: ConnectionOptions) {
|
||||
super(stream, options);
|
||||
this[kAutoEncrypter] = options.autoEncrypter;
|
||||
}
|
||||
|
||||
/** @internal @override */
|
||||
override command(
|
||||
ns: MongoDBNamespace,
|
||||
cmd: Document,
|
||||
options: CommandOptions,
|
||||
callback: Callback
|
||||
): void {
|
||||
const autoEncrypter = this[kAutoEncrypter];
|
||||
if (!autoEncrypter) {
|
||||
return callback(new MongoMissingDependencyError('No AutoEncrypter available for encryption'));
|
||||
}
|
||||
|
||||
const serverWireVersion = maxWireVersion(this);
|
||||
if (serverWireVersion === 0) {
|
||||
// This means the initial handshake hasn't happened yet
|
||||
return super.command(ns, cmd, options, callback);
|
||||
}
|
||||
|
||||
if (serverWireVersion < 8) {
|
||||
callback(
|
||||
new MongoCompatibilityError('Auto-encryption requires a minimum MongoDB version of 4.2')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save sort or indexKeys based on the command being run
|
||||
// the encrypt API serializes our JS objects to BSON to pass to the native code layer
|
||||
// and then deserializes the encrypted result, the protocol level components
|
||||
// of the command (ex. sort) are then converted to JS objects potentially losing
|
||||
// import key order information. These fields are never encrypted so we can save the values
|
||||
// from before the encryption and replace them after encryption has been performed
|
||||
const sort: Map<string, number> | null = cmd.find || cmd.findAndModify ? cmd.sort : null;
|
||||
const indexKeys: Map<string, number>[] | null = cmd.createIndexes
|
||||
? cmd.indexes.map((index: { key: Map<string, number> }) => index.key)
|
||||
: null;
|
||||
|
||||
autoEncrypter.encrypt(ns.toString(), cmd, options, (err, encrypted) => {
|
||||
if (err || encrypted == null) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the saved values
|
||||
if (sort != null && (cmd.find || cmd.findAndModify)) {
|
||||
encrypted.sort = sort;
|
||||
}
|
||||
if (indexKeys != null && cmd.createIndexes) {
|
||||
for (const [offset, index] of indexKeys.entries()) {
|
||||
encrypted.indexes[offset].key = index;
|
||||
}
|
||||
}
|
||||
|
||||
super.command(ns, encrypted, options, (err, response) => {
|
||||
if (err || response == null) {
|
||||
callback(err, response);
|
||||
return;
|
||||
}
|
||||
|
||||
autoEncrypter.decrypt(response, options, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function hasSessionSupport(conn: Connection): boolean {
|
||||
const description = conn.description;
|
||||
return description.logicalSessionTimeoutMinutes != null || !!description.loadBalanced;
|
||||
}
|
||||
|
||||
function supportsOpMsg(conn: Connection) {
|
||||
const description = conn.description;
|
||||
if (description == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return maxWireVersion(conn) >= 6 && !description.__nodejs_mock_server__;
|
||||
}
|
||||
|
||||
function streamIdentifier(stream: Stream, options: ConnectionOptions): string {
|
||||
if (options.proxyHost) {
|
||||
// If proxy options are specified, the properties of `stream` itself
|
||||
// will not accurately reflect what endpoint this is connected to.
|
||||
return options.hostAddress.toString();
|
||||
}
|
||||
|
||||
if (typeof stream.address === 'function') {
|
||||
return `${stream.remoteAddress}:${stream.remotePort}`;
|
||||
}
|
||||
|
||||
return uuidV4().toString('hex');
|
||||
}
|
||||
|
||||
function write(
|
||||
conn: Connection,
|
||||
command: WriteProtocolMessageType,
|
||||
options: CommandOptions,
|
||||
callback: Callback
|
||||
) {
|
||||
options = options ?? {};
|
||||
const operationDescription: OperationDescription = {
|
||||
requestId: command.requestId,
|
||||
cb: callback,
|
||||
session: options.session,
|
||||
noResponse: typeof options.noResponse === 'boolean' ? options.noResponse : false,
|
||||
documentsReturnedIn: options.documentsReturnedIn,
|
||||
command: !!options.command,
|
||||
|
||||
// for BSON parsing
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
||||
bsonRegExp: typeof options.bsonRegExp === 'boolean' ? options.bsonRegExp : false,
|
||||
enableUtf8Validation:
|
||||
typeof options.enableUtf8Validation === 'boolean' ? options.enableUtf8Validation : true,
|
||||
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
||||
started: 0
|
||||
};
|
||||
|
||||
if (conn[kDescription] && conn[kDescription].compressor) {
|
||||
operationDescription.agreedCompressor = conn[kDescription].compressor;
|
||||
|
||||
if (conn[kDescription].zlibCompressionLevel) {
|
||||
operationDescription.zlibCompressionLevel = conn[kDescription].zlibCompressionLevel;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.socketTimeoutMS === 'number') {
|
||||
operationDescription.socketTimeoutOverride = true;
|
||||
conn[kStream].setTimeout(options.socketTimeoutMS);
|
||||
}
|
||||
|
||||
// if command monitoring is enabled we need to modify the callback here
|
||||
if (conn.monitorCommands) {
|
||||
conn.emit(Connection.COMMAND_STARTED, new CommandStartedEvent(conn, command));
|
||||
|
||||
operationDescription.started = now();
|
||||
operationDescription.cb = (err, reply) => {
|
||||
if (err) {
|
||||
conn.emit(
|
||||
Connection.COMMAND_FAILED,
|
||||
new CommandFailedEvent(conn, command, err, operationDescription.started)
|
||||
);
|
||||
} else {
|
||||
if (reply && (reply.ok === 0 || reply.$err)) {
|
||||
conn.emit(
|
||||
Connection.COMMAND_FAILED,
|
||||
new CommandFailedEvent(conn, command, reply, operationDescription.started)
|
||||
);
|
||||
} else {
|
||||
conn.emit(
|
||||
Connection.COMMAND_SUCCEEDED,
|
||||
new CommandSucceededEvent(conn, command, reply, operationDescription.started)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(err, reply);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!operationDescription.noResponse) {
|
||||
conn[kQueue].set(operationDescription.requestId, operationDescription);
|
||||
}
|
||||
|
||||
try {
|
||||
conn[kMessageStream].writeCommand(command, operationDescription);
|
||||
} catch (e) {
|
||||
if (!operationDescription.noResponse) {
|
||||
conn[kQueue].delete(operationDescription.requestId);
|
||||
operationDescription.cb(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (operationDescription.noResponse) {
|
||||
operationDescription.cb();
|
||||
}
|
||||
}
|
||||
+747
@@ -0,0 +1,747 @@
|
||||
import Denque = require('denque');
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
|
||||
import type { ObjectId } from '../bson';
|
||||
import {
|
||||
APM_EVENTS,
|
||||
CONNECTION_CHECK_OUT_FAILED,
|
||||
CONNECTION_CHECK_OUT_STARTED,
|
||||
CONNECTION_CHECKED_IN,
|
||||
CONNECTION_CHECKED_OUT,
|
||||
CONNECTION_CLOSED,
|
||||
CONNECTION_CREATED,
|
||||
CONNECTION_POOL_CLEARED,
|
||||
CONNECTION_POOL_CLOSED,
|
||||
CONNECTION_POOL_CREATED,
|
||||
CONNECTION_READY
|
||||
} from '../constants';
|
||||
import { MongoError, MongoInvalidArgumentError, MongoRuntimeError } from '../error';
|
||||
import { Logger } from '../logger';
|
||||
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
|
||||
import { Callback, eachAsync, makeCounter } from '../utils';
|
||||
import { connect } from './connect';
|
||||
import { Connection, ConnectionEvents, ConnectionOptions } from './connection';
|
||||
import {
|
||||
ConnectionCheckedInEvent,
|
||||
ConnectionCheckedOutEvent,
|
||||
ConnectionCheckOutFailedEvent,
|
||||
ConnectionCheckOutStartedEvent,
|
||||
ConnectionClosedEvent,
|
||||
ConnectionCreatedEvent,
|
||||
ConnectionPoolClearedEvent,
|
||||
ConnectionPoolClosedEvent,
|
||||
ConnectionPoolCreatedEvent,
|
||||
ConnectionReadyEvent
|
||||
} from './connection_pool_events';
|
||||
import { PoolClosedError, WaitQueueTimeoutError } from './errors';
|
||||
import { ConnectionPoolMetrics } from './metrics';
|
||||
|
||||
/** @internal */
|
||||
const kLogger = Symbol('logger');
|
||||
/** @internal */
|
||||
const kConnections = Symbol('connections');
|
||||
/** @internal */
|
||||
const kPending = Symbol('pending');
|
||||
/** @internal */
|
||||
const kCheckedOut = Symbol('checkedOut');
|
||||
/** @internal */
|
||||
const kMinPoolSizeTimer = Symbol('minPoolSizeTimer');
|
||||
/** @internal */
|
||||
const kGeneration = Symbol('generation');
|
||||
/** @internal */
|
||||
const kServiceGenerations = Symbol('serviceGenerations');
|
||||
/** @internal */
|
||||
const kConnectionCounter = Symbol('connectionCounter');
|
||||
/** @internal */
|
||||
const kCancellationToken = Symbol('cancellationToken');
|
||||
/** @internal */
|
||||
const kWaitQueue = Symbol('waitQueue');
|
||||
/** @internal */
|
||||
const kCancelled = Symbol('cancelled');
|
||||
/** @internal */
|
||||
const kMetrics = Symbol('metrics');
|
||||
/** @internal */
|
||||
const kProcessingWaitQueue = Symbol('processingWaitQueue');
|
||||
/** @internal */
|
||||
const kPoolState = Symbol('poolState');
|
||||
|
||||
/** @public */
|
||||
export interface ConnectionPoolOptions extends Omit<ConnectionOptions, 'id' | 'generation'> {
|
||||
/** The maximum number of connections that may be associated with a pool at a given time. This includes in use and available connections. */
|
||||
maxPoolSize: number;
|
||||
/** The minimum number of connections that MUST exist at any moment in a single connection pool. */
|
||||
minPoolSize: number;
|
||||
/** The maximum number of connections that may be in the process of being established concurrently by the connection pool. */
|
||||
maxConnecting: number;
|
||||
/** The maximum amount of time a connection should remain idle in the connection pool before being marked idle. */
|
||||
maxIdleTimeMS: number;
|
||||
/** The maximum amount of time operation execution should wait for a connection to become available. The default is 0 which means there is no limit. */
|
||||
waitQueueTimeoutMS: number;
|
||||
/** If we are in load balancer mode. */
|
||||
loadBalanced: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface WaitQueueMember {
|
||||
callback: Callback<Connection>;
|
||||
timer?: NodeJS.Timeout;
|
||||
[kCancelled]?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const PoolState = Object.freeze({
|
||||
paused: 'paused',
|
||||
ready: 'ready',
|
||||
closed: 'closed'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export interface CloseOptions {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ConnectionPoolEvents = {
|
||||
connectionPoolCreated(event: ConnectionPoolCreatedEvent): void;
|
||||
connectionPoolClosed(event: ConnectionPoolClosedEvent): void;
|
||||
connectionPoolCleared(event: ConnectionPoolClearedEvent): void;
|
||||
connectionCreated(event: ConnectionCreatedEvent): void;
|
||||
connectionReady(event: ConnectionReadyEvent): void;
|
||||
connectionClosed(event: ConnectionClosedEvent): void;
|
||||
connectionCheckOutStarted(event: ConnectionCheckOutStartedEvent): void;
|
||||
connectionCheckOutFailed(event: ConnectionCheckOutFailedEvent): void;
|
||||
connectionCheckedOut(event: ConnectionCheckedOutEvent): void;
|
||||
connectionCheckedIn(event: ConnectionCheckedInEvent): void;
|
||||
} & Omit<ConnectionEvents, 'close' | 'message'>;
|
||||
|
||||
/**
|
||||
* A pool of connections which dynamically resizes, and emit events related to pool activity
|
||||
* @internal
|
||||
*/
|
||||
export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
|
||||
options: Readonly<ConnectionPoolOptions>;
|
||||
/** @internal */
|
||||
[kPoolState]: typeof PoolState[keyof typeof PoolState];
|
||||
/** @internal */
|
||||
[kLogger]: Logger;
|
||||
/** @internal */
|
||||
[kConnections]: Denque<Connection>;
|
||||
/** @internal */
|
||||
[kPending]: number;
|
||||
/** @internal */
|
||||
[kCheckedOut]: number;
|
||||
/** @internal */
|
||||
[kMinPoolSizeTimer]?: NodeJS.Timeout;
|
||||
/**
|
||||
* An integer representing the SDAM generation of the pool
|
||||
* @internal
|
||||
*/
|
||||
[kGeneration]: number;
|
||||
/** A map of generations to service ids
|
||||
* @internal
|
||||
*/
|
||||
[kServiceGenerations]: Map<string, number>;
|
||||
/** @internal */
|
||||
[kConnectionCounter]: Generator<number>;
|
||||
/** @internal */
|
||||
[kCancellationToken]: CancellationToken;
|
||||
/** @internal */
|
||||
[kWaitQueue]: Denque<WaitQueueMember>;
|
||||
/** @internal */
|
||||
[kMetrics]: ConnectionPoolMetrics;
|
||||
/** @internal */
|
||||
[kProcessingWaitQueue]: boolean;
|
||||
|
||||
/**
|
||||
* Emitted when the connection pool is created.
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_POOL_CREATED = CONNECTION_POOL_CREATED;
|
||||
/**
|
||||
* Emitted once when the connection pool is closed
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_POOL_CLOSED = CONNECTION_POOL_CLOSED;
|
||||
/**
|
||||
* Emitted each time the connection pool is cleared and it's generation incremented
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_POOL_CLEARED = CONNECTION_POOL_CLEARED;
|
||||
/**
|
||||
* Emitted when a connection is created.
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_CREATED = CONNECTION_CREATED;
|
||||
/**
|
||||
* Emitted when a connection becomes established, and is ready to use
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_READY = CONNECTION_READY;
|
||||
/**
|
||||
* Emitted when a connection is closed
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_CLOSED = CONNECTION_CLOSED;
|
||||
/**
|
||||
* Emitted when an attempt to check out a connection begins
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_CHECK_OUT_STARTED = CONNECTION_CHECK_OUT_STARTED;
|
||||
/**
|
||||
* Emitted when an attempt to check out a connection fails
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_CHECK_OUT_FAILED = CONNECTION_CHECK_OUT_FAILED;
|
||||
/**
|
||||
* Emitted each time a connection is successfully checked out of the connection pool
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_CHECKED_OUT = CONNECTION_CHECKED_OUT;
|
||||
/**
|
||||
* Emitted each time a connection is successfully checked into the connection pool
|
||||
* @event
|
||||
*/
|
||||
static readonly CONNECTION_CHECKED_IN = CONNECTION_CHECKED_IN;
|
||||
|
||||
/** @internal */
|
||||
constructor(options: ConnectionPoolOptions) {
|
||||
super();
|
||||
|
||||
this.options = Object.freeze({
|
||||
...options,
|
||||
connectionType: Connection,
|
||||
maxPoolSize: options.maxPoolSize ?? 100,
|
||||
minPoolSize: options.minPoolSize ?? 0,
|
||||
maxConnecting: options.maxConnecting ?? 2,
|
||||
maxIdleTimeMS: options.maxIdleTimeMS ?? 0,
|
||||
waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0,
|
||||
autoEncrypter: options.autoEncrypter,
|
||||
metadata: options.metadata
|
||||
});
|
||||
|
||||
if (this.options.minPoolSize > this.options.maxPoolSize) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
'Connection pool minimum size must not be greater than maximum pool size'
|
||||
);
|
||||
}
|
||||
|
||||
this[kPoolState] = PoolState.paused;
|
||||
this[kLogger] = new Logger('ConnectionPool');
|
||||
this[kConnections] = new Denque();
|
||||
this[kPending] = 0;
|
||||
this[kCheckedOut] = 0;
|
||||
this[kMinPoolSizeTimer] = undefined;
|
||||
this[kGeneration] = 0;
|
||||
this[kServiceGenerations] = new Map();
|
||||
this[kConnectionCounter] = makeCounter(1);
|
||||
this[kCancellationToken] = new CancellationToken();
|
||||
this[kCancellationToken].setMaxListeners(Infinity);
|
||||
this[kWaitQueue] = new Denque();
|
||||
this[kMetrics] = new ConnectionPoolMetrics();
|
||||
this[kProcessingWaitQueue] = false;
|
||||
|
||||
process.nextTick(() => {
|
||||
this.emit(ConnectionPool.CONNECTION_POOL_CREATED, new ConnectionPoolCreatedEvent(this));
|
||||
this.ensureMinPoolSize();
|
||||
});
|
||||
}
|
||||
|
||||
/** The address of the endpoint the pool is connected to */
|
||||
get address(): string {
|
||||
return this.options.hostAddress.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the pool has been closed
|
||||
*
|
||||
* TODO(NODE-3263): We can remove this property once shell no longer needs it
|
||||
*/
|
||||
get closed(): boolean {
|
||||
return this[kPoolState] === PoolState.closed;
|
||||
}
|
||||
|
||||
/** An integer representing the SDAM generation of the pool */
|
||||
get generation(): number {
|
||||
return this[kGeneration];
|
||||
}
|
||||
|
||||
/** An integer expressing how many total connections (available + pending + in use) the pool currently has */
|
||||
get totalConnectionCount(): number {
|
||||
return (
|
||||
this.availableConnectionCount + this.pendingConnectionCount + this.currentCheckedOutCount
|
||||
);
|
||||
}
|
||||
|
||||
/** An integer expressing how many connections are currently available in the pool. */
|
||||
get availableConnectionCount(): number {
|
||||
return this[kConnections].length;
|
||||
}
|
||||
|
||||
get pendingConnectionCount(): number {
|
||||
return this[kPending];
|
||||
}
|
||||
|
||||
get currentCheckedOutCount(): number {
|
||||
return this[kCheckedOut];
|
||||
}
|
||||
|
||||
get waitQueueSize(): number {
|
||||
return this[kWaitQueue].length;
|
||||
}
|
||||
|
||||
get loadBalanced(): boolean {
|
||||
return this.options.loadBalanced;
|
||||
}
|
||||
|
||||
get serviceGenerations(): Map<string, number> {
|
||||
return this[kServiceGenerations];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metrics information for the pool when a wait queue timeout occurs.
|
||||
*/
|
||||
private waitQueueErrorMetrics(): string {
|
||||
return this[kMetrics].info(this.options.maxPoolSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the pool state to "ready"
|
||||
*/
|
||||
ready(): void {
|
||||
this[kPoolState] = PoolState.ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a connection out of this pool. The connection will continue to be tracked, but no reference to it
|
||||
* will be held by the pool. This means that if a connection is checked out it MUST be checked back in or
|
||||
* explicitly destroyed by the new owner.
|
||||
*/
|
||||
checkOut(callback: Callback<Connection>): void {
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CHECK_OUT_STARTED,
|
||||
new ConnectionCheckOutStartedEvent(this)
|
||||
);
|
||||
|
||||
if (this.closed) {
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CHECK_OUT_FAILED,
|
||||
new ConnectionCheckOutFailedEvent(this, 'poolClosed')
|
||||
);
|
||||
callback(new PoolClosedError(this));
|
||||
return;
|
||||
}
|
||||
|
||||
const waitQueueMember: WaitQueueMember = { callback };
|
||||
const waitQueueTimeoutMS = this.options.waitQueueTimeoutMS;
|
||||
if (waitQueueTimeoutMS) {
|
||||
waitQueueMember.timer = setTimeout(() => {
|
||||
waitQueueMember[kCancelled] = true;
|
||||
waitQueueMember.timer = undefined;
|
||||
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CHECK_OUT_FAILED,
|
||||
new ConnectionCheckOutFailedEvent(this, 'timeout')
|
||||
);
|
||||
waitQueueMember.callback(
|
||||
new WaitQueueTimeoutError(
|
||||
this.loadBalanced
|
||||
? this.waitQueueErrorMetrics()
|
||||
: 'Timed out while checking out a connection from connection pool',
|
||||
this.address
|
||||
)
|
||||
);
|
||||
}, waitQueueTimeoutMS);
|
||||
}
|
||||
|
||||
this[kWaitQueue].push(waitQueueMember);
|
||||
process.nextTick(() => this.processWaitQueue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a connection into the pool.
|
||||
*
|
||||
* @param connection - The connection to check in
|
||||
*/
|
||||
checkIn(connection: Connection): void {
|
||||
const poolClosed = this.closed;
|
||||
const stale = this.connectionIsStale(connection);
|
||||
const willDestroy = !!(poolClosed || stale || connection.closed);
|
||||
|
||||
if (!willDestroy) {
|
||||
connection.markAvailable();
|
||||
this[kConnections].unshift(connection);
|
||||
}
|
||||
|
||||
this[kCheckedOut]--;
|
||||
this.emit(ConnectionPool.CONNECTION_CHECKED_IN, new ConnectionCheckedInEvent(this, connection));
|
||||
|
||||
if (willDestroy) {
|
||||
const reason = connection.closed ? 'error' : poolClosed ? 'poolClosed' : 'stale';
|
||||
this.destroyConnection(connection, reason);
|
||||
}
|
||||
|
||||
process.nextTick(() => this.processWaitQueue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the pool
|
||||
*
|
||||
* Pool reset is handled by incrementing the pool's generation count. Any existing connection of a
|
||||
* previous generation will eventually be pruned during subsequent checkouts.
|
||||
*/
|
||||
clear(serviceId?: ObjectId): void {
|
||||
if (this.loadBalanced && serviceId) {
|
||||
const sid = serviceId.toHexString();
|
||||
const generation = this.serviceGenerations.get(sid);
|
||||
// Only need to worry if the generation exists, since it should
|
||||
// always be there but typescript needs the check.
|
||||
if (generation == null) {
|
||||
// TODO(NODE-3483)
|
||||
throw new MongoRuntimeError('Service generations are required in load balancer mode.');
|
||||
} else {
|
||||
// Increment the generation for the service id.
|
||||
this.serviceGenerations.set(sid, generation + 1);
|
||||
}
|
||||
} else {
|
||||
this[kGeneration] += 1;
|
||||
}
|
||||
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_POOL_CLEARED,
|
||||
new ConnectionPoolClearedEvent(this, serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
/** Close the pool */
|
||||
close(callback: Callback<void>): void;
|
||||
close(options: CloseOptions, callback: Callback<void>): void;
|
||||
close(_options?: CloseOptions | Callback<void>, _cb?: Callback<void>): void {
|
||||
let options = _options as CloseOptions;
|
||||
const callback = (_cb ?? _options) as Callback<void>;
|
||||
if (typeof options === 'function') {
|
||||
options = {};
|
||||
}
|
||||
|
||||
options = Object.assign({ force: false }, options);
|
||||
if (this.closed) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// immediately cancel any in-flight connections
|
||||
this[kCancellationToken].emit('cancel');
|
||||
|
||||
// drain the wait queue
|
||||
while (this.waitQueueSize) {
|
||||
const waitQueueMember = this[kWaitQueue].pop();
|
||||
if (waitQueueMember) {
|
||||
if (waitQueueMember.timer) {
|
||||
clearTimeout(waitQueueMember.timer);
|
||||
}
|
||||
if (!waitQueueMember[kCancelled]) {
|
||||
// TODO(NODE-3483): Replace with MongoConnectionPoolClosedError
|
||||
waitQueueMember.callback(new MongoRuntimeError('Connection pool closed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clear the min pool size timer
|
||||
const minPoolSizeTimer = this[kMinPoolSizeTimer];
|
||||
if (minPoolSizeTimer) {
|
||||
clearTimeout(minPoolSizeTimer);
|
||||
}
|
||||
|
||||
// end the connection counter
|
||||
if (typeof this[kConnectionCounter].return === 'function') {
|
||||
this[kConnectionCounter].return(undefined);
|
||||
}
|
||||
|
||||
// mark the pool as closed immediately
|
||||
this[kPoolState] = PoolState.closed;
|
||||
eachAsync<Connection>(
|
||||
this[kConnections].toArray(),
|
||||
(conn, cb) => {
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CLOSED,
|
||||
new ConnectionClosedEvent(this, conn, 'poolClosed')
|
||||
);
|
||||
conn.destroy(options, cb);
|
||||
},
|
||||
err => {
|
||||
this[kConnections].clear();
|
||||
this.emit(ConnectionPool.CONNECTION_POOL_CLOSED, new ConnectionPoolClosedEvent(this));
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a lambda with an implicitly checked out connection, checking that connection back in when the lambda
|
||||
* has completed by calling back.
|
||||
*
|
||||
* NOTE: please note the required signature of `fn`
|
||||
*
|
||||
* @remarks When in load balancer mode, connections can be pinned to cursors or transactions.
|
||||
* In these cases we pass the connection in to this method to ensure it is used and a new
|
||||
* connection is not checked out.
|
||||
*
|
||||
* @param conn - A pinned connection for use in load balancing mode.
|
||||
* @param fn - A function which operates on a managed connection
|
||||
* @param callback - The original callback
|
||||
*/
|
||||
withConnection(
|
||||
conn: Connection | undefined,
|
||||
fn: WithConnectionCallback,
|
||||
callback?: Callback<Connection>
|
||||
): void {
|
||||
if (conn) {
|
||||
// use the provided connection, and do _not_ check it in after execution
|
||||
fn(undefined, conn, (fnErr, result) => {
|
||||
if (typeof callback === 'function') {
|
||||
if (fnErr) {
|
||||
callback(fnErr);
|
||||
} else {
|
||||
callback(undefined, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkOut((err, conn) => {
|
||||
// don't callback with `err` here, we might want to act upon it inside `fn`
|
||||
fn(err as MongoError, conn, (fnErr, result) => {
|
||||
if (typeof callback === 'function') {
|
||||
if (fnErr) {
|
||||
callback(fnErr);
|
||||
} else {
|
||||
callback(undefined, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (conn) {
|
||||
this.checkIn(conn);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private destroyConnection(connection: Connection, reason: string) {
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CLOSED,
|
||||
new ConnectionClosedEvent(this, connection, reason)
|
||||
);
|
||||
|
||||
// destroy the connection
|
||||
process.nextTick(() => connection.destroy());
|
||||
}
|
||||
|
||||
private connectionIsStale(connection: Connection) {
|
||||
const serviceId = connection.serviceId;
|
||||
if (this.loadBalanced && serviceId) {
|
||||
const sid = serviceId.toHexString();
|
||||
const generation = this.serviceGenerations.get(sid);
|
||||
return connection.generation !== generation;
|
||||
}
|
||||
|
||||
return connection.generation !== this[kGeneration];
|
||||
}
|
||||
|
||||
private connectionIsIdle(connection: Connection) {
|
||||
return !!(this.options.maxIdleTimeMS && connection.idleTime > this.options.maxIdleTimeMS);
|
||||
}
|
||||
|
||||
private connectionIsPerished(connection: Connection) {
|
||||
const isStale = this.connectionIsStale(connection);
|
||||
const isIdle = this.connectionIsIdle(connection);
|
||||
if (!isStale && !isIdle && !connection.closed) {
|
||||
return false;
|
||||
}
|
||||
const reason = connection.closed ? 'error' : isStale ? 'stale' : 'idle';
|
||||
this.destroyConnection(connection, reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
private createConnection(callback: Callback<Connection>) {
|
||||
const connectOptions: ConnectionOptions = {
|
||||
...this.options,
|
||||
id: this[kConnectionCounter].next().value,
|
||||
generation: this[kGeneration],
|
||||
cancellationToken: this[kCancellationToken]
|
||||
};
|
||||
|
||||
this[kPending]++;
|
||||
// This is our version of a "virtual" no-I/O connection as the spec requires
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CREATED,
|
||||
new ConnectionCreatedEvent(this, { id: connectOptions.id })
|
||||
);
|
||||
|
||||
connect(connectOptions, (err, connection) => {
|
||||
if (err || !connection) {
|
||||
this[kLogger].debug(`connection attempt failed with error [${JSON.stringify(err)}]`);
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// The pool might have closed since we started trying to create a connection
|
||||
if (this.closed) {
|
||||
this[kPending]--;
|
||||
connection.destroy({ force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// forward all events from the connection to the pool
|
||||
for (const event of [...APM_EVENTS, Connection.CLUSTER_TIME_RECEIVED]) {
|
||||
connection.on(event, (e: any) => this.emit(event, e));
|
||||
}
|
||||
|
||||
if (this.loadBalanced) {
|
||||
connection.on(Connection.PINNED, pinType => this[kMetrics].markPinned(pinType));
|
||||
connection.on(Connection.UNPINNED, pinType => this[kMetrics].markUnpinned(pinType));
|
||||
|
||||
const serviceId = connection.serviceId;
|
||||
if (serviceId) {
|
||||
let generation;
|
||||
const sid = serviceId.toHexString();
|
||||
if ((generation = this.serviceGenerations.get(sid))) {
|
||||
connection.generation = generation;
|
||||
} else {
|
||||
this.serviceGenerations.set(sid, 0);
|
||||
connection.generation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connection.markAvailable();
|
||||
this.emit(ConnectionPool.CONNECTION_READY, new ConnectionReadyEvent(this, connection));
|
||||
|
||||
callback(undefined, connection);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
private ensureMinPoolSize() {
|
||||
const minPoolSize = this.options.minPoolSize;
|
||||
if (this.closed || minPoolSize === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.totalConnectionCount < minPoolSize &&
|
||||
this.pendingConnectionCount < this.options.maxConnecting
|
||||
) {
|
||||
// NOTE: ensureMinPoolSize should not try to get all the pending
|
||||
// connection permits because that potentially delays the availability of
|
||||
// the connection to a checkout request
|
||||
this.createConnection((err, connection) => {
|
||||
this[kPending]--;
|
||||
if (!err && connection) {
|
||||
this[kConnections].push(connection);
|
||||
process.nextTick(() => this.processWaitQueue());
|
||||
}
|
||||
this[kMinPoolSizeTimer] = setTimeout(() => this.ensureMinPoolSize(), 10);
|
||||
});
|
||||
} else {
|
||||
this[kMinPoolSizeTimer] = setTimeout(() => this.ensureMinPoolSize(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
private processWaitQueue() {
|
||||
if (this.closed || this[kProcessingWaitQueue]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[kProcessingWaitQueue] = true;
|
||||
|
||||
while (this.waitQueueSize) {
|
||||
const waitQueueMember = this[kWaitQueue].peekFront();
|
||||
if (!waitQueueMember) {
|
||||
this[kWaitQueue].shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitQueueMember[kCancelled]) {
|
||||
this[kWaitQueue].shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.availableConnectionCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
const connection = this[kConnections].shift();
|
||||
if (!connection) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!this.connectionIsPerished(connection)) {
|
||||
this[kCheckedOut]++;
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CHECKED_OUT,
|
||||
new ConnectionCheckedOutEvent(this, connection)
|
||||
);
|
||||
if (waitQueueMember.timer) {
|
||||
clearTimeout(waitQueueMember.timer);
|
||||
}
|
||||
|
||||
this[kWaitQueue].shift();
|
||||
waitQueueMember.callback(undefined, connection);
|
||||
}
|
||||
}
|
||||
|
||||
const { maxPoolSize, maxConnecting } = this.options;
|
||||
while (
|
||||
this.waitQueueSize > 0 &&
|
||||
this.pendingConnectionCount < maxConnecting &&
|
||||
(maxPoolSize === 0 || this.totalConnectionCount < maxPoolSize)
|
||||
) {
|
||||
const waitQueueMember = this[kWaitQueue].shift();
|
||||
if (!waitQueueMember || waitQueueMember[kCancelled]) {
|
||||
continue;
|
||||
}
|
||||
this.createConnection((err, connection) => {
|
||||
this[kPending]--;
|
||||
if (waitQueueMember[kCancelled]) {
|
||||
if (!err && connection) {
|
||||
this[kConnections].push(connection);
|
||||
}
|
||||
} else {
|
||||
if (err) {
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CHECK_OUT_FAILED,
|
||||
new ConnectionCheckOutFailedEvent(this, err)
|
||||
);
|
||||
} else if (connection) {
|
||||
this[kCheckedOut]++;
|
||||
this.emit(
|
||||
ConnectionPool.CONNECTION_CHECKED_OUT,
|
||||
new ConnectionCheckedOutEvent(this, connection)
|
||||
);
|
||||
}
|
||||
|
||||
if (waitQueueMember.timer) {
|
||||
clearTimeout(waitQueueMember.timer);
|
||||
}
|
||||
waitQueueMember.callback(err, connection);
|
||||
}
|
||||
process.nextTick(() => this.processWaitQueue());
|
||||
});
|
||||
}
|
||||
this[kProcessingWaitQueue] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback provided to `withConnection`
|
||||
* @internal
|
||||
*
|
||||
* @param error - An error instance representing the error during the execution.
|
||||
* @param connection - The managed connection which was checked out of the pool.
|
||||
* @param callback - A function to call back after connection management is complete
|
||||
*/
|
||||
export type WithConnectionCallback = (
|
||||
error: MongoError | undefined,
|
||||
connection: Connection | undefined,
|
||||
callback: Callback<Connection>
|
||||
) => void;
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import type { ObjectId } from '../bson';
|
||||
import type { AnyError } from '../error';
|
||||
import type { Connection } from './connection';
|
||||
import type { ConnectionPool, ConnectionPoolOptions } from './connection_pool';
|
||||
|
||||
/**
|
||||
* The base export class for all monitoring events published from the connection pool
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionPoolMonitoringEvent {
|
||||
/** A timestamp when the event was created */
|
||||
time: Date;
|
||||
/** The address (host/port pair) of the pool */
|
||||
address: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool) {
|
||||
this.time = new Date();
|
||||
this.address = pool.address;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection pool is created
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionPoolCreatedEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** The options used to create this connection pool */
|
||||
options?: ConnectionPoolOptions;
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool) {
|
||||
super(pool);
|
||||
this.options = pool.options;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection pool is closed
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionPoolClosedEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool) {
|
||||
super(pool);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection pool creates a new connection
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionCreatedEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** A monotonically increasing, per-pool id for the newly created connection */
|
||||
connectionId: number | '<monitor>';
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool, connection: { id: number | '<monitor>' }) {
|
||||
super(pool);
|
||||
this.connectionId = connection.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection is ready for use
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionReadyEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** The id of the connection */
|
||||
connectionId: number | '<monitor>';
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool, connection: Connection) {
|
||||
super(pool);
|
||||
this.connectionId = connection.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection is closed
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionClosedEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** The id of the connection */
|
||||
connectionId: number | '<monitor>';
|
||||
/** The reason the connection was closed */
|
||||
reason: string;
|
||||
serviceId?: ObjectId;
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool, connection: Connection, reason: string) {
|
||||
super(pool);
|
||||
this.connectionId = connection.id;
|
||||
this.reason = reason || 'unknown';
|
||||
this.serviceId = connection.serviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a request to check a connection out begins
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionCheckOutStartedEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool) {
|
||||
super(pool);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a request to check a connection out fails
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionCheckOutFailedEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** The reason the attempt to check out failed */
|
||||
reason: AnyError | string;
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool, reason: AnyError | string) {
|
||||
super(pool);
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection is checked out of the connection pool
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionCheckedOutEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** The id of the connection */
|
||||
connectionId: number | '<monitor>';
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool, connection: Connection) {
|
||||
super(pool);
|
||||
this.connectionId = connection.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection is checked into the connection pool
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionCheckedInEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** The id of the connection */
|
||||
connectionId: number | '<monitor>';
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool, connection: Connection) {
|
||||
super(pool);
|
||||
this.connectionId = connection.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event published when a connection pool is cleared
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ConnectionPoolClearedEvent extends ConnectionPoolMonitoringEvent {
|
||||
/** @internal */
|
||||
serviceId?: ObjectId;
|
||||
|
||||
/** @internal */
|
||||
constructor(pool: ConnectionPool, serviceId?: ObjectId) {
|
||||
super(pool);
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { MongoDriverError } from '../error';
|
||||
import type { ConnectionPool } from './connection_pool';
|
||||
|
||||
/**
|
||||
* An error indicating a connection pool is closed
|
||||
* @category Error
|
||||
*/
|
||||
export class PoolClosedError extends MongoDriverError {
|
||||
/** The address of the connection pool */
|
||||
address: string;
|
||||
|
||||
constructor(pool: ConnectionPool) {
|
||||
super('Attempted to check out a connection from closed connection pool');
|
||||
this.address = pool.address;
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoPoolClosedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when a request to check out a connection times out
|
||||
* @category Error
|
||||
*/
|
||||
export class WaitQueueTimeoutError extends MongoDriverError {
|
||||
/** The address of the connection pool */
|
||||
address: string;
|
||||
|
||||
constructor(message: string, address: string) {
|
||||
super(message);
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoWaitQueueTimeoutError';
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { Duplex, DuplexOptions } from 'stream';
|
||||
|
||||
import type { BSONSerializeOptions, Document } from '../bson';
|
||||
import { MongoDecompressionError, MongoParseError } from '../error';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { BufferPool, Callback } from '../utils';
|
||||
import { BinMsg, MessageHeader, Msg, Response, WriteProtocolMessageType } from './commands';
|
||||
import {
|
||||
compress,
|
||||
Compressor,
|
||||
CompressorName,
|
||||
decompress,
|
||||
uncompressibleCommands
|
||||
} from './wire_protocol/compression';
|
||||
import { OP_COMPRESSED, OP_MSG } from './wire_protocol/constants';
|
||||
|
||||
const MESSAGE_HEADER_SIZE = 16;
|
||||
const COMPRESSION_DETAILS_SIZE = 9; // originalOpcode + uncompressedSize, compressorID
|
||||
|
||||
const kDefaultMaxBsonMessageSize = 1024 * 1024 * 16 * 4;
|
||||
/** @internal */
|
||||
const kBuffer = Symbol('buffer');
|
||||
|
||||
/** @internal */
|
||||
export interface MessageStreamOptions extends DuplexOptions {
|
||||
maxBsonMessageSize?: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface OperationDescription extends BSONSerializeOptions {
|
||||
started: number;
|
||||
cb: Callback<Document>;
|
||||
command: boolean;
|
||||
documentsReturnedIn?: string;
|
||||
noResponse: boolean;
|
||||
raw: boolean;
|
||||
requestId: number;
|
||||
session?: ClientSession;
|
||||
socketTimeoutOverride?: boolean;
|
||||
agreedCompressor?: CompressorName;
|
||||
zlibCompressionLevel?: number;
|
||||
$clusterTime?: Document;
|
||||
}
|
||||
|
||||
/**
|
||||
* A duplex stream that is capable of reading and writing raw wire protocol messages, with
|
||||
* support for optional compression
|
||||
* @internal
|
||||
*/
|
||||
export class MessageStream extends Duplex {
|
||||
/** @internal */
|
||||
maxBsonMessageSize: number;
|
||||
/** @internal */
|
||||
[kBuffer]: BufferPool;
|
||||
/** @internal */
|
||||
isMonitoringConnection = false;
|
||||
|
||||
constructor(options: MessageStreamOptions = {}) {
|
||||
super(options);
|
||||
this.maxBsonMessageSize = options.maxBsonMessageSize || kDefaultMaxBsonMessageSize;
|
||||
this[kBuffer] = new BufferPool();
|
||||
}
|
||||
|
||||
get buffer(): BufferPool {
|
||||
return this[kBuffer];
|
||||
}
|
||||
|
||||
override _write(chunk: Buffer, _: unknown, callback: Callback<Buffer>): void {
|
||||
this[kBuffer].append(chunk);
|
||||
processIncomingData(this, callback);
|
||||
}
|
||||
|
||||
override _read(/* size */): void {
|
||||
// NOTE: This implementation is empty because we explicitly push data to be read
|
||||
// when `writeMessage` is called.
|
||||
return;
|
||||
}
|
||||
|
||||
writeCommand(
|
||||
command: WriteProtocolMessageType,
|
||||
operationDescription: OperationDescription
|
||||
): void {
|
||||
// TODO: agreed compressor should live in `StreamDescription`
|
||||
const compressorName: CompressorName =
|
||||
operationDescription && operationDescription.agreedCompressor
|
||||
? operationDescription.agreedCompressor
|
||||
: 'none';
|
||||
if (compressorName === 'none' || !canCompress(command)) {
|
||||
const data = command.toBin();
|
||||
this.push(Array.isArray(data) ? Buffer.concat(data) : data);
|
||||
return;
|
||||
}
|
||||
// otherwise, compress the message
|
||||
const concatenatedOriginalCommandBuffer = Buffer.concat(command.toBin());
|
||||
const messageToBeCompressed = concatenatedOriginalCommandBuffer.slice(MESSAGE_HEADER_SIZE);
|
||||
|
||||
// Extract information needed for OP_COMPRESSED from the uncompressed message
|
||||
const originalCommandOpCode = concatenatedOriginalCommandBuffer.readInt32LE(12);
|
||||
|
||||
// Compress the message body
|
||||
compress({ options: operationDescription }, messageToBeCompressed, (err, compressedMessage) => {
|
||||
if (err || !compressedMessage) {
|
||||
operationDescription.cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the msgHeader of OP_COMPRESSED
|
||||
const msgHeader = Buffer.alloc(MESSAGE_HEADER_SIZE);
|
||||
msgHeader.writeInt32LE(
|
||||
MESSAGE_HEADER_SIZE + COMPRESSION_DETAILS_SIZE + compressedMessage.length,
|
||||
0
|
||||
); // messageLength
|
||||
msgHeader.writeInt32LE(command.requestId, 4); // requestID
|
||||
msgHeader.writeInt32LE(0, 8); // responseTo (zero)
|
||||
msgHeader.writeInt32LE(OP_COMPRESSED, 12); // opCode
|
||||
|
||||
// Create the compression details of OP_COMPRESSED
|
||||
const compressionDetails = Buffer.alloc(COMPRESSION_DETAILS_SIZE);
|
||||
compressionDetails.writeInt32LE(originalCommandOpCode, 0); // originalOpcode
|
||||
compressionDetails.writeInt32LE(messageToBeCompressed.length, 4); // Size of the uncompressed compressedMessage, excluding the MsgHeader
|
||||
compressionDetails.writeUInt8(Compressor[compressorName], 8); // compressorID
|
||||
this.push(Buffer.concat([msgHeader, compressionDetails, compressedMessage]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Return whether a command contains an uncompressible command term
|
||||
// Will return true if command contains no uncompressible command terms
|
||||
function canCompress(command: WriteProtocolMessageType) {
|
||||
const commandDoc = command instanceof Msg ? command.command : command.query;
|
||||
const commandName = Object.keys(commandDoc)[0];
|
||||
return !uncompressibleCommands.has(commandName);
|
||||
}
|
||||
|
||||
function processIncomingData(stream: MessageStream, callback: Callback<Buffer>) {
|
||||
const buffer = stream[kBuffer];
|
||||
if (buffer.length < 4) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const sizeOfMessage = buffer.peek(4).readInt32LE();
|
||||
if (sizeOfMessage < 0) {
|
||||
callback(new MongoParseError(`Invalid message size: ${sizeOfMessage}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sizeOfMessage > stream.maxBsonMessageSize) {
|
||||
callback(
|
||||
new MongoParseError(
|
||||
`Invalid message size: ${sizeOfMessage}, max allowed: ${stream.maxBsonMessageSize}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sizeOfMessage > buffer.length) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const message = buffer.read(sizeOfMessage);
|
||||
const messageHeader: MessageHeader = {
|
||||
length: message.readInt32LE(0),
|
||||
requestId: message.readInt32LE(4),
|
||||
responseTo: message.readInt32LE(8),
|
||||
opCode: message.readInt32LE(12)
|
||||
};
|
||||
|
||||
const monitorHasAnotherHello = () => {
|
||||
if (stream.isMonitoringConnection) {
|
||||
// Can we read the next message size?
|
||||
if (buffer.length >= 4) {
|
||||
const sizeOfMessage = buffer.peek(4).readInt32LE();
|
||||
if (sizeOfMessage <= buffer.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let ResponseType = messageHeader.opCode === OP_MSG ? BinMsg : Response;
|
||||
if (messageHeader.opCode !== OP_COMPRESSED) {
|
||||
const messageBody = message.slice(MESSAGE_HEADER_SIZE);
|
||||
|
||||
// If we are a monitoring connection message stream and
|
||||
// there is more in the buffer that can be read, skip processing since we
|
||||
// want the last hello command response that is in the buffer.
|
||||
if (monitorHasAnotherHello()) {
|
||||
processIncomingData(stream, callback);
|
||||
} else {
|
||||
stream.emit('message', new ResponseType(message, messageHeader, messageBody));
|
||||
|
||||
if (buffer.length >= 4) {
|
||||
processIncomingData(stream, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
messageHeader.fromCompressed = true;
|
||||
messageHeader.opCode = message.readInt32LE(MESSAGE_HEADER_SIZE);
|
||||
messageHeader.length = message.readInt32LE(MESSAGE_HEADER_SIZE + 4);
|
||||
const compressorID: Compressor = message[MESSAGE_HEADER_SIZE + 8] as Compressor;
|
||||
const compressedBuffer = message.slice(MESSAGE_HEADER_SIZE + 9);
|
||||
|
||||
// recalculate based on wrapped opcode
|
||||
ResponseType = messageHeader.opCode === OP_MSG ? BinMsg : Response;
|
||||
decompress(compressorID, compressedBuffer, (err, messageBody) => {
|
||||
if (err || !messageBody) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageBody.length !== messageHeader.length) {
|
||||
callback(
|
||||
new MongoDecompressionError('Message body and message header must be the same length')
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are a monitoring connection message stream and
|
||||
// there is more in the buffer that can be read, skip processing since we
|
||||
// want the last hello command response that is in the buffer.
|
||||
if (monitorHasAnotherHello()) {
|
||||
processIncomingData(stream, callback);
|
||||
} else {
|
||||
stream.emit('message', new ResponseType(message, messageHeader, messageBody));
|
||||
|
||||
if (buffer.length >= 4) {
|
||||
processIncomingData(stream, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/** @internal */
|
||||
export class ConnectionPoolMetrics {
|
||||
static readonly TXN = 'txn' as const;
|
||||
static readonly CURSOR = 'cursor' as const;
|
||||
static readonly OTHER = 'other' as const;
|
||||
|
||||
txnConnections = 0;
|
||||
cursorConnections = 0;
|
||||
otherConnections = 0;
|
||||
|
||||
/**
|
||||
* Mark a connection as pinned for a specific operation.
|
||||
*/
|
||||
markPinned(pinType: string): void {
|
||||
if (pinType === ConnectionPoolMetrics.TXN) {
|
||||
this.txnConnections += 1;
|
||||
} else if (pinType === ConnectionPoolMetrics.CURSOR) {
|
||||
this.cursorConnections += 1;
|
||||
} else {
|
||||
this.otherConnections += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark a connection as pinned for an operation.
|
||||
*/
|
||||
markUnpinned(pinType: string): void {
|
||||
if (pinType === ConnectionPoolMetrics.TXN) {
|
||||
this.txnConnections -= 1;
|
||||
} else if (pinType === ConnectionPoolMetrics.CURSOR) {
|
||||
this.cursorConnections -= 1;
|
||||
} else {
|
||||
this.otherConnections -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return information about the cmap metrics as a string.
|
||||
*/
|
||||
info(maxPoolSize: number): string {
|
||||
return (
|
||||
'Timed out while checking out a connection from connection pool: ' +
|
||||
`maxPoolSize: ${maxPoolSize}, ` +
|
||||
`connections in use by cursors: ${this.cursorConnections}, ` +
|
||||
`connections in use by transactions: ${this.txnConnections}, ` +
|
||||
`connections in use by other operations: ${this.otherConnections}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the metrics to the initial values.
|
||||
*/
|
||||
reset(): void {
|
||||
this.txnConnections = 0;
|
||||
this.cursorConnections = 0;
|
||||
this.otherConnections = 0;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type { Document } from '../bson';
|
||||
import { ServerType } from '../sdam/common';
|
||||
import { parseServerType } from '../sdam/server_description';
|
||||
import type { CompressorName } from './wire_protocol/compression';
|
||||
|
||||
const RESPONSE_FIELDS = [
|
||||
'minWireVersion',
|
||||
'maxWireVersion',
|
||||
'maxBsonObjectSize',
|
||||
'maxMessageSizeBytes',
|
||||
'maxWriteBatchSize',
|
||||
'logicalSessionTimeoutMinutes'
|
||||
] as const;
|
||||
|
||||
/** @public */
|
||||
export interface StreamDescriptionOptions {
|
||||
compressors?: CompressorName[];
|
||||
logicalSessionTimeoutMinutes?: number;
|
||||
loadBalanced: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class StreamDescription {
|
||||
address: string;
|
||||
type: string;
|
||||
minWireVersion?: number;
|
||||
maxWireVersion?: number;
|
||||
maxBsonObjectSize: number;
|
||||
maxMessageSizeBytes: number;
|
||||
maxWriteBatchSize: number;
|
||||
compressors: CompressorName[];
|
||||
compressor?: CompressorName;
|
||||
logicalSessionTimeoutMinutes?: number;
|
||||
loadBalanced: boolean;
|
||||
|
||||
__nodejs_mock_server__?: boolean;
|
||||
|
||||
zlibCompressionLevel?: number;
|
||||
|
||||
constructor(address: string, options?: StreamDescriptionOptions) {
|
||||
this.address = address;
|
||||
this.type = ServerType.Unknown;
|
||||
this.minWireVersion = undefined;
|
||||
this.maxWireVersion = undefined;
|
||||
this.maxBsonObjectSize = 16777216;
|
||||
this.maxMessageSizeBytes = 48000000;
|
||||
this.maxWriteBatchSize = 100000;
|
||||
this.logicalSessionTimeoutMinutes = options?.logicalSessionTimeoutMinutes;
|
||||
this.loadBalanced = !!options?.loadBalanced;
|
||||
this.compressors =
|
||||
options && options.compressors && Array.isArray(options.compressors)
|
||||
? options.compressors
|
||||
: [];
|
||||
}
|
||||
|
||||
receiveResponse(response: Document | null): void {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
this.type = parseServerType(response);
|
||||
for (const field of RESPONSE_FIELDS) {
|
||||
if (response[field] != null) {
|
||||
this[field] = response[field];
|
||||
}
|
||||
|
||||
// testing case
|
||||
if ('__nodejs_mock_server__' in response) {
|
||||
this.__nodejs_mock_server__ = response['__nodejs_mock_server__'];
|
||||
}
|
||||
}
|
||||
|
||||
if (response.compression) {
|
||||
this.compressor = this.compressors.filter(c => response.compression?.includes(c))[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
import { LEGACY_HELLO_COMMAND } from '../../constants';
|
||||
import { PKG_VERSION, Snappy, ZStandard } from '../../deps';
|
||||
import { MongoDecompressionError, MongoInvalidArgumentError } from '../../error';
|
||||
import type { Callback } from '../../utils';
|
||||
import type { OperationDescription } from '../message_stream';
|
||||
|
||||
/** @public */
|
||||
export const Compressor = Object.freeze({
|
||||
none: 0,
|
||||
snappy: 1,
|
||||
zlib: 2,
|
||||
zstd: 3
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type Compressor = typeof Compressor[CompressorName];
|
||||
|
||||
/** @public */
|
||||
export type CompressorName = keyof typeof Compressor;
|
||||
|
||||
export const uncompressibleCommands = new Set([
|
||||
LEGACY_HELLO_COMMAND,
|
||||
'saslStart',
|
||||
'saslContinue',
|
||||
'getnonce',
|
||||
'authenticate',
|
||||
'createUser',
|
||||
'updateUser',
|
||||
'copydbSaslStart',
|
||||
'copydbgetnonce',
|
||||
'copydb'
|
||||
]);
|
||||
|
||||
const MAX_COMPRESSOR_ID = 3;
|
||||
const ZSTD_COMPRESSION_LEVEL = 3;
|
||||
|
||||
// Facilitate compressing a message using an agreed compressor
|
||||
export function compress(
|
||||
self: { options: OperationDescription & zlib.ZlibOptions },
|
||||
dataToBeCompressed: Buffer,
|
||||
callback: Callback<Buffer>
|
||||
): void {
|
||||
const zlibOptions = {} as zlib.ZlibOptions;
|
||||
switch (self.options.agreedCompressor) {
|
||||
case 'snappy': {
|
||||
if ('kModuleError' in Snappy) {
|
||||
return callback(Snappy['kModuleError']);
|
||||
}
|
||||
|
||||
if (Snappy[PKG_VERSION].major <= 6) {
|
||||
Snappy.compress(dataToBeCompressed, callback);
|
||||
} else {
|
||||
Snappy.compress(dataToBeCompressed).then(
|
||||
buffer => callback(undefined, buffer),
|
||||
error => callback(error)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'zlib':
|
||||
// Determine zlibCompressionLevel
|
||||
if (self.options.zlibCompressionLevel) {
|
||||
zlibOptions.level = self.options.zlibCompressionLevel;
|
||||
}
|
||||
zlib.deflate(dataToBeCompressed, zlibOptions, callback as zlib.CompressCallback);
|
||||
break;
|
||||
case 'zstd':
|
||||
if ('kModuleError' in ZStandard) {
|
||||
return callback(ZStandard['kModuleError']);
|
||||
}
|
||||
ZStandard.compress(dataToBeCompressed, ZSTD_COMPRESSION_LEVEL).then(
|
||||
buffer => callback(undefined, buffer),
|
||||
error => callback(error)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Unknown compressor ${self.options.agreedCompressor} failed to compress`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Decompress a message using the given compressor
|
||||
export function decompress(
|
||||
compressorID: Compressor,
|
||||
compressedData: Buffer,
|
||||
callback: Callback<Buffer>
|
||||
): void {
|
||||
if (compressorID < 0 || compressorID > MAX_COMPRESSOR_ID) {
|
||||
throw new MongoDecompressionError(
|
||||
`Server sent message compressed using an unsupported compressor. (Received compressor ID ${compressorID})`
|
||||
);
|
||||
}
|
||||
|
||||
switch (compressorID) {
|
||||
case Compressor.snappy: {
|
||||
if ('kModuleError' in Snappy) {
|
||||
return callback(Snappy['kModuleError']);
|
||||
}
|
||||
|
||||
if (Snappy[PKG_VERSION].major <= 6) {
|
||||
Snappy.uncompress(compressedData, { asBuffer: true }, callback);
|
||||
} else {
|
||||
Snappy.uncompress(compressedData, { asBuffer: true }).then(
|
||||
buffer => callback(undefined, buffer),
|
||||
error => callback(error)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Compressor.zstd: {
|
||||
if ('kModuleError' in ZStandard) {
|
||||
return callback(ZStandard['kModuleError']);
|
||||
}
|
||||
|
||||
ZStandard.decompress(compressedData).then(
|
||||
buffer => callback(undefined, buffer),
|
||||
error => callback(error)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Compressor.zlib:
|
||||
zlib.inflate(compressedData, callback as zlib.CompressCallback);
|
||||
break;
|
||||
default:
|
||||
callback(undefined, compressedData);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export const MIN_SUPPORTED_SERVER_VERSION = '3.6';
|
||||
export const MAX_SUPPORTED_SERVER_VERSION = '6.0';
|
||||
export const MIN_SUPPORTED_WIRE_VERSION = 6;
|
||||
export const MAX_SUPPORTED_WIRE_VERSION = 17;
|
||||
export const OP_REPLY = 1;
|
||||
export const OP_UPDATE = 2001;
|
||||
export const OP_INSERT = 2002;
|
||||
export const OP_QUERY = 2004;
|
||||
export const OP_DELETE = 2006;
|
||||
export const OP_COMPRESSED = 2012;
|
||||
export const OP_MSG = 2013;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type { Document } from '../../bson';
|
||||
import { MongoInvalidArgumentError } from '../../error';
|
||||
import type { ReadPreferenceLike } from '../../read_preference';
|
||||
import { ReadPreference } from '../../read_preference';
|
||||
import { ServerType } from '../../sdam/common';
|
||||
import type { Server } from '../../sdam/server';
|
||||
import type { ServerDescription } from '../../sdam/server_description';
|
||||
import type { Topology } from '../../sdam/topology';
|
||||
import { TopologyDescription } from '../../sdam/topology_description';
|
||||
import type { OpQueryOptions } from '../commands';
|
||||
import type { CommandOptions, Connection } from '../connection';
|
||||
|
||||
export interface ReadPreferenceOption {
|
||||
readPreference?: ReadPreferenceLike;
|
||||
}
|
||||
|
||||
export function getReadPreference(cmd: Document, options?: ReadPreferenceOption): ReadPreference {
|
||||
// Default to command version of the readPreference
|
||||
let readPreference = cmd.readPreference || ReadPreference.primary;
|
||||
// If we have an option readPreference override the command one
|
||||
if (options?.readPreference) {
|
||||
readPreference = options.readPreference;
|
||||
}
|
||||
|
||||
if (typeof readPreference === 'string') {
|
||||
readPreference = ReadPreference.fromString(readPreference);
|
||||
}
|
||||
|
||||
if (!(readPreference instanceof ReadPreference)) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
'Option "readPreference" must be a ReadPreference instance'
|
||||
);
|
||||
}
|
||||
|
||||
return readPreference;
|
||||
}
|
||||
|
||||
export function applyCommonQueryOptions(
|
||||
queryOptions: OpQueryOptions,
|
||||
options: CommandOptions
|
||||
): CommandOptions {
|
||||
Object.assign(queryOptions, {
|
||||
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
||||
bsonRegExp: typeof options.bsonRegExp === 'boolean' ? options.bsonRegExp : false,
|
||||
enableUtf8Validation:
|
||||
typeof options.enableUtf8Validation === 'boolean' ? options.enableUtf8Validation : true
|
||||
});
|
||||
|
||||
if (options.session) {
|
||||
queryOptions.session = options.session;
|
||||
}
|
||||
|
||||
return queryOptions;
|
||||
}
|
||||
|
||||
export function isSharded(topologyOrServer?: Topology | Server | Connection): boolean {
|
||||
if (topologyOrServer == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (topologyOrServer.description && topologyOrServer.description.type === ServerType.Mongos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// NOTE: This is incredibly inefficient, and should be removed once command construction
|
||||
// happens based on `Server` not `Topology`.
|
||||
if (topologyOrServer.description && topologyOrServer.description instanceof TopologyDescription) {
|
||||
const servers: ServerDescription[] = Array.from(topologyOrServer.description.servers.values());
|
||||
return servers.some((server: ServerDescription) => server.type === ServerType.Mongos);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+1700
File diff suppressed because it is too large
Load Diff
+1267
File diff suppressed because it is too large
Load Diff
+134
@@ -0,0 +1,134 @@
|
||||
export const SYSTEM_NAMESPACE_COLLECTION = 'system.namespaces';
|
||||
export const SYSTEM_INDEX_COLLECTION = 'system.indexes';
|
||||
export const SYSTEM_PROFILE_COLLECTION = 'system.profile';
|
||||
export const SYSTEM_USER_COLLECTION = 'system.users';
|
||||
export const SYSTEM_COMMAND_COLLECTION = '$cmd';
|
||||
export const SYSTEM_JS_COLLECTION = 'system.js';
|
||||
|
||||
// events
|
||||
export const ERROR = 'error' as const;
|
||||
export const TIMEOUT = 'timeout' as const;
|
||||
export const CLOSE = 'close' as const;
|
||||
export const OPEN = 'open' as const;
|
||||
export const CONNECT = 'connect' as const;
|
||||
export const CLOSED = 'closed' as const;
|
||||
export const ENDED = 'ended' as const;
|
||||
export const MESSAGE = 'message' as const;
|
||||
export const PINNED = 'pinned' as const;
|
||||
export const UNPINNED = 'unpinned' as const;
|
||||
export const DESCRIPTION_RECEIVED = 'descriptionReceived';
|
||||
export const SERVER_OPENING = 'serverOpening' as const;
|
||||
export const SERVER_CLOSED = 'serverClosed' as const;
|
||||
export const SERVER_DESCRIPTION_CHANGED = 'serverDescriptionChanged' as const;
|
||||
export const TOPOLOGY_OPENING = 'topologyOpening' as const;
|
||||
export const TOPOLOGY_CLOSED = 'topologyClosed' as const;
|
||||
export const TOPOLOGY_DESCRIPTION_CHANGED = 'topologyDescriptionChanged' as const;
|
||||
export const CONNECTION_POOL_CREATED = 'connectionPoolCreated' as const;
|
||||
export const CONNECTION_POOL_CLOSED = 'connectionPoolClosed' as const;
|
||||
export const CONNECTION_POOL_CLEARED = 'connectionPoolCleared' as const;
|
||||
export const CONNECTION_CREATED = 'connectionCreated' as const;
|
||||
export const CONNECTION_READY = 'connectionReady' as const;
|
||||
export const CONNECTION_CLOSED = 'connectionClosed' as const;
|
||||
export const CONNECTION_CHECK_OUT_STARTED = 'connectionCheckOutStarted' as const;
|
||||
export const CONNECTION_CHECK_OUT_FAILED = 'connectionCheckOutFailed' as const;
|
||||
export const CONNECTION_CHECKED_OUT = 'connectionCheckedOut' as const;
|
||||
export const CONNECTION_CHECKED_IN = 'connectionCheckedIn' as const;
|
||||
export const CLUSTER_TIME_RECEIVED = 'clusterTimeReceived' as const;
|
||||
export const COMMAND_STARTED = 'commandStarted' as const;
|
||||
export const COMMAND_SUCCEEDED = 'commandSucceeded' as const;
|
||||
export const COMMAND_FAILED = 'commandFailed' as const;
|
||||
export const SERVER_HEARTBEAT_STARTED = 'serverHeartbeatStarted' as const;
|
||||
export const SERVER_HEARTBEAT_SUCCEEDED = 'serverHeartbeatSucceeded' as const;
|
||||
export const SERVER_HEARTBEAT_FAILED = 'serverHeartbeatFailed' as const;
|
||||
export const RESPONSE = 'response' as const;
|
||||
export const MORE = 'more' as const;
|
||||
export const INIT = 'init' as const;
|
||||
export const CHANGE = 'change' as const;
|
||||
export const END = 'end' as const;
|
||||
export const RESUME_TOKEN_CHANGED = 'resumeTokenChanged' as const;
|
||||
|
||||
/** @public */
|
||||
export const HEARTBEAT_EVENTS = Object.freeze([
|
||||
SERVER_HEARTBEAT_STARTED,
|
||||
SERVER_HEARTBEAT_SUCCEEDED,
|
||||
SERVER_HEARTBEAT_FAILED
|
||||
] as const);
|
||||
|
||||
/** @public */
|
||||
export const CMAP_EVENTS = Object.freeze([
|
||||
CONNECTION_POOL_CREATED,
|
||||
CONNECTION_POOL_CLOSED,
|
||||
CONNECTION_CREATED,
|
||||
CONNECTION_READY,
|
||||
CONNECTION_CLOSED,
|
||||
CONNECTION_CHECK_OUT_STARTED,
|
||||
CONNECTION_CHECK_OUT_FAILED,
|
||||
CONNECTION_CHECKED_OUT,
|
||||
CONNECTION_CHECKED_IN,
|
||||
CONNECTION_POOL_CLEARED
|
||||
] as const);
|
||||
|
||||
/** @public */
|
||||
export const TOPOLOGY_EVENTS = Object.freeze([
|
||||
SERVER_OPENING,
|
||||
SERVER_CLOSED,
|
||||
SERVER_DESCRIPTION_CHANGED,
|
||||
TOPOLOGY_OPENING,
|
||||
TOPOLOGY_CLOSED,
|
||||
TOPOLOGY_DESCRIPTION_CHANGED,
|
||||
ERROR,
|
||||
TIMEOUT,
|
||||
CLOSE
|
||||
] as const);
|
||||
|
||||
/** @public */
|
||||
export const APM_EVENTS = Object.freeze([
|
||||
COMMAND_STARTED,
|
||||
COMMAND_SUCCEEDED,
|
||||
COMMAND_FAILED
|
||||
] as const);
|
||||
|
||||
/**
|
||||
* All events that we relay to the `Topology`
|
||||
* @internal
|
||||
*/
|
||||
export const SERVER_RELAY_EVENTS = Object.freeze([
|
||||
SERVER_HEARTBEAT_STARTED,
|
||||
SERVER_HEARTBEAT_SUCCEEDED,
|
||||
SERVER_HEARTBEAT_FAILED,
|
||||
COMMAND_STARTED,
|
||||
COMMAND_SUCCEEDED,
|
||||
COMMAND_FAILED,
|
||||
...CMAP_EVENTS
|
||||
] as const);
|
||||
|
||||
/**
|
||||
* All events we listen to from `Server` instances, but do not forward to the client
|
||||
* @internal
|
||||
*/
|
||||
export const LOCAL_SERVER_EVENTS = Object.freeze([
|
||||
CONNECT,
|
||||
DESCRIPTION_RECEIVED,
|
||||
CLOSED,
|
||||
ENDED
|
||||
] as const);
|
||||
|
||||
/** @public */
|
||||
export const MONGO_CLIENT_EVENTS = Object.freeze([
|
||||
...CMAP_EVENTS,
|
||||
...APM_EVENTS,
|
||||
...TOPOLOGY_EVENTS,
|
||||
...HEARTBEAT_EVENTS
|
||||
] as const);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* The legacy hello command that was deprecated in MongoDB 5.0.
|
||||
*/
|
||||
export const LEGACY_HELLO_COMMAND = 'ismaster';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* The legacy hello command that was deprecated in MongoDB 5.0.
|
||||
*/
|
||||
export const LEGACY_HELLO_COMMAND_CAMEL_CASE = 'isMaster';
|
||||
+931
@@ -0,0 +1,931 @@
|
||||
import { Readable, Transform } from 'stream';
|
||||
|
||||
import { BSONSerializeOptions, Document, Long, pluckBSONSerializeOptions } from '../bson';
|
||||
import {
|
||||
AnyError,
|
||||
MongoCursorExhaustedError,
|
||||
MongoCursorInUseError,
|
||||
MongoInvalidArgumentError,
|
||||
MongoNetworkError,
|
||||
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 { KillCursorsOperation } from '../operations/kill_cursors';
|
||||
import { ReadConcern, ReadConcernLike } from '../read_concern';
|
||||
import { ReadPreference, ReadPreferenceLike } from '../read_preference';
|
||||
import type { Server } from '../sdam/server';
|
||||
import { ClientSession, maybeClearPinnedConnection } from '../sessions';
|
||||
import { Callback, maybePromise, MongoDBNamespace, ns } from '../utils';
|
||||
|
||||
/** @internal */
|
||||
const kId = Symbol('id');
|
||||
/** @internal */
|
||||
const kDocuments = Symbol('documents');
|
||||
/** @internal */
|
||||
const kServer = Symbol('server');
|
||||
/** @internal */
|
||||
const kNamespace = Symbol('namespace');
|
||||
/** @internal */
|
||||
const kClient = Symbol('client');
|
||||
/** @internal */
|
||||
const kSession = Symbol('session');
|
||||
/** @internal */
|
||||
const kOptions = Symbol('options');
|
||||
/** @internal */
|
||||
const kTransform = Symbol('transform');
|
||||
/** @internal */
|
||||
const kInitialized = Symbol('initialized');
|
||||
/** @internal */
|
||||
const kClosed = Symbol('closed');
|
||||
/** @internal */
|
||||
const kKilled = Symbol('killed');
|
||||
/** @internal */
|
||||
const kInit = Symbol('kInit');
|
||||
|
||||
/** @public */
|
||||
export const CURSOR_FLAGS = [
|
||||
'tailable',
|
||||
'oplogReplay',
|
||||
'noCursorTimeout',
|
||||
'awaitData',
|
||||
'exhaust',
|
||||
'partial'
|
||||
] as const;
|
||||
|
||||
/** @public
|
||||
* @deprecated This interface is deprecated */
|
||||
export interface CursorCloseOptions {
|
||||
/** Bypass calling killCursors when closing the cursor. */
|
||||
/** @deprecated the skipKillCursors option is deprecated */
|
||||
skipKillCursors?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CursorStreamOptions {
|
||||
/** A transformation method applied to each document emitted by the stream */
|
||||
transform?(this: void, doc: Document): Document;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type CursorFlag = typeof CURSOR_FLAGS[number];
|
||||
|
||||
/** @public */
|
||||
export interface AbstractCursorOptions extends BSONSerializeOptions {
|
||||
session?: ClientSession;
|
||||
readPreference?: ReadPreferenceLike;
|
||||
readConcern?: ReadConcernLike;
|
||||
/**
|
||||
* Specifies the number of documents to return in each response from MongoDB
|
||||
*/
|
||||
batchSize?: number;
|
||||
/**
|
||||
* When applicable `maxTimeMS` controls the amount of time the initial command
|
||||
* that constructs a cursor should take. (ex. find, aggregate, listCollections)
|
||||
*/
|
||||
maxTimeMS?: number;
|
||||
/**
|
||||
* When applicable `maxAwaitTimeMS` controls the amount of time subsequent getMores
|
||||
* that a cursor uses to fetch more data should take. (ex. cursor.next())
|
||||
*/
|
||||
maxAwaitTimeMS?: number;
|
||||
/**
|
||||
* Comment to apply to the operation.
|
||||
*
|
||||
* In server versions pre-4.4, 'comment' must be string. A server
|
||||
* error will be thrown if any other type is provided.
|
||||
*
|
||||
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
|
||||
*/
|
||||
comment?: unknown;
|
||||
/**
|
||||
* By default, MongoDB will automatically close a cursor when the
|
||||
* client has exhausted all results in the cursor. However, for [capped collections](https://www.mongodb.com/docs/manual/core/capped-collections)
|
||||
* you may use a Tailable Cursor that remains open after the client exhausts
|
||||
* the results in the initial cursor.
|
||||
*/
|
||||
tailable?: boolean;
|
||||
/**
|
||||
* If awaitData is set to true, when the cursor reaches the end of the capped collection,
|
||||
* MongoDB blocks the query thread for a period of time waiting for new data to arrive.
|
||||
* When new data is inserted into the capped collection, the blocked thread is signaled
|
||||
* to wake up and return the next batch to the client.
|
||||
*/
|
||||
awaitData?: boolean;
|
||||
noCursorTimeout?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type InternalAbstractCursorOptions = Omit<AbstractCursorOptions, 'readPreference'> & {
|
||||
// resolved
|
||||
readPreference: ReadPreference;
|
||||
readConcern?: ReadConcern;
|
||||
|
||||
// cursor flags, some are deprecated
|
||||
oplogReplay?: boolean;
|
||||
exhaust?: boolean;
|
||||
partial?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type AbstractCursorEvents = {
|
||||
[AbstractCursor.CLOSE](): void;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export abstract class AbstractCursor<
|
||||
TSchema = any,
|
||||
CursorEvents extends AbstractCursorEvents = AbstractCursorEvents
|
||||
> extends TypedEventEmitter<CursorEvents> {
|
||||
/** @internal */
|
||||
[kId]?: Long;
|
||||
/** @internal */
|
||||
[kSession]: ClientSession;
|
||||
/** @internal */
|
||||
[kServer]?: Server;
|
||||
/** @internal */
|
||||
[kNamespace]: MongoDBNamespace;
|
||||
/** @internal */
|
||||
[kDocuments]: TSchema[];
|
||||
/** @internal */
|
||||
[kClient]: MongoClient;
|
||||
/** @internal */
|
||||
[kTransform]?: (doc: TSchema) => any;
|
||||
/** @internal */
|
||||
[kInitialized]: boolean;
|
||||
/** @internal */
|
||||
[kClosed]: boolean;
|
||||
/** @internal */
|
||||
[kKilled]: boolean;
|
||||
/** @internal */
|
||||
[kOptions]: InternalAbstractCursorOptions;
|
||||
|
||||
/** @event */
|
||||
static readonly CLOSE = 'close' as const;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
client: MongoClient,
|
||||
namespace: MongoDBNamespace,
|
||||
options: AbstractCursorOptions = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
if (!client.s.isMongoClient) {
|
||||
throw new MongoRuntimeError('Cursor must be constructed with MongoClient');
|
||||
}
|
||||
this[kClient] = client;
|
||||
this[kNamespace] = namespace;
|
||||
this[kDocuments] = [];
|
||||
this[kInitialized] = false;
|
||||
this[kClosed] = false;
|
||||
this[kKilled] = false;
|
||||
this[kOptions] = {
|
||||
readPreference:
|
||||
options.readPreference && options.readPreference instanceof ReadPreference
|
||||
? options.readPreference
|
||||
: ReadPreference.primary,
|
||||
...pluckBSONSerializeOptions(options)
|
||||
};
|
||||
|
||||
const readConcern = ReadConcern.fromOptions(options);
|
||||
if (readConcern) {
|
||||
this[kOptions].readConcern = readConcern;
|
||||
}
|
||||
|
||||
if (typeof options.batchSize === 'number') {
|
||||
this[kOptions].batchSize = options.batchSize;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (options.comment !== undefined) {
|
||||
this[kOptions].comment = options.comment;
|
||||
}
|
||||
|
||||
if (typeof options.maxTimeMS === 'number') {
|
||||
this[kOptions].maxTimeMS = options.maxTimeMS;
|
||||
}
|
||||
|
||||
if (typeof options.maxAwaitTimeMS === 'number') {
|
||||
this[kOptions].maxAwaitTimeMS = options.maxAwaitTimeMS;
|
||||
}
|
||||
|
||||
if (options.session instanceof ClientSession) {
|
||||
this[kSession] = options.session;
|
||||
} else {
|
||||
this[kSession] = this[kClient].startSession({ owner: this, explicit: false });
|
||||
}
|
||||
}
|
||||
|
||||
get id(): Long | undefined {
|
||||
return this[kId];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
get client(): MongoClient {
|
||||
return this[kClient];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
get server(): Server | undefined {
|
||||
return this[kServer];
|
||||
}
|
||||
|
||||
get namespace(): MongoDBNamespace {
|
||||
return this[kNamespace];
|
||||
}
|
||||
|
||||
get readPreference(): ReadPreference {
|
||||
return this[kOptions].readPreference;
|
||||
}
|
||||
|
||||
get readConcern(): ReadConcern | undefined {
|
||||
return this[kOptions].readConcern;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
get session(): ClientSession {
|
||||
return this[kSession];
|
||||
}
|
||||
|
||||
set session(clientSession: ClientSession) {
|
||||
this[kSession] = clientSession;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
get cursorOptions(): InternalAbstractCursorOptions {
|
||||
return this[kOptions];
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
return this[kClosed];
|
||||
}
|
||||
|
||||
get killed(): boolean {
|
||||
return this[kKilled];
|
||||
}
|
||||
|
||||
get loadBalanced(): boolean {
|
||||
return !!this[kClient].topology?.loadBalanced;
|
||||
}
|
||||
|
||||
/** Returns current buffered documents length */
|
||||
bufferedCount(): number {
|
||||
return this[kDocuments].length;
|
||||
}
|
||||
|
||||
/** Returns current buffered documents */
|
||||
readBufferedDocuments(number?: number): TSchema[] {
|
||||
return this[kDocuments].splice(0, number ?? this[kDocuments].length);
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<TSchema, void> {
|
||||
return {
|
||||
next: () =>
|
||||
this.next().then(value =>
|
||||
value != null ? { value, done: false } : { value: undefined, done: true }
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
stream(options?: CursorStreamOptions): Readable & AsyncIterable<TSchema> {
|
||||
if (options?.transform) {
|
||||
const transform = options.transform;
|
||||
const readable = new ReadableCursorStream(this);
|
||||
|
||||
return readable.pipe(
|
||||
new Transform({
|
||||
objectMode: true,
|
||||
highWaterMark: 1,
|
||||
transform(chunk, _, callback) {
|
||||
try {
|
||||
const transformed = transform(chunk);
|
||||
callback(undefined, transformed);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return new ReadableCursorStream(this);
|
||||
}
|
||||
|
||||
hasNext(): Promise<boolean>;
|
||||
hasNext(callback: Callback<boolean>): void;
|
||||
hasNext(callback?: Callback<boolean>): Promise<boolean> | void {
|
||||
return maybePromise(callback, done => {
|
||||
if (this[kId] === Long.ZERO) {
|
||||
return done(undefined, false);
|
||||
}
|
||||
|
||||
if (this[kDocuments].length) {
|
||||
return done(undefined, true);
|
||||
}
|
||||
|
||||
next<TSchema>(this, true, (err, doc) => {
|
||||
if (err) return done(err);
|
||||
|
||||
if (doc) {
|
||||
this[kDocuments].unshift(doc);
|
||||
done(undefined, true);
|
||||
return;
|
||||
}
|
||||
|
||||
done(undefined, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Get the next available document from the cursor, returns null if no more documents are available. */
|
||||
next(): Promise<TSchema | null>;
|
||||
next(callback: Callback<TSchema | null>): void;
|
||||
next(callback?: Callback<TSchema | null>): Promise<TSchema | null> | void;
|
||||
next(callback?: Callback<TSchema | null>): Promise<TSchema | null> | void {
|
||||
return maybePromise(callback, done => {
|
||||
if (this[kId] === Long.ZERO) {
|
||||
return done(new MongoCursorExhaustedError());
|
||||
}
|
||||
|
||||
next(this, true, done);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get the next available document from the cursor or `null` if an empty batch is returned
|
||||
*/
|
||||
tryNext(): Promise<TSchema | null>;
|
||||
tryNext(callback: Callback<TSchema | null>): void;
|
||||
tryNext(callback?: Callback<TSchema | null>): Promise<TSchema | null> | void {
|
||||
return maybePromise(callback, done => {
|
||||
if (this[kId] === Long.ZERO) {
|
||||
return done(new MongoCursorExhaustedError());
|
||||
}
|
||||
|
||||
next(this, false, done);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all the documents for this cursor using the iterator, callback pattern.
|
||||
*
|
||||
* @param iterator - The iteration callback.
|
||||
* @param callback - The end callback.
|
||||
*/
|
||||
forEach(iterator: (doc: TSchema) => boolean | void): Promise<void>;
|
||||
forEach(iterator: (doc: TSchema) => boolean | void, callback: Callback<void>): void;
|
||||
forEach(
|
||||
iterator: (doc: TSchema) => boolean | void,
|
||||
callback?: Callback<void>
|
||||
): Promise<void> | void {
|
||||
if (typeof iterator !== 'function') {
|
||||
throw new MongoInvalidArgumentError('Argument "iterator" must be a function');
|
||||
}
|
||||
return maybePromise(callback, done => {
|
||||
const transform = this[kTransform];
|
||||
const fetchDocs = () => {
|
||||
next<TSchema>(this, true, (err, doc) => {
|
||||
if (err || doc == null) return done(err);
|
||||
let result;
|
||||
// NOTE: no need to transform because `next` will do this automatically
|
||||
try {
|
||||
result = iterator(doc); // TODO(NODE-3283): Improve transform typing
|
||||
} catch (error) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
if (result === false) return done();
|
||||
|
||||
// these do need to be transformed since they are copying the rest of the batch
|
||||
const internalDocs = this[kDocuments].splice(0, this[kDocuments].length);
|
||||
for (let i = 0; i < internalDocs.length; ++i) {
|
||||
try {
|
||||
result = iterator(
|
||||
(transform ? transform(internalDocs[i]) : internalDocs[i]) as TSchema // TODO(NODE-3283): Improve transform typing
|
||||
);
|
||||
} catch (error) {
|
||||
return done(error);
|
||||
}
|
||||
if (result === false) return done();
|
||||
}
|
||||
|
||||
fetchDocs();
|
||||
});
|
||||
};
|
||||
|
||||
fetchDocs();
|
||||
});
|
||||
}
|
||||
|
||||
close(): Promise<void>;
|
||||
close(callback: Callback): void;
|
||||
/**
|
||||
* @deprecated options argument is deprecated
|
||||
*/
|
||||
close(options: CursorCloseOptions): Promise<void>;
|
||||
/**
|
||||
* @deprecated options argument is deprecated
|
||||
*/
|
||||
close(options: CursorCloseOptions, callback: Callback): void;
|
||||
close(options?: CursorCloseOptions | Callback, callback?: Callback): Promise<void> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
|
||||
const needsToEmitClosed = !this[kClosed];
|
||||
this[kClosed] = true;
|
||||
|
||||
return maybePromise(callback, done => cleanupCursor(this, { needsToEmitClosed }, done));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of documents. The caller is responsible for making sure that there
|
||||
* is enough memory to store the results. Note that the array only contains partial
|
||||
* results when this cursor had been previously accessed. In that case,
|
||||
* cursor.rewind() can be used to reset the cursor.
|
||||
*
|
||||
* @param callback - The result callback.
|
||||
*/
|
||||
toArray(): Promise<TSchema[]>;
|
||||
toArray(callback: Callback<TSchema[]>): void;
|
||||
toArray(callback?: Callback<TSchema[]>): Promise<TSchema[]> | void {
|
||||
return maybePromise(callback, done => {
|
||||
const docs: TSchema[] = [];
|
||||
const transform = this[kTransform];
|
||||
const fetchDocs = () => {
|
||||
// NOTE: if we add a `nextBatch` then we should use it here
|
||||
next<TSchema>(this, true, (err, doc) => {
|
||||
if (err) return done(err);
|
||||
if (doc == null) return done(undefined, docs);
|
||||
|
||||
// NOTE: no need to transform because `next` will do this automatically
|
||||
docs.push(doc);
|
||||
|
||||
// these do need to be transformed since they are copying the rest of the batch
|
||||
const internalDocs = (
|
||||
transform
|
||||
? this[kDocuments].splice(0, this[kDocuments].length).map(transform)
|
||||
: this[kDocuments].splice(0, this[kDocuments].length)
|
||||
) as TSchema[]; // TODO(NODE-3283): Improve transform typing
|
||||
|
||||
if (internalDocs) {
|
||||
docs.push(...internalDocs);
|
||||
}
|
||||
|
||||
fetchDocs();
|
||||
});
|
||||
};
|
||||
|
||||
fetchDocs();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a cursor flag to the cursor
|
||||
*
|
||||
* @param flag - The flag to set, must be one of following ['tailable', 'oplogReplay', 'noCursorTimeout', 'awaitData', 'partial' -.
|
||||
* @param value - The flag boolean value.
|
||||
*/
|
||||
addCursorFlag(flag: CursorFlag, value: boolean): this {
|
||||
assertUninitialized(this);
|
||||
if (!CURSOR_FLAGS.includes(flag)) {
|
||||
throw new MongoInvalidArgumentError(`Flag ${flag} is not one of ${CURSOR_FLAGS}`);
|
||||
}
|
||||
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new MongoInvalidArgumentError(`Flag ${flag} must be a boolean value`);
|
||||
}
|
||||
|
||||
this[kOptions][flag] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map all documents using the provided function
|
||||
* If there is a transform set on the cursor, that will be called first and the result passed to
|
||||
* this function's transform.
|
||||
*
|
||||
* @remarks
|
||||
* **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
|
||||
* it **does not** return a new instance of a cursor. This means when calling map,
|
||||
* you should always assign the result to a new variable in order to get a correctly typed cursor variable.
|
||||
* Take note of the following example:
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cursor: FindCursor<Document> = coll.find();
|
||||
* const mappedCursor: FindCursor<number> = cursor.map(doc => Object.keys(doc).length);
|
||||
* const keyCounts: number[] = await mappedCursor.toArray(); // cursor.toArray() still returns Document[]
|
||||
* ```
|
||||
* @param transform - The mapping transformation method.
|
||||
*/
|
||||
map<T = any>(transform: (doc: TSchema) => T): AbstractCursor<T> {
|
||||
assertUninitialized(this);
|
||||
const oldTransform = this[kTransform] as (doc: TSchema) => TSchema; // TODO(NODE-3283): Improve transform typing
|
||||
if (oldTransform) {
|
||||
this[kTransform] = doc => {
|
||||
return transform(oldTransform(doc));
|
||||
};
|
||||
} else {
|
||||
this[kTransform] = transform;
|
||||
}
|
||||
|
||||
return this as unknown as AbstractCursor<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ReadPreference for the cursor.
|
||||
*
|
||||
* @param readPreference - The new read preference for the cursor.
|
||||
*/
|
||||
withReadPreference(readPreference: ReadPreferenceLike): this {
|
||||
assertUninitialized(this);
|
||||
if (readPreference instanceof ReadPreference) {
|
||||
this[kOptions].readPreference = readPreference;
|
||||
} else if (typeof readPreference === 'string') {
|
||||
this[kOptions].readPreference = ReadPreference.fromString(readPreference);
|
||||
} else {
|
||||
throw new MongoInvalidArgumentError(`Invalid read preference: ${readPreference}`);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ReadPreference for the cursor.
|
||||
*
|
||||
* @param readPreference - The new read preference for the cursor.
|
||||
*/
|
||||
withReadConcern(readConcern: ReadConcernLike): this {
|
||||
assertUninitialized(this);
|
||||
const resolvedReadConcern = ReadConcern.fromOptions({ readConcern });
|
||||
if (resolvedReadConcern) {
|
||||
this[kOptions].readConcern = resolvedReadConcern;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a maxTimeMS on the cursor query, allowing for hard timeout limits on queries (Only supported on MongoDB 2.6 or higher)
|
||||
*
|
||||
* @param value - Number of milliseconds to wait before aborting the query.
|
||||
*/
|
||||
maxTimeMS(value: number): this {
|
||||
assertUninitialized(this);
|
||||
if (typeof value !== 'number') {
|
||||
throw new MongoInvalidArgumentError('Argument for maxTimeMS must be a number');
|
||||
}
|
||||
|
||||
this[kOptions].maxTimeMS = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the batch size for the cursor.
|
||||
*
|
||||
* @param value - The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/find/|find command documentation}.
|
||||
*/
|
||||
batchSize(value: number): this {
|
||||
assertUninitialized(this);
|
||||
if (this[kOptions].tailable) {
|
||||
throw new MongoTailableCursorError('Tailable cursor does not support batchSize');
|
||||
}
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
throw new MongoInvalidArgumentError('Operation "batchSize" requires an integer');
|
||||
}
|
||||
|
||||
this[kOptions].batchSize = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind this cursor to its uninitialized state. Any options that are present on the cursor will
|
||||
* remain in effect. Iterating this cursor will cause new queries to be sent to the server, even
|
||||
* if the resultant data has already been retrieved by this cursor.
|
||||
*/
|
||||
rewind(): void {
|
||||
if (!this[kInitialized]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[kId] = undefined;
|
||||
this[kDocuments] = [];
|
||||
this[kClosed] = false;
|
||||
this[kKilled] = false;
|
||||
this[kInitialized] = false;
|
||||
|
||||
const session = this[kSession];
|
||||
if (session) {
|
||||
// We only want to end this session if we created it, and it hasn't ended yet
|
||||
if (session.explicit === false) {
|
||||
if (!session.hasEnded) {
|
||||
session.endSession().catch(() => null);
|
||||
}
|
||||
this[kSession] = this.client.startSession({ owner: this, explicit: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new uninitialized copy of this cursor, with options matching those that have been set on the current instance
|
||||
*/
|
||||
abstract clone(): AbstractCursor<TSchema>;
|
||||
|
||||
/** @internal */
|
||||
abstract _initialize(
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<ExecutionResult>
|
||||
): void;
|
||||
|
||||
/** @internal */
|
||||
_getMore(batchSize: number, callback: Callback<Document>): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const getMoreOperation = new GetMoreOperation(this[kNamespace], this[kId]!, this[kServer]!, {
|
||||
...this[kOptions],
|
||||
session: this[kSession],
|
||||
batchSize
|
||||
});
|
||||
|
||||
executeOperation(this[kClient], getMoreOperation, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* This function is exposed for the unified test runner's createChangeStream
|
||||
* operation. We cannot refactor to use the abstract _initialize method without
|
||||
* a significant refactor.
|
||||
*/
|
||||
[kInit](callback: Callback<TSchema | null>): void {
|
||||
this._initialize(this[kSession], (err, state) => {
|
||||
if (state) {
|
||||
const response = state.response;
|
||||
this[kServer] = state.server;
|
||||
|
||||
if (response.cursor) {
|
||||
// TODO(NODE-2674): Preserve int64 sent from MongoDB
|
||||
this[kId] =
|
||||
typeof response.cursor.id === 'number'
|
||||
? Long.fromNumber(response.cursor.id)
|
||||
: response.cursor.id;
|
||||
|
||||
if (response.cursor.ns) {
|
||||
this[kNamespace] = ns(response.cursor.ns);
|
||||
}
|
||||
|
||||
this[kDocuments] = response.cursor.firstBatch;
|
||||
}
|
||||
|
||||
// When server responses return without a cursor document, we close this cursor
|
||||
// and return the raw server response. This is often the case for explain commands
|
||||
// for example
|
||||
if (this[kId] == null) {
|
||||
this[kId] = Long.ZERO;
|
||||
// TODO(NODE-3286): ExecutionResult needs to accept a generic parameter
|
||||
this[kDocuments] = [state.response as TODO_NODE_3286];
|
||||
}
|
||||
}
|
||||
|
||||
// the cursor is now initialized, even if an error occurred or it is dead
|
||||
this[kInitialized] = true;
|
||||
|
||||
if (err || cursorIsDead(this)) {
|
||||
return cleanupCursor(this, { error: err }, () => callback(err, nextDocument(this)));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function nextDocument<T>(cursor: AbstractCursor): T | null {
|
||||
if (cursor[kDocuments] == null || !cursor[kDocuments].length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const doc = cursor[kDocuments].shift();
|
||||
if (doc) {
|
||||
const transform = cursor[kTransform];
|
||||
if (transform) {
|
||||
return transform(doc) as T;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cursor - the cursor on which to call `next`
|
||||
* @param blocking - a boolean indicating whether or not the cursor should `block` until data
|
||||
* is available. Generally, this flag is set to `false` because if the getMore returns no documents,
|
||||
* the cursor has been exhausted. In certain scenarios (ChangeStreams, tailable await cursors and
|
||||
* `tryNext`, for example) blocking is necessary because a getMore returning no documents does
|
||||
* not indicate the end of the cursor.
|
||||
* @param callback - callback to return the result to the caller
|
||||
* @returns
|
||||
*/
|
||||
export function next<T>(
|
||||
cursor: AbstractCursor<T>,
|
||||
blocking: boolean,
|
||||
callback: Callback<T | null>
|
||||
): void {
|
||||
const cursorId = cursor[kId];
|
||||
if (cursor.closed) {
|
||||
return callback(undefined, null);
|
||||
}
|
||||
|
||||
if (cursor[kDocuments] && cursor[kDocuments].length) {
|
||||
callback(undefined, nextDocument<T>(cursor));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursorId == null) {
|
||||
// All cursors must operate within a session, one must be made implicitly if not explicitly provided
|
||||
cursor[kInit]((err, value) => {
|
||||
if (err) return callback(err);
|
||||
if (value) {
|
||||
return callback(undefined, value);
|
||||
}
|
||||
return next(cursor, blocking, callback);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursorIsDead(cursor)) {
|
||||
return cleanupCursor(cursor, undefined, () => callback(undefined, null));
|
||||
}
|
||||
|
||||
// otherwise need to call getMore
|
||||
const batchSize = cursor[kOptions].batchSize || 1000;
|
||||
cursor._getMore(batchSize, (err, response) => {
|
||||
if (response) {
|
||||
const cursorId =
|
||||
typeof response.cursor.id === 'number'
|
||||
? Long.fromNumber(response.cursor.id)
|
||||
: response.cursor.id;
|
||||
|
||||
cursor[kDocuments] = response.cursor.nextBatch;
|
||||
cursor[kId] = cursorId;
|
||||
}
|
||||
|
||||
if (err || cursorIsDead(cursor)) {
|
||||
return cleanupCursor(cursor, { error: err }, () => callback(err, nextDocument<T>(cursor)));
|
||||
}
|
||||
|
||||
if (cursor[kDocuments].length === 0 && blocking === false) {
|
||||
return callback(undefined, null);
|
||||
}
|
||||
|
||||
next(cursor, blocking, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function cursorIsDead(cursor: AbstractCursor): boolean {
|
||||
const cursorId = cursor[kId];
|
||||
return !!cursorId && cursorId.isZero();
|
||||
}
|
||||
|
||||
function cleanupCursor(
|
||||
cursor: AbstractCursor,
|
||||
options: { error?: AnyError | undefined; needsToEmitClosed?: boolean } | undefined,
|
||||
callback: Callback
|
||||
): void {
|
||||
const cursorId = cursor[kId];
|
||||
const cursorNs = cursor[kNamespace];
|
||||
const server = cursor[kServer];
|
||||
const session = cursor[kSession];
|
||||
const error = options?.error;
|
||||
const needsToEmitClosed = options?.needsToEmitClosed ?? cursor[kDocuments].length === 0;
|
||||
|
||||
if (error) {
|
||||
if (cursor.loadBalanced && error instanceof MongoNetworkError) {
|
||||
return completeCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorId == null || server == null || cursorId.isZero() || cursorNs == null) {
|
||||
if (needsToEmitClosed) {
|
||||
cursor[kClosed] = true;
|
||||
cursor[kId] = Long.ZERO;
|
||||
cursor.emit(AbstractCursor.CLOSE);
|
||||
}
|
||||
|
||||
if (session) {
|
||||
if (session.owner === cursor) {
|
||||
return session.endSession({ error }, callback);
|
||||
}
|
||||
|
||||
if (!session.inTransaction()) {
|
||||
maybeClearPinnedConnection(session, { error });
|
||||
}
|
||||
}
|
||||
|
||||
return callback();
|
||||
}
|
||||
|
||||
function completeCleanup() {
|
||||
if (session) {
|
||||
if (session.owner === cursor) {
|
||||
return session.endSession({ error }, () => {
|
||||
cursor.emit(AbstractCursor.CLOSE);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
if (!session.inTransaction()) {
|
||||
maybeClearPinnedConnection(session, { error });
|
||||
}
|
||||
}
|
||||
|
||||
cursor.emit(AbstractCursor.CLOSE);
|
||||
return callback();
|
||||
}
|
||||
|
||||
cursor[kKilled] = true;
|
||||
|
||||
return executeOperation(
|
||||
cursor[kClient],
|
||||
new KillCursorsOperation(cursorId, cursorNs, server, { session }),
|
||||
completeCleanup
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function assertUninitialized(cursor: AbstractCursor): void {
|
||||
if (cursor[kInitialized]) {
|
||||
throw new MongoCursorInUseError();
|
||||
}
|
||||
}
|
||||
|
||||
class ReadableCursorStream extends Readable {
|
||||
private _cursor: AbstractCursor;
|
||||
private _readInProgress = false;
|
||||
|
||||
constructor(cursor: AbstractCursor) {
|
||||
super({
|
||||
objectMode: true,
|
||||
autoDestroy: false,
|
||||
highWaterMark: 1
|
||||
});
|
||||
this._cursor = cursor;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
override _read(size: number): void {
|
||||
if (!this._readInProgress) {
|
||||
this._readInProgress = true;
|
||||
this._readNext();
|
||||
}
|
||||
}
|
||||
|
||||
override _destroy(error: Error | null, callback: (error?: Error | null) => void): void {
|
||||
this._cursor.close(err => process.nextTick(callback, err || error));
|
||||
}
|
||||
|
||||
private _readNext() {
|
||||
next(this._cursor, true, (err, result) => {
|
||||
if (err) {
|
||||
// NOTE: This is questionable, but we have a test backing the behavior. It seems the
|
||||
// desired behavior is that a stream ends cleanly when a user explicitly closes
|
||||
// a client during iteration. Alternatively, we could do the "right" thing and
|
||||
// propagate the error message by removing this special case.
|
||||
if (err.message.match(/server is closed/)) {
|
||||
this._cursor.close().catch(() => null);
|
||||
return this.push(null);
|
||||
}
|
||||
|
||||
// NOTE: This is also perhaps questionable. The rationale here is that these errors tend
|
||||
// to be "operation interrupted", where a cursor has been closed but there is an
|
||||
// active getMore in-flight. This used to check if the cursor was killed but once
|
||||
// that changed to happen in cleanup legitimate errors would not destroy the
|
||||
// stream. There are change streams test specifically test these cases.
|
||||
if (err.message.match(/interrupted/)) {
|
||||
return this.push(null);
|
||||
}
|
||||
|
||||
return this.destroy(err);
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
this.push(null);
|
||||
} else if (this.destroyed) {
|
||||
this._cursor.close().catch(() => null);
|
||||
} else {
|
||||
if (this.push(result)) {
|
||||
return this._readNext();
|
||||
}
|
||||
|
||||
this._readInProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
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 { ClientSession } from '../sessions';
|
||||
import type { Sort } from '../sort';
|
||||
import type { Callback, MongoDBNamespace } from '../utils';
|
||||
import { mergeOptions } from '../utils';
|
||||
import type { AbstractCursorOptions } from './abstract_cursor';
|
||||
import { AbstractCursor, assertUninitialized } from './abstract_cursor';
|
||||
|
||||
/** @public */
|
||||
export interface AggregationCursorOptions extends AbstractCursorOptions, AggregateOptions {}
|
||||
|
||||
/** @internal */
|
||||
const kPipeline = Symbol('pipeline');
|
||||
/** @internal */
|
||||
const kOptions = Symbol('options');
|
||||
|
||||
/**
|
||||
* The **AggregationCursor** class is an internal class that embodies an aggregation cursor on MongoDB
|
||||
* allowing for iteration over the results returned from the underlying query. It supports
|
||||
* one by one document iteration, conversion to an array or can be iterated as a Node 4.X
|
||||
* or higher stream
|
||||
* @public
|
||||
*/
|
||||
export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
|
||||
/** @internal */
|
||||
[kPipeline]: Document[];
|
||||
/** @internal */
|
||||
[kOptions]: AggregateOptions;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
client: MongoClient,
|
||||
namespace: MongoDBNamespace,
|
||||
pipeline: Document[] = [],
|
||||
options: AggregateOptions = {}
|
||||
) {
|
||||
super(client, namespace, options);
|
||||
|
||||
this[kPipeline] = pipeline;
|
||||
this[kOptions] = options;
|
||||
}
|
||||
|
||||
get pipeline(): Document[] {
|
||||
return this[kPipeline];
|
||||
}
|
||||
|
||||
clone(): AggregationCursor<TSchema> {
|
||||
const clonedOptions = mergeOptions({}, this[kOptions]);
|
||||
delete clonedOptions.session;
|
||||
return new AggregationCursor(this.client, this.namespace, this[kPipeline], {
|
||||
...clonedOptions
|
||||
});
|
||||
}
|
||||
|
||||
override map<T>(transform: (doc: TSchema) => T): AggregationCursor<T> {
|
||||
return super.map(transform) as AggregationCursor<T>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_initialize(session: ClientSession, callback: Callback<ExecutionResult>): void {
|
||||
const aggregateOperation = new AggregateOperation(this.namespace, this[kPipeline], {
|
||||
...this[kOptions],
|
||||
...this.cursorOptions,
|
||||
session
|
||||
});
|
||||
|
||||
executeOperation(this.client, aggregateOperation, (err, response) => {
|
||||
if (err || response == null) return callback(err);
|
||||
|
||||
// TODO: NODE-2882
|
||||
callback(undefined, { server: aggregateOperation.server, session, response });
|
||||
});
|
||||
}
|
||||
|
||||
/** Execute the explain for the cursor */
|
||||
explain(): Promise<Document>;
|
||||
explain(callback: Callback): void;
|
||||
explain(verbosity: ExplainVerbosityLike): Promise<Document>;
|
||||
explain(
|
||||
verbosity?: ExplainVerbosityLike | Callback,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true);
|
||||
if (verbosity == null) verbosity = true;
|
||||
|
||||
return executeOperation(
|
||||
this.client,
|
||||
new AggregateOperation(this.namespace, this[kPipeline], {
|
||||
...this[kOptions], // NOTE: order matters here, we may need to refine this
|
||||
...this.cursorOptions,
|
||||
explain: verbosity
|
||||
}),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/** Add a group stage to the aggregation pipeline */
|
||||
group<T = TSchema>($group: Document): AggregationCursor<T>;
|
||||
group($group: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $group });
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add a limit stage to the aggregation pipeline */
|
||||
limit($limit: number): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $limit });
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add a match stage to the aggregation pipeline */
|
||||
match($match: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $match });
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add an out stage to the aggregation pipeline */
|
||||
out($out: { db: string; coll: string } | string): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $out });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a project stage to the aggregation pipeline
|
||||
*
|
||||
* @remarks
|
||||
* In order to strictly type this function you must provide an interface
|
||||
* that represents the effect of your projection on the result documents.
|
||||
*
|
||||
* By default chaining a projection to your cursor changes the returned type to the generic {@link Document} type.
|
||||
* You should specify a parameterized type to have assertions on your final results.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Best way
|
||||
* const docs: AggregationCursor<{ a: number }> = cursor.project<{ a: number }>({ _id: 0, a: true });
|
||||
* // Flexible way
|
||||
* const docs: AggregationCursor<Document> = cursor.project({ _id: 0, a: true });
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* In order to strictly type this function you must provide an interface
|
||||
* that represents the effect of your projection on the result documents.
|
||||
*
|
||||
* **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
|
||||
* it **does not** return a new instance of a cursor. This means when calling project,
|
||||
* you should always assign the result to a new variable in order to get a correctly typed cursor variable.
|
||||
* Take note of the following example:
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cursor: AggregationCursor<{ a: number; b: string }> = coll.aggregate([]);
|
||||
* const projectCursor = cursor.project<{ a: number }>({ _id: 0, a: true });
|
||||
* const aPropOnlyArray: {a: number}[] = await projectCursor.toArray();
|
||||
*
|
||||
* // or always use chaining and save the final cursor
|
||||
*
|
||||
* const cursor = coll.aggregate().project<{ a: string }>({
|
||||
* _id: 0,
|
||||
* a: { $convert: { input: '$a', to: 'string' }
|
||||
* }});
|
||||
* ```
|
||||
*/
|
||||
project<T extends Document = Document>($project: Document): AggregationCursor<T> {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $project });
|
||||
return this as unknown as AggregationCursor<T>;
|
||||
}
|
||||
|
||||
/** Add a lookup stage to the aggregation pipeline */
|
||||
lookup($lookup: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $lookup });
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add a redact stage to the aggregation pipeline */
|
||||
redact($redact: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $redact });
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add a skip stage to the aggregation pipeline */
|
||||
skip($skip: number): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $skip });
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add a sort stage to the aggregation pipeline */
|
||||
sort($sort: Sort): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $sort });
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add a unwind stage to the aggregation pipeline */
|
||||
unwind($unwind: Document | string): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $unwind });
|
||||
return this;
|
||||
}
|
||||
|
||||
// deprecated methods
|
||||
/** @deprecated Add a geoNear stage to the aggregation pipeline */
|
||||
geoNear($geoNear: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kPipeline].push({ $geoNear });
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import type { Document, Long, Timestamp } from '../bson';
|
||||
import {
|
||||
type ChangeStreamDocument,
|
||||
type ChangeStreamEvents,
|
||||
type OperationTime,
|
||||
type ResumeToken,
|
||||
ChangeStream
|
||||
} from '../change_stream';
|
||||
import { INIT, RESPONSE } from '../constants';
|
||||
import type { MongoClient } from '../mongo_client';
|
||||
import type { TODO_NODE_3286 } from '../mongo_types';
|
||||
import { AggregateOperation } from '../operations/aggregate';
|
||||
import type { CollationOptions } from '../operations/command';
|
||||
import { type ExecutionResult, executeOperation } from '../operations/execute_operation';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { type Callback, type MongoDBNamespace, maxWireVersion } from '../utils';
|
||||
import { type AbstractCursorOptions, AbstractCursor } from './abstract_cursor';
|
||||
|
||||
/** @internal */
|
||||
export interface ChangeStreamCursorOptions extends AbstractCursorOptions {
|
||||
startAtOperationTime?: OperationTime;
|
||||
resumeAfter?: ResumeToken;
|
||||
startAfter?: ResumeToken;
|
||||
maxAwaitTimeMS?: number;
|
||||
collation?: CollationOptions;
|
||||
fullDocument?: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type ChangeStreamAggregateRawResult<TChange> = {
|
||||
$clusterTime: { clusterTime: Timestamp };
|
||||
cursor: {
|
||||
postBatchResumeToken: ResumeToken;
|
||||
ns: string;
|
||||
id: number | Long;
|
||||
} & ({ firstBatch: TChange[] } | { nextBatch: TChange[] });
|
||||
ok: 1;
|
||||
operationTime: Timestamp;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export class ChangeStreamCursor<
|
||||
TSchema extends Document = Document,
|
||||
TChange extends Document = ChangeStreamDocument<TSchema>
|
||||
> extends AbstractCursor<TChange, ChangeStreamEvents> {
|
||||
_resumeToken: ResumeToken;
|
||||
startAtOperationTime?: OperationTime;
|
||||
hasReceived?: boolean;
|
||||
resumeAfter: ResumeToken;
|
||||
startAfter: ResumeToken;
|
||||
options: ChangeStreamCursorOptions;
|
||||
|
||||
postBatchResumeToken?: ResumeToken;
|
||||
pipeline: Document[];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* used to determine change stream resumability
|
||||
*/
|
||||
maxWireVersion: number | undefined;
|
||||
|
||||
constructor(
|
||||
client: MongoClient,
|
||||
namespace: MongoDBNamespace,
|
||||
pipeline: Document[] = [],
|
||||
options: ChangeStreamCursorOptions = {}
|
||||
) {
|
||||
super(client, namespace, options);
|
||||
|
||||
this.pipeline = pipeline;
|
||||
this.options = options;
|
||||
this._resumeToken = null;
|
||||
this.startAtOperationTime = options.startAtOperationTime;
|
||||
|
||||
if (options.startAfter) {
|
||||
this.resumeToken = options.startAfter;
|
||||
} else if (options.resumeAfter) {
|
||||
this.resumeToken = options.resumeAfter;
|
||||
}
|
||||
}
|
||||
|
||||
set resumeToken(token: ResumeToken) {
|
||||
this._resumeToken = token;
|
||||
this.emit(ChangeStream.RESUME_TOKEN_CHANGED, token);
|
||||
}
|
||||
|
||||
get resumeToken(): ResumeToken {
|
||||
return this._resumeToken;
|
||||
}
|
||||
|
||||
get resumeOptions(): ChangeStreamCursorOptions {
|
||||
const options: ChangeStreamCursorOptions = {
|
||||
...this.options
|
||||
};
|
||||
|
||||
for (const key of ['resumeAfter', 'startAfter', 'startAtOperationTime'] as const) {
|
||||
delete options[key];
|
||||
}
|
||||
|
||||
if (this.resumeToken != null) {
|
||||
if (this.options.startAfter && !this.hasReceived) {
|
||||
options.startAfter = this.resumeToken;
|
||||
} else {
|
||||
options.resumeAfter = this.resumeToken;
|
||||
}
|
||||
} else if (this.startAtOperationTime != null && maxWireVersion(this.server) >= 7) {
|
||||
options.startAtOperationTime = this.startAtOperationTime;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
cacheResumeToken(resumeToken: ResumeToken): void {
|
||||
if (this.bufferedCount() === 0 && this.postBatchResumeToken) {
|
||||
this.resumeToken = this.postBatchResumeToken;
|
||||
} else {
|
||||
this.resumeToken = resumeToken;
|
||||
}
|
||||
this.hasReceived = true;
|
||||
}
|
||||
|
||||
_processBatch(response: ChangeStreamAggregateRawResult<TChange>): void {
|
||||
const cursor = response.cursor;
|
||||
if (cursor.postBatchResumeToken) {
|
||||
this.postBatchResumeToken = response.cursor.postBatchResumeToken;
|
||||
|
||||
const batch =
|
||||
'firstBatch' in response.cursor ? response.cursor.firstBatch : response.cursor.nextBatch;
|
||||
if (batch.length === 0) {
|
||||
this.resumeToken = cursor.postBatchResumeToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clone(): AbstractCursor<TChange> {
|
||||
return new ChangeStreamCursor(this.client, this.namespace, this.pipeline, {
|
||||
...this.cursorOptions
|
||||
});
|
||||
}
|
||||
|
||||
_initialize(session: ClientSession, callback: Callback<ExecutionResult>): void {
|
||||
const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, {
|
||||
...this.cursorOptions,
|
||||
...this.options,
|
||||
session
|
||||
});
|
||||
|
||||
executeOperation<TODO_NODE_3286, ChangeStreamAggregateRawResult<TChange>>(
|
||||
session.client,
|
||||
aggregateOperation,
|
||||
(err, response) => {
|
||||
if (err || response == null) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const server = aggregateOperation.server;
|
||||
this.maxWireVersion = maxWireVersion(server);
|
||||
|
||||
if (
|
||||
this.startAtOperationTime == null &&
|
||||
this.resumeAfter == null &&
|
||||
this.startAfter == null &&
|
||||
this.maxWireVersion >= 7
|
||||
) {
|
||||
this.startAtOperationTime = response.operationTime;
|
||||
}
|
||||
|
||||
this._processBatch(response);
|
||||
|
||||
this.emit(INIT, response);
|
||||
this.emit(RESPONSE);
|
||||
|
||||
// TODO: NODE-2882
|
||||
callback(undefined, { server, session, response });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override _getMore(batchSize: number, callback: Callback): void {
|
||||
super._getMore(batchSize, (err, response) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this.maxWireVersion = maxWireVersion(this.server);
|
||||
this._processBatch(response as TODO_NODE_3286 as ChangeStreamAggregateRawResult<TChange>);
|
||||
|
||||
this.emit(ChangeStream.MORE, response);
|
||||
this.emit(ChangeStream.RESPONSE);
|
||||
callback(err, response);
|
||||
});
|
||||
}
|
||||
}
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
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 { ClientSession } from '../sessions';
|
||||
import { formatSort, Sort, SortDirection } from '../sort';
|
||||
import { Callback, emitWarningOnce, mergeOptions, MongoDBNamespace } from '../utils';
|
||||
import { AbstractCursor, assertUninitialized } from './abstract_cursor';
|
||||
|
||||
/** @internal */
|
||||
const kFilter = Symbol('filter');
|
||||
/** @internal */
|
||||
const kNumReturned = Symbol('numReturned');
|
||||
/** @internal */
|
||||
const kBuiltOptions = Symbol('builtOptions');
|
||||
|
||||
/** @public Flags allowed for cursor */
|
||||
export const FLAGS = [
|
||||
'tailable',
|
||||
'oplogReplay',
|
||||
'noCursorTimeout',
|
||||
'awaitData',
|
||||
'exhaust',
|
||||
'partial'
|
||||
] as const;
|
||||
|
||||
/** @public */
|
||||
export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
|
||||
/** @internal */
|
||||
[kFilter]: Document;
|
||||
/** @internal */
|
||||
[kNumReturned]?: number;
|
||||
/** @internal */
|
||||
[kBuiltOptions]: FindOptions;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
client: MongoClient,
|
||||
namespace: MongoDBNamespace,
|
||||
filter: Document | undefined,
|
||||
options: FindOptions = {}
|
||||
) {
|
||||
super(client, namespace, options);
|
||||
|
||||
this[kFilter] = filter || {};
|
||||
this[kBuiltOptions] = options;
|
||||
|
||||
if (options.sort != null) {
|
||||
this[kBuiltOptions].sort = formatSort(options.sort);
|
||||
}
|
||||
}
|
||||
|
||||
clone(): FindCursor<TSchema> {
|
||||
const clonedOptions = mergeOptions({}, this[kBuiltOptions]);
|
||||
delete clonedOptions.session;
|
||||
return new FindCursor(this.client, this.namespace, this[kFilter], {
|
||||
...clonedOptions
|
||||
});
|
||||
}
|
||||
|
||||
override map<T>(transform: (doc: TSchema) => T): FindCursor<T> {
|
||||
return super.map(transform) as FindCursor<T>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_initialize(session: ClientSession, callback: Callback<ExecutionResult>): void {
|
||||
const findOperation = new FindOperation(undefined, this.namespace, this[kFilter], {
|
||||
...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this
|
||||
...this.cursorOptions,
|
||||
session
|
||||
});
|
||||
|
||||
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
|
||||
// the value should only be saved in those cases.
|
||||
if (response.cursor) {
|
||||
this[kNumReturned] = response.cursor.firstBatch.length;
|
||||
} else {
|
||||
this[kNumReturned] = response.documents ? response.documents.length : 0;
|
||||
}
|
||||
|
||||
// TODO: NODE-2882
|
||||
callback(undefined, { server: findOperation.server, session, response });
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override _getMore(batchSize: number, callback: Callback<Document>): void {
|
||||
// NOTE: this is to support client provided limits in pre-command servers
|
||||
const numReturned = this[kNumReturned];
|
||||
if (numReturned) {
|
||||
const limit = this[kBuiltOptions].limit;
|
||||
batchSize =
|
||||
limit && limit > 0 && numReturned + batchSize > limit ? limit - numReturned : batchSize;
|
||||
|
||||
if (batchSize <= 0) {
|
||||
return this.close(callback);
|
||||
}
|
||||
}
|
||||
|
||||
super._getMore(batchSize, (err, response) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// TODO: wrap this in some logic to prevent it from happening if we don't need this support
|
||||
if (response) {
|
||||
this[kNumReturned] = this[kNumReturned] + response.cursor.nextBatch.length;
|
||||
}
|
||||
|
||||
callback(undefined, response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of documents for this cursor
|
||||
* @deprecated Use `collection.estimatedDocumentCount` or `collection.countDocuments` instead
|
||||
*/
|
||||
count(): Promise<number>;
|
||||
/** @deprecated Use `collection.estimatedDocumentCount` or `collection.countDocuments` instead */
|
||||
count(callback: Callback<number>): void;
|
||||
/** @deprecated Use `collection.estimatedDocumentCount` or `collection.countDocuments` instead */
|
||||
count(options: CountOptions): Promise<number>;
|
||||
/** @deprecated Use `collection.estimatedDocumentCount` or `collection.countDocuments` instead */
|
||||
count(options: CountOptions, callback: Callback<number>): void;
|
||||
count(
|
||||
options?: CountOptions | Callback<number>,
|
||||
callback?: Callback<number>
|
||||
): Promise<number> | void {
|
||||
emitWarningOnce(
|
||||
'cursor.count is deprecated and will be removed in the next major version, please use `collection.estimatedDocumentCount` or `collection.countDocuments` instead '
|
||||
);
|
||||
if (typeof options === 'boolean') {
|
||||
throw new MongoInvalidArgumentError('Invalid first parameter to count');
|
||||
}
|
||||
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
|
||||
return executeOperation(
|
||||
this.client,
|
||||
new CountOperation(this.namespace, this[kFilter], {
|
||||
...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this
|
||||
...this.cursorOptions,
|
||||
...options
|
||||
}),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/** Execute the explain for the cursor */
|
||||
explain(): Promise<Document>;
|
||||
explain(callback: Callback): void;
|
||||
explain(verbosity?: ExplainVerbosityLike): Promise<Document>;
|
||||
explain(
|
||||
verbosity?: ExplainVerbosityLike | Callback,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true);
|
||||
if (verbosity == null) verbosity = true;
|
||||
|
||||
return executeOperation(
|
||||
this.client,
|
||||
new FindOperation(undefined, this.namespace, this[kFilter], {
|
||||
...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this
|
||||
...this.cursorOptions,
|
||||
explain: verbosity
|
||||
}),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/** Set the cursor query */
|
||||
filter(filter: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kFilter] = filter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cursor hint
|
||||
*
|
||||
* @param hint - If specified, then the query system will only consider plans using the hinted index.
|
||||
*/
|
||||
hint(hint: Hint): this {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].hint = hint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cursor min
|
||||
*
|
||||
* @param min - Specify a $min value to specify the inclusive lower bound for a specific index in order to constrain the results of find(). The $min specifies the lower bound for all keys of a specific index in order.
|
||||
*/
|
||||
min(min: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].min = min;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cursor max
|
||||
*
|
||||
* @param max - Specify a $max value to specify the exclusive upper bound for a specific index in order to constrain the results of find(). The $max specifies the upper bound for all keys of a specific index in order.
|
||||
*/
|
||||
max(max: Document): this {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].max = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cursor returnKey.
|
||||
* If set to true, modifies the cursor to only return the index field or fields for the results of the query, rather than documents.
|
||||
* If set to true and the query does not use an index to perform the read operation, the returned documents will not contain any fields.
|
||||
*
|
||||
* @param value - the returnKey value.
|
||||
*/
|
||||
returnKey(value: boolean): this {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].returnKey = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the output of a query by adding a field $recordId to matching documents. $recordId is the internal key which uniquely identifies a document in a collection.
|
||||
*
|
||||
* @param value - The $showDiskLoc option has now been deprecated and replaced with the showRecordId field. $showDiskLoc will still be accepted for OP_QUERY stye find.
|
||||
*/
|
||||
showRecordId(value: boolean): this {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].showRecordId = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a query modifier to the cursor query
|
||||
*
|
||||
* @param name - The query modifier (must start with $, such as $orderby etc)
|
||||
* @param value - The modifier value.
|
||||
*/
|
||||
addQueryModifier(name: string, value: string | boolean | number | Document): this {
|
||||
assertUninitialized(this);
|
||||
if (name[0] !== '$') {
|
||||
throw new MongoInvalidArgumentError(`${name} is not a valid query modifier`);
|
||||
}
|
||||
|
||||
// Strip of the $
|
||||
const field = name.substr(1);
|
||||
|
||||
// NOTE: consider some TS magic for this
|
||||
switch (field) {
|
||||
case 'comment':
|
||||
this[kBuiltOptions].comment = value as string | Document;
|
||||
break;
|
||||
|
||||
case 'explain':
|
||||
this[kBuiltOptions].explain = value as boolean;
|
||||
break;
|
||||
|
||||
case 'hint':
|
||||
this[kBuiltOptions].hint = value as string | Document;
|
||||
break;
|
||||
|
||||
case 'max':
|
||||
this[kBuiltOptions].max = value as Document;
|
||||
break;
|
||||
|
||||
case 'maxTimeMS':
|
||||
this[kBuiltOptions].maxTimeMS = value as number;
|
||||
break;
|
||||
|
||||
case 'min':
|
||||
this[kBuiltOptions].min = value as Document;
|
||||
break;
|
||||
|
||||
case 'orderby':
|
||||
this[kBuiltOptions].sort = formatSort(value as string | Document);
|
||||
break;
|
||||
|
||||
case 'query':
|
||||
this[kFilter] = value as Document;
|
||||
break;
|
||||
|
||||
case 'returnKey':
|
||||
this[kBuiltOptions].returnKey = value as boolean;
|
||||
break;
|
||||
|
||||
case 'showDiskLoc':
|
||||
this[kBuiltOptions].showRecordId = value as boolean;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new MongoInvalidArgumentError(`Invalid query modifier: ${name}`);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a comment to the cursor query allowing for tracking the comment in the log.
|
||||
*
|
||||
* @param value - The comment attached to this query.
|
||||
*/
|
||||
comment(value: string): this {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].comment = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a maxAwaitTimeMS on a tailing cursor query to allow to customize the timeout value for the option awaitData (Only supported on MongoDB 3.2 or higher, ignored otherwise)
|
||||
*
|
||||
* @param value - Number of milliseconds to wait before aborting the tailed query.
|
||||
*/
|
||||
maxAwaitTimeMS(value: number): this {
|
||||
assertUninitialized(this);
|
||||
if (typeof value !== 'number') {
|
||||
throw new MongoInvalidArgumentError('Argument for maxAwaitTimeMS must be a number');
|
||||
}
|
||||
|
||||
this[kBuiltOptions].maxAwaitTimeMS = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a maxTimeMS on the cursor query, allowing for hard timeout limits on queries (Only supported on MongoDB 2.6 or higher)
|
||||
*
|
||||
* @param value - Number of milliseconds to wait before aborting the query.
|
||||
*/
|
||||
override maxTimeMS(value: number): this {
|
||||
assertUninitialized(this);
|
||||
if (typeof value !== 'number') {
|
||||
throw new MongoInvalidArgumentError('Argument for maxTimeMS must be a number');
|
||||
}
|
||||
|
||||
this[kBuiltOptions].maxTimeMS = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a project stage to the aggregation pipeline
|
||||
*
|
||||
* @remarks
|
||||
* In order to strictly type this function you must provide an interface
|
||||
* that represents the effect of your projection on the result documents.
|
||||
*
|
||||
* By default chaining a projection to your cursor changes the returned type to the generic
|
||||
* {@link Document} type.
|
||||
* You should specify a parameterized type to have assertions on your final results.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Best way
|
||||
* const docs: FindCursor<{ a: number }> = cursor.project<{ a: number }>({ _id: 0, a: true });
|
||||
* // Flexible way
|
||||
* const docs: FindCursor<Document> = cursor.project({ _id: 0, a: true });
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
|
||||
* it **does not** return a new instance of a cursor. This means when calling project,
|
||||
* you should always assign the result to a new variable in order to get a correctly typed cursor variable.
|
||||
* Take note of the following example:
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cursor: FindCursor<{ a: number; b: string }> = coll.find();
|
||||
* const projectCursor = cursor.project<{ a: number }>({ _id: 0, a: true });
|
||||
* const aPropOnlyArray: {a: number}[] = await projectCursor.toArray();
|
||||
*
|
||||
* // or always use chaining and save the final cursor
|
||||
*
|
||||
* const cursor = coll.find().project<{ a: string }>({
|
||||
* _id: 0,
|
||||
* a: { $convert: { input: '$a', to: 'string' }
|
||||
* }});
|
||||
* ```
|
||||
*/
|
||||
project<T extends Document = Document>(value: Document): FindCursor<T> {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].projection = value;
|
||||
return this as unknown as FindCursor<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sort order of the cursor query.
|
||||
*
|
||||
* @param sort - The key or keys set for the sort.
|
||||
* @param direction - The direction of the sorting (1 or -1).
|
||||
*/
|
||||
sort(sort: Sort | string, direction?: SortDirection): this {
|
||||
assertUninitialized(this);
|
||||
if (this[kBuiltOptions].tailable) {
|
||||
throw new MongoTailableCursorError('Tailable cursor does not support sorting');
|
||||
}
|
||||
|
||||
this[kBuiltOptions].sort = formatSort(sort, direction);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows disk use for blocking sort operations exceeding 100MB memory. (MongoDB 3.2 or higher)
|
||||
*
|
||||
* @remarks
|
||||
* {@link https://docs.mongodb.com/manual/reference/command/find/#find-cmd-allowdiskuse | find command allowDiskUse documentation}
|
||||
*/
|
||||
allowDiskUse(allow = true): this {
|
||||
assertUninitialized(this);
|
||||
|
||||
if (!this[kBuiltOptions].sort) {
|
||||
throw new MongoInvalidArgumentError('Option "allowDiskUse" requires a sort specification');
|
||||
}
|
||||
|
||||
// As of 6.0 the default is true. This allows users to get back to the old behaviour.
|
||||
if (!allow) {
|
||||
this[kBuiltOptions].allowDiskUse = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
this[kBuiltOptions].allowDiskUse = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the collation options for the cursor.
|
||||
*
|
||||
* @param value - The cursor collation options (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
|
||||
*/
|
||||
collation(value: CollationOptions): this {
|
||||
assertUninitialized(this);
|
||||
this[kBuiltOptions].collation = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the limit for the cursor.
|
||||
*
|
||||
* @param value - The limit for the cursor query.
|
||||
*/
|
||||
limit(value: number): this {
|
||||
assertUninitialized(this);
|
||||
if (this[kBuiltOptions].tailable) {
|
||||
throw new MongoTailableCursorError('Tailable cursor does not support limit');
|
||||
}
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
throw new MongoInvalidArgumentError('Operation "limit" requires an integer');
|
||||
}
|
||||
|
||||
this[kBuiltOptions].limit = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the skip for the cursor.
|
||||
*
|
||||
* @param value - The skip for the cursor query.
|
||||
*/
|
||||
skip(value: number): this {
|
||||
assertUninitialized(this);
|
||||
if (this[kBuiltOptions].tailable) {
|
||||
throw new MongoTailableCursorError('Tailable cursor does not support skip');
|
||||
}
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
throw new MongoInvalidArgumentError('Operation "skip" requires an integer');
|
||||
}
|
||||
|
||||
this[kBuiltOptions].skip = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Db } from '../db';
|
||||
import { executeOperation, ExecutionResult } from '../operations/execute_operation';
|
||||
import {
|
||||
CollectionInfo,
|
||||
ListCollectionsOperation,
|
||||
ListCollectionsOptions
|
||||
} from '../operations/list_collections';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { AbstractCursor } from './abstract_cursor';
|
||||
|
||||
/** @public */
|
||||
export class ListCollectionsCursor<
|
||||
T extends Pick<CollectionInfo, 'name' | 'type'> | CollectionInfo =
|
||||
| Pick<CollectionInfo, 'name' | 'type'>
|
||||
| CollectionInfo
|
||||
> extends AbstractCursor<T> {
|
||||
parent: Db;
|
||||
filter: Document;
|
||||
options?: ListCollectionsOptions;
|
||||
|
||||
constructor(db: Db, filter: Document, options?: ListCollectionsOptions) {
|
||||
super(db.s.client, db.s.namespace, options);
|
||||
this.parent = db;
|
||||
this.filter = filter;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
clone(): ListCollectionsCursor<T> {
|
||||
return new ListCollectionsCursor(this.parent, this.filter, {
|
||||
...this.options,
|
||||
...this.cursorOptions
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_initialize(session: ClientSession | undefined, callback: Callback<ExecutionResult>): void {
|
||||
const operation = new ListCollectionsOperation(this.parent, this.filter, {
|
||||
...this.cursorOptions,
|
||||
...this.options,
|
||||
session
|
||||
});
|
||||
|
||||
executeOperation(this.parent.s.client, operation, (err, response) => {
|
||||
if (err || response == null) return callback(err);
|
||||
|
||||
// TODO: NODE-2882
|
||||
callback(undefined, { server: operation.server, session, response });
|
||||
});
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import type { Collection } from '../collection';
|
||||
import { executeOperation, ExecutionResult } from '../operations/execute_operation';
|
||||
import { ListIndexesOperation, ListIndexesOptions } from '../operations/indexes';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { AbstractCursor } from './abstract_cursor';
|
||||
|
||||
/** @public */
|
||||
export class ListIndexesCursor extends AbstractCursor {
|
||||
parent: Collection;
|
||||
options?: ListIndexesOptions;
|
||||
|
||||
constructor(collection: Collection, options?: ListIndexesOptions) {
|
||||
super(collection.s.db.s.client, collection.s.namespace, options);
|
||||
this.parent = collection;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
clone(): ListIndexesCursor {
|
||||
return new ListIndexesCursor(this.parent, {
|
||||
...this.options,
|
||||
...this.cursorOptions
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_initialize(session: ClientSession | undefined, callback: Callback<ExecutionResult>): void {
|
||||
const operation = new ListIndexesOperation(this.parent, {
|
||||
...this.cursorOptions,
|
||||
...this.options,
|
||||
session
|
||||
});
|
||||
|
||||
executeOperation(this.parent.s.db.s.client, operation, (err, response) => {
|
||||
if (err || response == null) return callback(err);
|
||||
|
||||
// TODO: NODE-2882
|
||||
callback(undefined, { server: operation.server, session, response });
|
||||
});
|
||||
}
|
||||
}
|
||||
+770
@@ -0,0 +1,770 @@
|
||||
import { Admin } from './admin';
|
||||
import { BSONSerializeOptions, Document, resolveBSONOptions } from './bson';
|
||||
import { ChangeStream, ChangeStreamDocument, ChangeStreamOptions } from './change_stream';
|
||||
import { Collection, CollectionOptions } from './collection';
|
||||
import * as CONSTANTS from './constants';
|
||||
import { AggregationCursor } from './cursor/aggregation_cursor';
|
||||
import { ListCollectionsCursor } from './cursor/list_collections_cursor';
|
||||
import { MongoAPIError, MongoInvalidArgumentError } from './error';
|
||||
import { Logger, LoggerOptions } from './logger';
|
||||
import type { MongoClient, PkFactory } from './mongo_client';
|
||||
import type { TODO_NODE_3286 } from './mongo_types';
|
||||
import { AddUserOperation, AddUserOptions } from './operations/add_user';
|
||||
import type { AggregateOptions } from './operations/aggregate';
|
||||
import { CollectionsOperation } from './operations/collections';
|
||||
import type { IndexInformationOptions } from './operations/common_functions';
|
||||
import { CreateCollectionOperation, CreateCollectionOptions } from './operations/create_collection';
|
||||
import {
|
||||
DropCollectionOperation,
|
||||
DropCollectionOptions,
|
||||
DropDatabaseOperation,
|
||||
DropDatabaseOptions
|
||||
} from './operations/drop';
|
||||
import { executeOperation } from './operations/execute_operation';
|
||||
import {
|
||||
CreateIndexesOptions,
|
||||
CreateIndexOperation,
|
||||
IndexInformationOperation,
|
||||
IndexSpecification
|
||||
} from './operations/indexes';
|
||||
import type { CollectionInfo, ListCollectionsOptions } from './operations/list_collections';
|
||||
import { ProfilingLevelOperation, ProfilingLevelOptions } from './operations/profiling_level';
|
||||
import { RemoveUserOperation, RemoveUserOptions } from './operations/remove_user';
|
||||
import { RenameOperation, RenameOptions } from './operations/rename';
|
||||
import { RunCommandOperation, RunCommandOptions } from './operations/run_command';
|
||||
import {
|
||||
ProfilingLevel,
|
||||
SetProfilingLevelOperation,
|
||||
SetProfilingLevelOptions
|
||||
} from './operations/set_profiling_level';
|
||||
import { DbStatsOperation, DbStatsOptions } from './operations/stats';
|
||||
import { ReadConcern } from './read_concern';
|
||||
import { ReadPreference, ReadPreferenceLike } from './read_preference';
|
||||
import {
|
||||
Callback,
|
||||
DEFAULT_PK_FACTORY,
|
||||
filterOptions,
|
||||
getTopology,
|
||||
MongoDBNamespace,
|
||||
resolveOptions
|
||||
} from './utils';
|
||||
import { WriteConcern, WriteConcernOptions } from './write_concern';
|
||||
|
||||
// Allowed parameters
|
||||
const DB_OPTIONS_ALLOW_LIST = [
|
||||
'writeConcern',
|
||||
'readPreference',
|
||||
'readPreferenceTags',
|
||||
'native_parser',
|
||||
'forceServerObjectId',
|
||||
'pkFactory',
|
||||
'serializeFunctions',
|
||||
'raw',
|
||||
'authSource',
|
||||
'ignoreUndefined',
|
||||
'readConcern',
|
||||
'retryMiliSeconds',
|
||||
'numberOfRetries',
|
||||
'loggerLevel',
|
||||
'logger',
|
||||
'promoteBuffers',
|
||||
'promoteLongs',
|
||||
'bsonRegExp',
|
||||
'enableUtf8Validation',
|
||||
'promoteValues',
|
||||
'compression',
|
||||
'retryWrites'
|
||||
];
|
||||
|
||||
/** @internal */
|
||||
export interface DbPrivate {
|
||||
client: MongoClient;
|
||||
options?: DbOptions;
|
||||
logger: Logger;
|
||||
readPreference?: ReadPreference;
|
||||
pkFactory: PkFactory;
|
||||
readConcern?: ReadConcern;
|
||||
bsonOptions: BSONSerializeOptions;
|
||||
writeConcern?: WriteConcern;
|
||||
namespace: MongoDBNamespace;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface DbOptions extends BSONSerializeOptions, WriteConcernOptions, LoggerOptions {
|
||||
/** If the database authentication is dependent on another databaseName. */
|
||||
authSource?: string;
|
||||
/** Force server to assign _id values instead of driver. */
|
||||
forceServerObjectId?: boolean;
|
||||
/** The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). */
|
||||
readPreference?: ReadPreferenceLike;
|
||||
/** A primary key factory object for generation of custom _id keys. */
|
||||
pkFactory?: PkFactory;
|
||||
/** Specify a read concern for the collection. (only MongoDB 3.2 or higher supported) */
|
||||
readConcern?: ReadConcern;
|
||||
/** Should retry failed writes */
|
||||
retryWrites?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The **Db** class is a class that represents a MongoDB Database.
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const { MongoClient } = require('mongodb');
|
||||
* // Connection url
|
||||
* const url = 'mongodb://localhost:27017';
|
||||
* // Database Name
|
||||
* const dbName = 'test';
|
||||
* // Connect using MongoClient
|
||||
* MongoClient.connect(url, function(err, client) {
|
||||
* // Select the database by name
|
||||
* const testDb = client.db(dbName);
|
||||
* client.close();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class Db {
|
||||
/** @internal */
|
||||
s: DbPrivate;
|
||||
|
||||
public static SYSTEM_NAMESPACE_COLLECTION = CONSTANTS.SYSTEM_NAMESPACE_COLLECTION;
|
||||
public static SYSTEM_INDEX_COLLECTION = CONSTANTS.SYSTEM_INDEX_COLLECTION;
|
||||
public static SYSTEM_PROFILE_COLLECTION = CONSTANTS.SYSTEM_PROFILE_COLLECTION;
|
||||
public static SYSTEM_USER_COLLECTION = CONSTANTS.SYSTEM_USER_COLLECTION;
|
||||
public static SYSTEM_COMMAND_COLLECTION = CONSTANTS.SYSTEM_COMMAND_COLLECTION;
|
||||
public static SYSTEM_JS_COLLECTION = CONSTANTS.SYSTEM_JS_COLLECTION;
|
||||
|
||||
/**
|
||||
* Creates a new Db instance
|
||||
*
|
||||
* @param client - The MongoClient for the database.
|
||||
* @param databaseName - The name of the database this instance represents.
|
||||
* @param options - Optional settings for Db construction
|
||||
*/
|
||||
constructor(client: MongoClient, databaseName: string, options?: DbOptions) {
|
||||
options = options ?? {};
|
||||
|
||||
// Filter the options
|
||||
options = filterOptions(options, DB_OPTIONS_ALLOW_LIST);
|
||||
|
||||
// Ensure we have a valid db name
|
||||
validateDatabaseName(databaseName);
|
||||
|
||||
// Internal state of the db object
|
||||
this.s = {
|
||||
// Client
|
||||
client,
|
||||
// Options
|
||||
options,
|
||||
// Logger instance
|
||||
logger: new Logger('Db', options),
|
||||
// Unpack read preference
|
||||
readPreference: ReadPreference.fromOptions(options),
|
||||
// Merge bson options
|
||||
bsonOptions: resolveBSONOptions(options, client),
|
||||
// Set up the primary key factory or fallback to ObjectId
|
||||
pkFactory: options?.pkFactory ?? DEFAULT_PK_FACTORY,
|
||||
// ReadConcern
|
||||
readConcern: ReadConcern.fromOptions(options),
|
||||
writeConcern: WriteConcern.fromOptions(options),
|
||||
// Namespace
|
||||
namespace: new MongoDBNamespace(databaseName)
|
||||
};
|
||||
}
|
||||
|
||||
get databaseName(): string {
|
||||
return this.s.namespace.db;
|
||||
}
|
||||
|
||||
// Options
|
||||
get options(): DbOptions | undefined {
|
||||
return this.s.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* slaveOk specified
|
||||
* @deprecated Use secondaryOk instead
|
||||
*/
|
||||
get slaveOk(): boolean {
|
||||
return this.secondaryOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a secondary can be used (because the read preference is *not* set to primary)
|
||||
*/
|
||||
get secondaryOk(): boolean {
|
||||
return this.s.readPreference?.preference !== 'primary' || false;
|
||||
}
|
||||
|
||||
get readConcern(): ReadConcern | undefined {
|
||||
return this.s.readConcern;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current readPreference of the Db. If not explicitly defined for
|
||||
* this Db, will be inherited from the parent MongoClient
|
||||
*/
|
||||
get readPreference(): ReadPreference {
|
||||
if (this.s.readPreference == null) {
|
||||
return this.s.client.readPreference;
|
||||
}
|
||||
|
||||
return this.s.readPreference;
|
||||
}
|
||||
|
||||
get bsonOptions(): BSONSerializeOptions {
|
||||
return this.s.bsonOptions;
|
||||
}
|
||||
|
||||
// get the write Concern
|
||||
get writeConcern(): WriteConcern | undefined {
|
||||
return this.s.writeConcern;
|
||||
}
|
||||
|
||||
get namespace(): string {
|
||||
return this.s.namespace.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new collection on a server with the specified options. Use this to create capped collections.
|
||||
* More information about command options available at https://docs.mongodb.com/manual/reference/command/create/
|
||||
*
|
||||
* @param name - The name of the collection to create
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
createCollection<TSchema extends Document = Document>(
|
||||
name: string,
|
||||
options?: CreateCollectionOptions
|
||||
): Promise<Collection<TSchema>>;
|
||||
createCollection<TSchema extends Document = Document>(
|
||||
name: string,
|
||||
callback: Callback<Collection<TSchema>>
|
||||
): void;
|
||||
createCollection<TSchema extends Document = Document>(
|
||||
name: string,
|
||||
options: CreateCollectionOptions | undefined,
|
||||
callback: Callback<Collection<TSchema>>
|
||||
): void;
|
||||
createCollection<TSchema extends Document = Document>(
|
||||
name: string,
|
||||
options?: CreateCollectionOptions | Callback<Collection>,
|
||||
callback?: Callback<Collection>
|
||||
): Promise<Collection<TSchema>> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new CreateCollectionOperation(this, name, resolveOptions(this, options)) as TODO_NODE_3286,
|
||||
callback
|
||||
) as TODO_NODE_3286;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command
|
||||
*
|
||||
* @remarks
|
||||
* This command does not inherit options from the MongoClient.
|
||||
*
|
||||
* @param command - The command to run
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
command(command: Document): Promise<Document>;
|
||||
command(command: Document, callback: Callback<Document>): void;
|
||||
command(command: Document, options: RunCommandOptions): Promise<Document>;
|
||||
command(command: Document, options: RunCommandOptions, callback: Callback<Document>): void;
|
||||
command(
|
||||
command: Document,
|
||||
options?: RunCommandOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
// Intentionally, we do not inherit options from parent for this operation.
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new RunCommandOperation(this, command, options ?? {}),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an aggregation framework pipeline against the database, needs MongoDB \>= 3.6
|
||||
*
|
||||
* @param pipeline - An array of aggregation stages to be executed
|
||||
* @param options - Optional settings for the command
|
||||
*/
|
||||
aggregate<T extends Document = Document>(
|
||||
pipeline: Document[] = [],
|
||||
options?: AggregateOptions
|
||||
): AggregationCursor<T> {
|
||||
if (arguments.length > 2) {
|
||||
throw new MongoInvalidArgumentError('Method "db.aggregate()" accepts at most two arguments');
|
||||
}
|
||||
if (typeof pipeline === 'function') {
|
||||
throw new MongoInvalidArgumentError('Argument "pipeline" must not be function');
|
||||
}
|
||||
if (typeof options === 'function') {
|
||||
throw new MongoInvalidArgumentError('Argument "options" must not be function');
|
||||
}
|
||||
|
||||
return new AggregationCursor(
|
||||
this.s.client,
|
||||
this.s.namespace,
|
||||
pipeline,
|
||||
resolveOptions(this, options)
|
||||
);
|
||||
}
|
||||
|
||||
/** Return the Admin db instance */
|
||||
admin(): Admin {
|
||||
return new Admin(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to a MongoDB Collection. If it does not exist it will be created implicitly.
|
||||
*
|
||||
* @param name - the collection name we wish to access.
|
||||
* @returns return the new Collection instance
|
||||
*/
|
||||
collection<TSchema extends Document = Document>(
|
||||
name: string,
|
||||
options: CollectionOptions = {}
|
||||
): Collection<TSchema> {
|
||||
if (typeof options === 'function') {
|
||||
throw new MongoInvalidArgumentError('The callback form of this helper has been removed.');
|
||||
}
|
||||
const finalOptions = resolveOptions(this, options);
|
||||
return new Collection<TSchema>(this, name, finalOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the db statistics.
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
stats(): Promise<Document>;
|
||||
stats(callback: Callback<Document>): void;
|
||||
stats(options: DbStatsOptions): Promise<Document>;
|
||||
stats(options: DbStatsOptions, callback: Callback<Document>): void;
|
||||
stats(
|
||||
options?: DbStatsOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new DbStatsOperation(this, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all collections of this database with optional filter
|
||||
*
|
||||
* @param filter - Query to filter collections by
|
||||
* @param options - Optional settings for the command
|
||||
*/
|
||||
listCollections(
|
||||
filter: Document,
|
||||
options: Exclude<ListCollectionsOptions, 'nameOnly'> & { nameOnly: true }
|
||||
): ListCollectionsCursor<Pick<CollectionInfo, 'name' | 'type'>>;
|
||||
listCollections(
|
||||
filter: Document,
|
||||
options: Exclude<ListCollectionsOptions, 'nameOnly'> & { nameOnly: false }
|
||||
): ListCollectionsCursor<CollectionInfo>;
|
||||
listCollections<
|
||||
T extends Pick<CollectionInfo, 'name' | 'type'> | CollectionInfo =
|
||||
| Pick<CollectionInfo, 'name' | 'type'>
|
||||
| CollectionInfo
|
||||
>(filter?: Document, options?: ListCollectionsOptions): ListCollectionsCursor<T>;
|
||||
listCollections<
|
||||
T extends Pick<CollectionInfo, 'name' | 'type'> | CollectionInfo =
|
||||
| Pick<CollectionInfo, 'name' | 'type'>
|
||||
| CollectionInfo
|
||||
>(filter: Document = {}, options: ListCollectionsOptions = {}): ListCollectionsCursor<T> {
|
||||
return new ListCollectionsCursor<T>(this, filter, resolveOptions(this, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a collection.
|
||||
*
|
||||
* @remarks
|
||||
* This operation does not inherit options from the MongoClient.
|
||||
*
|
||||
* @param fromCollection - Name of current collection to rename
|
||||
* @param toCollection - New name of of the collection
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
renameCollection<TSchema extends Document = Document>(
|
||||
fromCollection: string,
|
||||
toCollection: string
|
||||
): Promise<Collection<TSchema>>;
|
||||
renameCollection<TSchema extends Document = Document>(
|
||||
fromCollection: string,
|
||||
toCollection: string,
|
||||
callback: Callback<Collection<TSchema>>
|
||||
): void;
|
||||
renameCollection<TSchema extends Document = Document>(
|
||||
fromCollection: string,
|
||||
toCollection: string,
|
||||
options: RenameOptions
|
||||
): Promise<Collection<TSchema>>;
|
||||
renameCollection<TSchema extends Document = Document>(
|
||||
fromCollection: string,
|
||||
toCollection: string,
|
||||
options: RenameOptions,
|
||||
callback: Callback<Collection<TSchema>>
|
||||
): void;
|
||||
renameCollection<TSchema extends Document = Document>(
|
||||
fromCollection: string,
|
||||
toCollection: string,
|
||||
options?: RenameOptions | Callback<Collection<TSchema>>,
|
||||
callback?: Callback<Collection<TSchema>>
|
||||
): Promise<Collection<TSchema>> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
// Intentionally, we do not inherit options from parent for this operation.
|
||||
options = { ...options, readPreference: ReadPreference.PRIMARY };
|
||||
|
||||
// Add return new collection
|
||||
options.new_collection = true;
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new RenameOperation(
|
||||
this.collection<TSchema>(fromCollection) as TODO_NODE_3286,
|
||||
toCollection,
|
||||
options
|
||||
) as TODO_NODE_3286,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a collection from the database, removing it permanently. New accesses will create a new collection.
|
||||
*
|
||||
* @param name - Name of collection to drop
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
dropCollection(name: string): Promise<boolean>;
|
||||
dropCollection(name: string, callback: Callback<boolean>): void;
|
||||
dropCollection(name: string, options: DropCollectionOptions): Promise<boolean>;
|
||||
dropCollection(name: string, options: DropCollectionOptions, callback: Callback<boolean>): void;
|
||||
dropCollection(
|
||||
name: string,
|
||||
options?: DropCollectionOptions | Callback<boolean>,
|
||||
callback?: Callback<boolean>
|
||||
): Promise<boolean> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new DropCollectionOperation(this, name, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a database, removing it permanently from the server.
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
dropDatabase(): Promise<boolean>;
|
||||
dropDatabase(callback: Callback<boolean>): void;
|
||||
dropDatabase(options: DropDatabaseOptions): Promise<boolean>;
|
||||
dropDatabase(options: DropDatabaseOptions, callback: Callback<boolean>): void;
|
||||
dropDatabase(
|
||||
options?: DropDatabaseOptions | Callback<boolean>,
|
||||
callback?: Callback<boolean>
|
||||
): Promise<boolean> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new DropDatabaseOperation(this, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all collections for the current db.
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
collections(): Promise<Collection[]>;
|
||||
collections(callback: Callback<Collection[]>): void;
|
||||
collections(options: ListCollectionsOptions): Promise<Collection[]>;
|
||||
collections(options: ListCollectionsOptions, callback: Callback<Collection[]>): void;
|
||||
collections(
|
||||
options?: ListCollectionsOptions | Callback<Collection[]>,
|
||||
callback?: Callback<Collection[]>
|
||||
): Promise<Collection[]> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new CollectionsOperation(this, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an index on the db and collection.
|
||||
*
|
||||
* @param name - Name of the collection to create the index on.
|
||||
* @param indexSpec - Specify the field to index, or an index specification
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
createIndex(name: string, indexSpec: IndexSpecification): Promise<string>;
|
||||
createIndex(name: string, indexSpec: IndexSpecification, callback?: Callback<string>): void;
|
||||
createIndex(
|
||||
name: string,
|
||||
indexSpec: IndexSpecification,
|
||||
options: CreateIndexesOptions
|
||||
): Promise<string>;
|
||||
createIndex(
|
||||
name: string,
|
||||
indexSpec: IndexSpecification,
|
||||
options: CreateIndexesOptions,
|
||||
callback: Callback<string>
|
||||
): void;
|
||||
createIndex(
|
||||
name: string,
|
||||
indexSpec: IndexSpecification,
|
||||
options?: CreateIndexesOptions | Callback<string>,
|
||||
callback?: Callback<string>
|
||||
): Promise<string> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new CreateIndexOperation(this, name, indexSpec, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to the database
|
||||
*
|
||||
* @param username - The username for the new user
|
||||
* @param password - An optional password for the new user
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
addUser(username: string): Promise<Document>;
|
||||
addUser(username: string, callback: Callback<Document>): void;
|
||||
addUser(username: string, password: string): Promise<Document>;
|
||||
addUser(username: string, password: string, callback: Callback<Document>): void;
|
||||
addUser(username: string, options: AddUserOptions): Promise<Document>;
|
||||
addUser(username: string, options: AddUserOptions, callback: Callback<Document>): void;
|
||||
addUser(username: string, password: string, options: AddUserOptions): Promise<Document>;
|
||||
addUser(
|
||||
username: string,
|
||||
password: string,
|
||||
options: AddUserOptions,
|
||||
callback: Callback<Document>
|
||||
): void;
|
||||
addUser(
|
||||
username: string,
|
||||
password?: string | AddUserOptions | Callback<Document>,
|
||||
options?: AddUserOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof password === 'function') {
|
||||
(callback = password), (password = undefined), (options = {});
|
||||
} else if (typeof password !== 'string') {
|
||||
if (typeof options === 'function') {
|
||||
(callback = options), (options = password), (password = undefined);
|
||||
} else {
|
||||
(options = password), (callback = undefined), (password = undefined);
|
||||
}
|
||||
} else {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
}
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new AddUserOperation(this, username, password, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from a database
|
||||
*
|
||||
* @param username - The username to remove
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
removeUser(username: string): Promise<boolean>;
|
||||
removeUser(username: string, callback: Callback<boolean>): void;
|
||||
removeUser(username: string, options: RemoveUserOptions): Promise<boolean>;
|
||||
removeUser(username: string, options: RemoveUserOptions, callback: Callback<boolean>): void;
|
||||
removeUser(
|
||||
username: string,
|
||||
options?: RemoveUserOptions | Callback<boolean>,
|
||||
callback?: Callback<boolean>
|
||||
): Promise<boolean> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new RemoveUserOperation(this, username, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current profiling level of MongoDB
|
||||
*
|
||||
* @param level - The new profiling level (off, slow_only, all).
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
setProfilingLevel(level: ProfilingLevel): Promise<ProfilingLevel>;
|
||||
setProfilingLevel(level: ProfilingLevel, callback: Callback<ProfilingLevel>): void;
|
||||
setProfilingLevel(
|
||||
level: ProfilingLevel,
|
||||
options: SetProfilingLevelOptions
|
||||
): Promise<ProfilingLevel>;
|
||||
setProfilingLevel(
|
||||
level: ProfilingLevel,
|
||||
options: SetProfilingLevelOptions,
|
||||
callback: Callback<ProfilingLevel>
|
||||
): void;
|
||||
setProfilingLevel(
|
||||
level: ProfilingLevel,
|
||||
options?: SetProfilingLevelOptions | Callback<ProfilingLevel>,
|
||||
callback?: Callback<ProfilingLevel>
|
||||
): Promise<ProfilingLevel> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new SetProfilingLevelOperation(this, level, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current profiling Level for MongoDB
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
profilingLevel(): Promise<string>;
|
||||
profilingLevel(callback: Callback<string>): void;
|
||||
profilingLevel(options: ProfilingLevelOptions): Promise<string>;
|
||||
profilingLevel(options: ProfilingLevelOptions, callback: Callback<string>): void;
|
||||
profilingLevel(
|
||||
options?: ProfilingLevelOptions | Callback<string>,
|
||||
callback?: Callback<string>
|
||||
): Promise<string> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new ProfilingLevelOperation(this, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves this collections index info.
|
||||
*
|
||||
* @param name - The name of the collection.
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
indexInformation(name: string): Promise<Document>;
|
||||
indexInformation(name: string, callback: Callback<Document>): void;
|
||||
indexInformation(name: string, options: IndexInformationOptions): Promise<Document>;
|
||||
indexInformation(
|
||||
name: string,
|
||||
options: IndexInformationOptions,
|
||||
callback: Callback<Document>
|
||||
): void;
|
||||
indexInformation(
|
||||
name: string,
|
||||
options?: IndexInformationOptions | Callback<Document>,
|
||||
callback?: Callback<Document>
|
||||
): Promise<Document> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
|
||||
return executeOperation(
|
||||
this.s.client,
|
||||
new IndexInformationOperation(this, name, resolveOptions(this, options)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unref all sockets
|
||||
* @deprecated This function is deprecated and will be removed in the next major version.
|
||||
*/
|
||||
unref(): void {
|
||||
getTopology(this).unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Change Stream, watching for new changes (insertions, updates,
|
||||
* replacements, deletions, and invalidations) in this database. Will ignore all
|
||||
* changes to system collections.
|
||||
*
|
||||
* @remarks
|
||||
* watch() accepts two generic arguments for distinct usecases:
|
||||
* - The first is to provide the schema that may be defined for all the collections within this database
|
||||
* - The second is to override the shape of the change stream document entirely, if it is not provided the type will default to ChangeStreamDocument of the first argument
|
||||
*
|
||||
* @param pipeline - An array of {@link https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents.
|
||||
* @param options - Optional settings for the command
|
||||
* @typeParam TSchema - Type of the data being detected by the change stream
|
||||
* @typeParam TChange - Type of the whole change stream document emitted
|
||||
*/
|
||||
watch<
|
||||
TSchema extends Document = Document,
|
||||
TChange extends Document = ChangeStreamDocument<TSchema>
|
||||
>(pipeline: Document[] = [], options: ChangeStreamOptions = {}): ChangeStream<TSchema, TChange> {
|
||||
// Allow optionally not specifying a pipeline
|
||||
if (!Array.isArray(pipeline)) {
|
||||
options = pipeline;
|
||||
pipeline = [];
|
||||
}
|
||||
|
||||
return new ChangeStream<TSchema, TChange>(this, pipeline, resolveOptions(this, options));
|
||||
}
|
||||
|
||||
/** Return the db logger */
|
||||
getLogger(): Logger {
|
||||
return this.s.logger;
|
||||
}
|
||||
|
||||
get logger(): Logger {
|
||||
return this.s.logger;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(NODE-3484): Refactor into MongoDBNamespace
|
||||
// Validate the database name
|
||||
function validateDatabaseName(databaseName: string) {
|
||||
if (typeof databaseName !== 'string')
|
||||
throw new MongoInvalidArgumentError('Database name must be a string');
|
||||
if (databaseName.length === 0)
|
||||
throw new MongoInvalidArgumentError('Database name cannot be the empty string');
|
||||
if (databaseName === '$external') return;
|
||||
|
||||
const invalidChars = [' ', '.', '$', '/', '\\'];
|
||||
for (let i = 0; i < invalidChars.length; i++) {
|
||||
if (databaseName.indexOf(invalidChars[i]) !== -1)
|
||||
throw new MongoAPIError(`database names cannot contain the character '${invalidChars[i]}'`);
|
||||
}
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import type { deserialize, Document, serialize } from './bson';
|
||||
import type { ProxyOptions } from './cmap/connection';
|
||||
import { MongoMissingDependencyError } from './error';
|
||||
import type { MongoClient } from './mongo_client';
|
||||
import { Callback, parsePackageVersion } from './utils';
|
||||
|
||||
export const PKG_VERSION = Symbol('kPkgVersion');
|
||||
|
||||
function makeErrorModule(error: any) {
|
||||
const props = error ? { kModuleError: error } : {};
|
||||
return new Proxy(props, {
|
||||
get: (_: any, key: any) => {
|
||||
if (key === 'kModuleError') {
|
||||
return error;
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
set: () => {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export let Kerberos: typeof import('kerberos') | { kModuleError: MongoMissingDependencyError } =
|
||||
makeErrorModule(
|
||||
new MongoMissingDependencyError(
|
||||
'Optional module `kerberos` not found. Please install it to enable kerberos authentication'
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
// Ensure you always wrap an optional require in the try block NODE-3199
|
||||
Kerberos = require('kerberos');
|
||||
} catch {} // eslint-disable-line
|
||||
|
||||
export interface KerberosClient {
|
||||
step(challenge: string): Promise<string>;
|
||||
step(challenge: string, callback: Callback<string>): void;
|
||||
wrap(challenge: string, options: { user: string }): Promise<string>;
|
||||
wrap(challenge: string, options: { user: string }, callback: Callback<string>): void;
|
||||
unwrap(challenge: string): Promise<string>;
|
||||
unwrap(challenge: string, callback: Callback<string>): void;
|
||||
}
|
||||
|
||||
type ZStandardLib = {
|
||||
/**
|
||||
* Compress using zstd.
|
||||
* @param buf - Buffer to be compressed.
|
||||
*/
|
||||
compress(buf: Buffer, level?: number): Promise<Buffer>;
|
||||
|
||||
/**
|
||||
* Decompress using zstd.
|
||||
*/
|
||||
decompress(buf: Buffer): Promise<Buffer>;
|
||||
};
|
||||
|
||||
export let ZStandard: ZStandardLib | { kModuleError: MongoMissingDependencyError } =
|
||||
makeErrorModule(
|
||||
new MongoMissingDependencyError(
|
||||
'Optional module `@mongodb-js/zstd` not found. Please install it to enable zstd compression'
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
ZStandard = require('@mongodb-js/zstd');
|
||||
} catch {} // eslint-disable-line
|
||||
|
||||
type SnappyLib = {
|
||||
[PKG_VERSION]: { major: number; minor: number; patch: number };
|
||||
|
||||
/**
|
||||
* - Snappy 6.x takes a callback and returns void
|
||||
* - Snappy 7.x returns a promise
|
||||
*
|
||||
* In order to support both we must check the return value of the function
|
||||
* @param buf - Buffer to be compressed
|
||||
* @param callback - ONLY USED IN SNAPPY 6.x
|
||||
*/
|
||||
compress(buf: Buffer): Promise<Buffer>;
|
||||
compress(buf: Buffer, callback: (error?: Error, buffer?: Buffer) => void): void;
|
||||
|
||||
/**
|
||||
* - Snappy 6.x takes a callback and returns void
|
||||
* - Snappy 7.x returns a promise
|
||||
*
|
||||
* In order to support both we must check the return value of the function
|
||||
* @param buf - Buffer to be compressed
|
||||
* @param callback - ONLY USED IN SNAPPY 6.x
|
||||
*/
|
||||
uncompress(buf: Buffer, opt: { asBuffer: true }): Promise<Buffer>;
|
||||
uncompress(
|
||||
buf: Buffer,
|
||||
opt: { asBuffer: true },
|
||||
callback: (error?: Error, buffer?: Buffer) => void
|
||||
): void;
|
||||
};
|
||||
|
||||
export let Snappy: SnappyLib | { kModuleError: MongoMissingDependencyError } = makeErrorModule(
|
||||
new MongoMissingDependencyError(
|
||||
'Optional module `snappy` not found. Please install it to enable snappy compression'
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
// Ensure you always wrap an optional require in the try block NODE-3199
|
||||
Snappy = require('snappy');
|
||||
try {
|
||||
(Snappy as any)[PKG_VERSION] = parsePackageVersion(require('snappy/package.json'));
|
||||
} catch {} // eslint-disable-line
|
||||
} catch {} // eslint-disable-line
|
||||
|
||||
export let saslprep: typeof import('saslprep') | { kModuleError: MongoMissingDependencyError } =
|
||||
makeErrorModule(
|
||||
new MongoMissingDependencyError(
|
||||
'Optional module `saslprep` not found.' +
|
||||
' Please install it to enable Stringprep Profile for User Names and Passwords'
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
// Ensure you always wrap an optional require in the try block NODE-3199
|
||||
saslprep = require('saslprep');
|
||||
} catch {} // eslint-disable-line
|
||||
|
||||
interface AWS4 {
|
||||
/**
|
||||
* Created these inline types to better assert future usage of this API
|
||||
* @param options - options for request
|
||||
* @param credentials - AWS credential details, sessionToken should be omitted entirely if its false-y
|
||||
*/
|
||||
sign(
|
||||
this: void,
|
||||
options: {
|
||||
path: '/';
|
||||
body: string;
|
||||
host: string;
|
||||
method: 'POST';
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded';
|
||||
'Content-Length': number;
|
||||
'X-MongoDB-Server-Nonce': string;
|
||||
'X-MongoDB-GS2-CB-Flag': 'n';
|
||||
};
|
||||
service: string;
|
||||
region: string;
|
||||
},
|
||||
credentials:
|
||||
| {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken: string;
|
||||
}
|
||||
| {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
}
|
||||
| undefined
|
||||
): {
|
||||
headers: {
|
||||
Authorization: string;
|
||||
'X-Amz-Date': string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export let aws4: AWS4 | { kModuleError: MongoMissingDependencyError } = makeErrorModule(
|
||||
new MongoMissingDependencyError(
|
||||
'Optional module `aws4` not found. Please install it to enable AWS authentication'
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
// Ensure you always wrap an optional require in the try block NODE-3199
|
||||
aws4 = require('aws4');
|
||||
} catch {} // eslint-disable-line
|
||||
|
||||
/** @public */
|
||||
export const AutoEncryptionLoggerLevel = Object.freeze({
|
||||
FatalError: 0,
|
||||
Error: 1,
|
||||
Warning: 2,
|
||||
Info: 3,
|
||||
Trace: 4
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type AutoEncryptionLoggerLevel =
|
||||
typeof AutoEncryptionLoggerLevel[keyof typeof AutoEncryptionLoggerLevel];
|
||||
|
||||
/** @public */
|
||||
export interface AutoEncryptionTlsOptions {
|
||||
/**
|
||||
* Specifies the location of a local .pem file that contains
|
||||
* either the client's TLS/SSL certificate and key or only the
|
||||
* client's TLS/SSL key when tlsCertificateFile is used to
|
||||
* provide the certificate.
|
||||
*/
|
||||
tlsCertificateKeyFile?: string;
|
||||
/**
|
||||
* Specifies the password to de-crypt the tlsCertificateKeyFile.
|
||||
*/
|
||||
tlsCertificateKeyFilePassword?: string;
|
||||
/**
|
||||
* Specifies the location of a local .pem file that contains the
|
||||
* root certificate chain from the Certificate Authority.
|
||||
* This file is used to validate the certificate presented by the
|
||||
* KMS provider.
|
||||
*/
|
||||
tlsCAFile?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface AutoEncryptionOptions {
|
||||
/** @internal */
|
||||
bson?: { serialize: typeof serialize; deserialize: typeof deserialize };
|
||||
/** @internal client for metadata lookups */
|
||||
metadataClient?: MongoClient;
|
||||
/** A `MongoClient` used to fetch keys from a key vault */
|
||||
keyVaultClient?: MongoClient;
|
||||
/** The namespace where keys are stored in the key vault */
|
||||
keyVaultNamespace?: string;
|
||||
/** Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. */
|
||||
kmsProviders?: {
|
||||
/** Configuration options for using 'aws' as your KMS provider */
|
||||
aws?: {
|
||||
/** The access key used for the AWS KMS provider */
|
||||
accessKeyId: string;
|
||||
/** The secret access key used for the AWS KMS provider */
|
||||
secretAccessKey: string;
|
||||
/**
|
||||
* An optional AWS session token that will be used as the
|
||||
* X-Amz-Security-Token header for AWS requests.
|
||||
*/
|
||||
sessionToken?: string;
|
||||
};
|
||||
/** Configuration options for using 'local' as your KMS provider */
|
||||
local?: {
|
||||
/**
|
||||
* The master key used to encrypt/decrypt data keys.
|
||||
* A 96-byte long Buffer or base64 encoded string.
|
||||
*/
|
||||
key: Buffer | string;
|
||||
};
|
||||
/** Configuration options for using 'azure' as your KMS provider */
|
||||
azure?: {
|
||||
/** The tenant ID identifies the organization for the account */
|
||||
tenantId: string;
|
||||
/** The client ID to authenticate a registered application */
|
||||
clientId: string;
|
||||
/** The client secret to authenticate a registered application */
|
||||
clientSecret: string;
|
||||
/**
|
||||
* If present, a host with optional port. E.g. "example.com" or "example.com:443".
|
||||
* This is optional, and only needed if customer is using a non-commercial Azure instance
|
||||
* (e.g. a government or China account, which use different URLs).
|
||||
* Defaults to "login.microsoftonline.com"
|
||||
*/
|
||||
identityPlatformEndpoint?: string | undefined;
|
||||
};
|
||||
/** Configuration options for using 'gcp' as your KMS provider */
|
||||
gcp?: {
|
||||
/** The service account email to authenticate */
|
||||
email: string;
|
||||
/** A PKCS#8 encrypted key. This can either be a base64 string or a binary representation */
|
||||
privateKey: string | Buffer;
|
||||
/**
|
||||
* If present, a host with optional port. E.g. "example.com" or "example.com:443".
|
||||
* Defaults to "oauth2.googleapis.com"
|
||||
*/
|
||||
endpoint?: string | undefined;
|
||||
};
|
||||
/**
|
||||
* Configuration options for using 'kmip' as your KMS provider
|
||||
*/
|
||||
kmip?: {
|
||||
/**
|
||||
* The output endpoint string.
|
||||
* The endpoint consists of a hostname and port separated by a colon.
|
||||
* E.g. "example.com:123". A port is always present.
|
||||
*/
|
||||
endpoint?: string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* A map of namespaces to a local JSON schema for encryption
|
||||
*
|
||||
* **NOTE**: Supplying options.schemaMap provides more security than relying on JSON Schemas obtained from the server.
|
||||
* It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending decrypted data that should be encrypted.
|
||||
* Schemas supplied in the schemaMap only apply to configuring automatic encryption for client side encryption.
|
||||
* Other validation rules in the JSON schema will not be enforced by the driver and will result in an error.
|
||||
*/
|
||||
schemaMap?: Document;
|
||||
/** @experimental Public Technical Preview: Supply a schema for the encrypted fields in the document */
|
||||
encryptedFieldsMap?: Document;
|
||||
/** Allows the user to bypass auto encryption, maintaining implicit decryption */
|
||||
bypassAutoEncryption?: boolean;
|
||||
/** @experimental Public Technical Preview: Allows users to bypass query analysis */
|
||||
bypassQueryAnalysis?: boolean;
|
||||
options?: {
|
||||
/** An optional hook to catch logging messages from the underlying encryption engine */
|
||||
logger?: (level: AutoEncryptionLoggerLevel, message: string) => void;
|
||||
};
|
||||
extraOptions?: {
|
||||
/**
|
||||
* A local process the driver communicates with to determine how to encrypt values in a command.
|
||||
* Defaults to "mongodb://%2Fvar%2Fmongocryptd.sock" if domain sockets are available or "mongodb://localhost:27020" otherwise
|
||||
*/
|
||||
mongocryptdURI?: string;
|
||||
/** If true, autoEncryption will not attempt to spawn a mongocryptd before connecting */
|
||||
mongocryptdBypassSpawn?: boolean;
|
||||
/** The path to the mongocryptd executable on the system */
|
||||
mongocryptdSpawnPath?: string;
|
||||
/** Command line arguments to use when auto-spawning a mongocryptd */
|
||||
mongocryptdSpawnArgs?: string[];
|
||||
/**
|
||||
* Full path to a MongoDB Crypt shared library to be used (instead of mongocryptd).
|
||||
*
|
||||
* This needs to be the path to the file itself, not a directory.
|
||||
* It can be an absolute or relative path. If the path is relative and
|
||||
* its first component is `$ORIGIN`, it will be replaced by the directory
|
||||
* containing the mongodb-client-encryption native addon file. Otherwise,
|
||||
* the path will be interpreted relative to the current working directory.
|
||||
*
|
||||
* Currently, loading different MongoDB Crypt shared library files from different
|
||||
* MongoClients in the same process is not supported.
|
||||
*
|
||||
* If this option is provided and no MongoDB Crypt shared library could be loaded
|
||||
* from the specified location, creating the MongoClient will fail.
|
||||
*
|
||||
* If this option is not provided and `cryptSharedLibRequired` is not specified,
|
||||
* the AutoEncrypter will attempt to spawn and/or use mongocryptd according
|
||||
* to the mongocryptd-specific `extraOptions` options.
|
||||
*
|
||||
* Specifying a path prevents mongocryptd from being used as a fallback.
|
||||
*
|
||||
* Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher.
|
||||
*/
|
||||
cryptSharedLibPath?: string;
|
||||
/**
|
||||
* If specified, never use mongocryptd and instead fail when the MongoDB Crypt
|
||||
* shared library could not be loaded.
|
||||
*
|
||||
* This is always true when `cryptSharedLibPath` is specified.
|
||||
*
|
||||
* Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher.
|
||||
*/
|
||||
cryptSharedLibRequired?: boolean;
|
||||
/**
|
||||
* Search paths for a MongoDB Crypt shared library to be used (instead of mongocryptd)
|
||||
* Only for driver testing!
|
||||
* @internal
|
||||
*/
|
||||
cryptSharedLibSearchPaths?: string[];
|
||||
};
|
||||
proxyOptions?: ProxyOptions;
|
||||
/** The TLS options to use connecting to the KMS provider */
|
||||
tlsOptions?: {
|
||||
aws?: AutoEncryptionTlsOptions;
|
||||
local?: AutoEncryptionTlsOptions;
|
||||
azure?: AutoEncryptionTlsOptions;
|
||||
gcp?: AutoEncryptionTlsOptions;
|
||||
kmip?: AutoEncryptionTlsOptions;
|
||||
};
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface AutoEncrypter {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-new
|
||||
new (client: MongoClient, options: AutoEncryptionOptions): AutoEncrypter;
|
||||
init(cb: Callback): void;
|
||||
teardown(force: boolean, callback: Callback): void;
|
||||
encrypt(ns: string, cmd: Document, options: any, callback: Callback<Document>): void;
|
||||
decrypt(cmd: Document, options: any, callback: Callback<Document>): void;
|
||||
/** @experimental */
|
||||
readonly cryptSharedLibVersionInfo: { version: bigint; versionStr: string } | null;
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { deserialize, serialize } from './bson';
|
||||
import { MONGO_CLIENT_EVENTS } from './constants';
|
||||
import type { AutoEncrypter, AutoEncryptionOptions } from './deps';
|
||||
import { MongoInvalidArgumentError, MongoMissingDependencyError } from './error';
|
||||
import { MongoClient, MongoClientOptions } from './mongo_client';
|
||||
import { Callback, getMongoDBClientEncryption } from './utils';
|
||||
|
||||
let AutoEncrypterClass: { new (...args: ConstructorParameters<AutoEncrypter>): AutoEncrypter };
|
||||
|
||||
/** @internal */
|
||||
const kInternalClient = Symbol('internalClient');
|
||||
|
||||
/** @internal */
|
||||
export interface EncrypterOptions {
|
||||
autoEncryption: AutoEncryptionOptions;
|
||||
maxPoolSize?: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class Encrypter {
|
||||
[kInternalClient]: MongoClient | null;
|
||||
bypassAutoEncryption: boolean;
|
||||
needsConnecting: boolean;
|
||||
autoEncrypter: AutoEncrypter;
|
||||
|
||||
constructor(client: MongoClient, uri: string, options: MongoClientOptions) {
|
||||
if (typeof options.autoEncryption !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Option "autoEncryption" must be specified');
|
||||
}
|
||||
// initialize to null, if we call getInternalClient, we may set this it is important to not overwrite those function calls.
|
||||
this[kInternalClient] = null;
|
||||
|
||||
this.bypassAutoEncryption = !!options.autoEncryption.bypassAutoEncryption;
|
||||
this.needsConnecting = false;
|
||||
|
||||
if (options.maxPoolSize === 0 && options.autoEncryption.keyVaultClient == null) {
|
||||
options.autoEncryption.keyVaultClient = client;
|
||||
} else if (options.autoEncryption.keyVaultClient == null) {
|
||||
options.autoEncryption.keyVaultClient = this.getInternalClient(client, uri, options);
|
||||
}
|
||||
|
||||
if (this.bypassAutoEncryption) {
|
||||
options.autoEncryption.metadataClient = undefined;
|
||||
} else if (options.maxPoolSize === 0) {
|
||||
options.autoEncryption.metadataClient = client;
|
||||
} else {
|
||||
options.autoEncryption.metadataClient = this.getInternalClient(client, uri, options);
|
||||
}
|
||||
|
||||
if (options.proxyHost) {
|
||||
options.autoEncryption.proxyOptions = {
|
||||
proxyHost: options.proxyHost,
|
||||
proxyPort: options.proxyPort,
|
||||
proxyUsername: options.proxyUsername,
|
||||
proxyPassword: options.proxyPassword
|
||||
};
|
||||
}
|
||||
|
||||
options.autoEncryption.bson = Object.create(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.autoEncryption.bson!.serialize = serialize;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.autoEncryption.bson!.deserialize = deserialize;
|
||||
|
||||
this.autoEncrypter = new AutoEncrypterClass(client, options.autoEncryption);
|
||||
}
|
||||
|
||||
getInternalClient(client: MongoClient, uri: string, options: MongoClientOptions): MongoClient {
|
||||
// TODO(NODE-4144): Remove new variable for type narrowing
|
||||
let internalClient = this[kInternalClient];
|
||||
if (internalClient == null) {
|
||||
const clonedOptions: MongoClientOptions = {};
|
||||
|
||||
for (const key of [
|
||||
...Object.getOwnPropertyNames(options),
|
||||
...Object.getOwnPropertySymbols(options)
|
||||
] as string[]) {
|
||||
if (['autoEncryption', 'minPoolSize', 'servers', 'caseTranslate', 'dbName'].includes(key))
|
||||
continue;
|
||||
Reflect.set(clonedOptions, key, Reflect.get(options, key));
|
||||
}
|
||||
|
||||
clonedOptions.minPoolSize = 0;
|
||||
|
||||
internalClient = new MongoClient(uri, clonedOptions);
|
||||
this[kInternalClient] = internalClient;
|
||||
|
||||
for (const eventName of MONGO_CLIENT_EVENTS) {
|
||||
for (const listener of client.listeners(eventName)) {
|
||||
internalClient.on(eventName, listener);
|
||||
}
|
||||
}
|
||||
|
||||
client.on('newListener', (eventName, listener) => {
|
||||
internalClient?.on(eventName, listener);
|
||||
});
|
||||
|
||||
this.needsConnecting = true;
|
||||
}
|
||||
return internalClient;
|
||||
}
|
||||
|
||||
connectInternalClient(callback: Callback): void {
|
||||
// TODO(NODE-4144): Remove new variable for type narrowing
|
||||
const internalClient = this[kInternalClient];
|
||||
if (this.needsConnecting && internalClient != null) {
|
||||
this.needsConnecting = false;
|
||||
return internalClient.connect(callback);
|
||||
}
|
||||
|
||||
return callback();
|
||||
}
|
||||
|
||||
close(client: MongoClient, force: boolean, callback: Callback): void {
|
||||
this.autoEncrypter.teardown(!!force, e => {
|
||||
const internalClient = this[kInternalClient];
|
||||
if (internalClient != null && client !== internalClient) {
|
||||
return internalClient.close(force, callback);
|
||||
}
|
||||
callback(e);
|
||||
});
|
||||
}
|
||||
|
||||
static checkForMongoCrypt(): void {
|
||||
const mongodbClientEncryption = getMongoDBClientEncryption();
|
||||
if (mongodbClientEncryption == null) {
|
||||
throw new MongoMissingDependencyError(
|
||||
'Auto-encryption requested, but the module is not installed. ' +
|
||||
'Please add `mongodb-client-encryption` as a dependency of your project'
|
||||
);
|
||||
}
|
||||
AutoEncrypterClass = mongodbClientEncryption.extension(require('../lib/index')).AutoEncrypter;
|
||||
}
|
||||
}
|
||||
+924
@@ -0,0 +1,924 @@
|
||||
import type { Document } from './bson';
|
||||
import type { TopologyVersion } from './sdam/server_description';
|
||||
import type { TopologyDescription } from './sdam/topology_description';
|
||||
|
||||
/** @public */
|
||||
export type AnyError = MongoError | Error;
|
||||
|
||||
/** @internal */
|
||||
const kErrorLabels = Symbol('errorLabels');
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* The legacy error message from the server that indicates the node is not a writable primary
|
||||
* https://github.com/mongodb/specifications/blob/b07c26dc40d04ac20349f989db531c9845fdd755/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#not-writable-primary-and-node-is-recovering
|
||||
*/
|
||||
export const LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE = new RegExp('not master', 'i');
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* The legacy error message from the server that indicates the node is not a primary or secondary
|
||||
* https://github.com/mongodb/specifications/blob/b07c26dc40d04ac20349f989db531c9845fdd755/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#not-writable-primary-and-node-is-recovering
|
||||
*/
|
||||
export const LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE = new RegExp(
|
||||
'not master or secondary',
|
||||
'i'
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* The error message from the server that indicates the node is recovering
|
||||
* https://github.com/mongodb/specifications/blob/b07c26dc40d04ac20349f989db531c9845fdd755/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#not-writable-primary-and-node-is-recovering
|
||||
*/
|
||||
export const NODE_IS_RECOVERING_ERROR_MESSAGE = new RegExp('node is recovering', 'i');
|
||||
|
||||
/** @internal MongoDB Error Codes */
|
||||
export const MONGODB_ERROR_CODES = Object.freeze({
|
||||
HostUnreachable: 6,
|
||||
HostNotFound: 7,
|
||||
NetworkTimeout: 89,
|
||||
ShutdownInProgress: 91,
|
||||
PrimarySteppedDown: 189,
|
||||
ExceededTimeLimit: 262,
|
||||
SocketException: 9001,
|
||||
NotWritablePrimary: 10107,
|
||||
InterruptedAtShutdown: 11600,
|
||||
InterruptedDueToReplStateChange: 11602,
|
||||
NotPrimaryNoSecondaryOk: 13435,
|
||||
NotPrimaryOrSecondary: 13436,
|
||||
StaleShardVersion: 63,
|
||||
StaleEpoch: 150,
|
||||
StaleConfig: 13388,
|
||||
RetryChangeStream: 234,
|
||||
FailedToSatisfyReadPreference: 133,
|
||||
CursorNotFound: 43,
|
||||
LegacyNotPrimary: 10058,
|
||||
WriteConcernFailed: 64,
|
||||
NamespaceNotFound: 26,
|
||||
IllegalOperation: 20,
|
||||
MaxTimeMSExpired: 50,
|
||||
UnknownReplWriteConcern: 79,
|
||||
UnsatisfiableWriteConcern: 100
|
||||
} as const);
|
||||
|
||||
// From spec@https://github.com/mongodb/specifications/blob/f93d78191f3db2898a59013a7ed5650352ef6da8/source/change-streams/change-streams.rst#resumable-error
|
||||
export const GET_MORE_RESUMABLE_CODES = new Set<number>([
|
||||
MONGODB_ERROR_CODES.HostUnreachable,
|
||||
MONGODB_ERROR_CODES.HostNotFound,
|
||||
MONGODB_ERROR_CODES.NetworkTimeout,
|
||||
MONGODB_ERROR_CODES.ShutdownInProgress,
|
||||
MONGODB_ERROR_CODES.PrimarySteppedDown,
|
||||
MONGODB_ERROR_CODES.ExceededTimeLimit,
|
||||
MONGODB_ERROR_CODES.SocketException,
|
||||
MONGODB_ERROR_CODES.NotWritablePrimary,
|
||||
MONGODB_ERROR_CODES.InterruptedAtShutdown,
|
||||
MONGODB_ERROR_CODES.InterruptedDueToReplStateChange,
|
||||
MONGODB_ERROR_CODES.NotPrimaryNoSecondaryOk,
|
||||
MONGODB_ERROR_CODES.NotPrimaryOrSecondary,
|
||||
MONGODB_ERROR_CODES.StaleShardVersion,
|
||||
MONGODB_ERROR_CODES.StaleEpoch,
|
||||
MONGODB_ERROR_CODES.StaleConfig,
|
||||
MONGODB_ERROR_CODES.RetryChangeStream,
|
||||
MONGODB_ERROR_CODES.FailedToSatisfyReadPreference,
|
||||
MONGODB_ERROR_CODES.CursorNotFound
|
||||
]);
|
||||
|
||||
/** @public */
|
||||
export const MongoErrorLabel = Object.freeze({
|
||||
RetryableWriteError: 'RetryableWriteError',
|
||||
TransientTransactionError: 'TransientTransactionError',
|
||||
UnknownTransactionCommitResult: 'UnknownTransactionCommitResult',
|
||||
ResumableChangeStreamError: 'ResumableChangeStreamError',
|
||||
HandshakeError: 'HandshakeError'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type MongoErrorLabel = typeof MongoErrorLabel[keyof typeof MongoErrorLabel];
|
||||
|
||||
/** @public */
|
||||
export interface ErrorDescription extends Document {
|
||||
message?: string;
|
||||
errmsg?: string;
|
||||
$err?: string;
|
||||
errorLabels?: string[];
|
||||
errInfo?: Document;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @category Error
|
||||
*
|
||||
* @privateRemarks
|
||||
* CSFLE has a dependency on this error, it uses the constructor with a string argument
|
||||
*/
|
||||
export class MongoError extends Error {
|
||||
/** @internal */
|
||||
[kErrorLabels]: Set<string>;
|
||||
/**
|
||||
* This is a number in MongoServerError and a string in MongoDriverError
|
||||
* @privateRemarks
|
||||
* Define the type override on the subclasses when we can use the override keyword
|
||||
*/
|
||||
code?: number | string;
|
||||
topologyVersion?: TopologyVersion;
|
||||
|
||||
constructor(message: string | Error) {
|
||||
if (message instanceof Error) {
|
||||
super(message.message);
|
||||
} else {
|
||||
super(message);
|
||||
}
|
||||
this[kErrorLabels] = new Set();
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoError';
|
||||
}
|
||||
|
||||
/** Legacy name for server error responses */
|
||||
get errmsg(): string {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the error to see if it has an error label
|
||||
*
|
||||
* @param label - The error label to check for
|
||||
* @returns returns true if the error has the provided error label
|
||||
*/
|
||||
hasErrorLabel(label: string): boolean {
|
||||
return this[kErrorLabels].has(label);
|
||||
}
|
||||
|
||||
addErrorLabel(label: string): void {
|
||||
this[kErrorLabels].add(label);
|
||||
}
|
||||
|
||||
get errorLabels(): string[] {
|
||||
return Array.from(this[kErrorLabels]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error coming from the mongo server
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoServerError extends MongoError {
|
||||
codeName?: string;
|
||||
writeConcernError?: Document;
|
||||
errInfo?: Document;
|
||||
ok?: number;
|
||||
[key: string]: any;
|
||||
|
||||
constructor(message: ErrorDescription) {
|
||||
super(message.message || message.errmsg || message.$err || 'n/a');
|
||||
if (message.errorLabels) {
|
||||
this[kErrorLabels] = new Set(message.errorLabels);
|
||||
}
|
||||
|
||||
for (const name in message) {
|
||||
if (name !== 'errorLabels' && name !== 'errmsg' && name !== 'message')
|
||||
this[name] = message[name];
|
||||
}
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoServerError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated by the driver
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoDriverError extends MongoError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoDriverError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when the driver API is used incorrectly
|
||||
*
|
||||
* @privateRemarks
|
||||
* Should **never** be directly instantiated
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
|
||||
export class MongoAPIError extends MongoDriverError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoAPIError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when the driver encounters unexpected input
|
||||
* or reaches an unexpected/invalid internal state
|
||||
*
|
||||
* @privateRemarks
|
||||
* Should **never** be directly instantiated.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoRuntimeError extends MongoDriverError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoRuntimeError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when a batch command is re-executed after one of the commands in the batch
|
||||
* has failed
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoBatchReExecutionError extends MongoAPIError {
|
||||
constructor(message = 'This batch has already been executed, create new batch to execute') {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoBatchReExecutionError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when the driver fails to decompress
|
||||
* data received from the server.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoDecompressionError extends MongoRuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoDecompressionError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when the user attempts to operate on a database or collection through a MongoClient
|
||||
* that has not yet successfully called the "connect" method
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoNotConnectedError extends MongoAPIError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoNotConnectedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when the user makes a mistake in the usage of transactions.
|
||||
* (e.g. attempting to commit a transaction with a readPreference other than primary)
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoTransactionError extends MongoAPIError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoTransactionError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when the user attempts to operate
|
||||
* on a session that has expired or has been closed.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoExpiredSessionError extends MongoAPIError {
|
||||
constructor(message = 'Cannot use a session that has ended') {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoExpiredSessionError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A error generated when the user attempts to authenticate
|
||||
* via Kerberos, but fails to connect to the Kerberos client.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoKerberosError extends MongoRuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoKerberosError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A error generated when the user attempts to authenticate
|
||||
* via AWS, but fails
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoAWSError extends MongoRuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoAWSError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when a ChangeStream operation fails to execute.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoChangeStreamError extends MongoRuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoChangeStreamError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when the user calls a function or method not supported on a tailable cursor
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoTailableCursorError extends MongoAPIError {
|
||||
constructor(message = 'Tailable cursor does not support this operation') {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoTailableCursorError';
|
||||
}
|
||||
}
|
||||
|
||||
/** An error generated when a GridFSStream operation fails to execute.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoGridFSStreamError extends MongoRuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoGridFSStreamError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when a malformed or invalid chunk is
|
||||
* encountered when reading from a GridFSStream.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoGridFSChunkError extends MongoRuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoGridFSChunkError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when a **parsable** unexpected response comes from the server.
|
||||
* This is generally an error where the driver in a state expecting a certain behavior to occur in
|
||||
* the next message from MongoDB but it receives something else.
|
||||
* This error **does not** represent an issue with wire message formatting.
|
||||
*
|
||||
* #### Example
|
||||
* When an operation fails, it is the driver's job to retry it. It must perform serverSelection
|
||||
* again to make sure that it attempts the operation against a server in a good state. If server
|
||||
* selection returns a server that does not support retryable operations, this error is used.
|
||||
* This scenario is unlikely as retryable support would also have been determined on the first attempt
|
||||
* but it is possible the state change could report a selectable server that does not support retries.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoUnexpectedServerResponseError extends MongoRuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoUnexpectedServerResponseError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when the user attempts to add options to a cursor that has already been
|
||||
* initialized
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoCursorInUseError extends MongoAPIError {
|
||||
constructor(message = 'Cursor is already initialized') {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoCursorInUseError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when an attempt is made to operate
|
||||
* on a closed/closing server.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoServerClosedError extends MongoAPIError {
|
||||
constructor(message = 'Server is closed') {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoServerClosedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when an attempt is made to read from a cursor that has been exhausted
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoCursorExhaustedError extends MongoAPIError {
|
||||
constructor(message?: string) {
|
||||
super(message || 'Cursor is exhausted');
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoCursorExhaustedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when an attempt is made to operate on a
|
||||
* dropped, or otherwise unavailable, database.
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoTopologyClosedError extends MongoAPIError {
|
||||
constructor(message = 'Topology is closed') {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoTopologyClosedError';
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
const kBeforeHandshake = Symbol('beforeHandshake');
|
||||
export function isNetworkErrorBeforeHandshake(err: MongoNetworkError): boolean {
|
||||
return err[kBeforeHandshake] === true;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface MongoNetworkErrorOptions {
|
||||
/** Indicates the timeout happened before a connection handshake completed */
|
||||
beforeHandshake: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error indicating an issue with the network, including TCP errors and timeouts.
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoNetworkError extends MongoError {
|
||||
/** @internal */
|
||||
[kBeforeHandshake]?: boolean;
|
||||
|
||||
constructor(message: string | Error, options?: MongoNetworkErrorOptions) {
|
||||
super(message);
|
||||
|
||||
if (options && typeof options.beforeHandshake === 'boolean') {
|
||||
this[kBeforeHandshake] = options.beforeHandshake;
|
||||
}
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoNetworkError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error indicating a network timeout occurred
|
||||
* @public
|
||||
* @category Error
|
||||
*
|
||||
* @privateRemarks
|
||||
* CSFLE has a dependency on this error with an instanceof check
|
||||
*/
|
||||
export class MongoNetworkTimeoutError extends MongoNetworkError {
|
||||
constructor(message: string, options?: MongoNetworkErrorOptions) {
|
||||
super(message, options);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoNetworkTimeoutError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error used when attempting to parse a value (like a connection string)
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoParseError extends MongoDriverError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoParseError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when the user supplies malformed or unexpected arguments
|
||||
* or when a required argument or field is not provided.
|
||||
*
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoInvalidArgumentError extends MongoAPIError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoInvalidArgumentError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when a feature that is not enabled or allowed for the current server
|
||||
* configuration is used
|
||||
*
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoCompatibilityError extends MongoAPIError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoCompatibilityError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when the user fails to provide authentication credentials before attempting
|
||||
* to connect to a mongo server instance.
|
||||
*
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoMissingCredentialsError extends MongoAPIError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoMissingCredentialsError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error generated when a required module or dependency is not present in the local environment
|
||||
*
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoMissingDependencyError extends MongoAPIError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoMissingDependencyError';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* An error signifying a general system issue
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoSystemError extends MongoError {
|
||||
/** An optional reason context, such as an error saved during flow of monitoring and selecting servers */
|
||||
reason?: TopologyDescription;
|
||||
|
||||
constructor(message: string, reason: TopologyDescription) {
|
||||
if (reason && reason.error) {
|
||||
super(reason.error.message || reason.error);
|
||||
} else {
|
||||
super(message);
|
||||
}
|
||||
|
||||
if (reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
this.code = reason.error?.code;
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoSystemError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error signifying a client-side server selection error
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoServerSelectionError extends MongoSystemError {
|
||||
constructor(message: string, reason: TopologyDescription) {
|
||||
super(message, reason);
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoServerSelectionError';
|
||||
}
|
||||
}
|
||||
|
||||
function makeWriteConcernResultObject(input: any) {
|
||||
const output = Object.assign({}, input);
|
||||
|
||||
if (output.ok === 0) {
|
||||
output.ok = 1;
|
||||
delete output.errmsg;
|
||||
delete output.code;
|
||||
delete output.codeName;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when the server reports a writeConcernError
|
||||
* @public
|
||||
* @category Error
|
||||
*/
|
||||
export class MongoWriteConcernError extends MongoServerError {
|
||||
/** The result document (provided if ok: 1) */
|
||||
result?: Document;
|
||||
|
||||
constructor(message: ErrorDescription, result?: Document) {
|
||||
if (result && Array.isArray(result.errorLabels)) {
|
||||
message.errorLabels = result.errorLabels;
|
||||
}
|
||||
|
||||
super(message);
|
||||
this.errInfo = message.errInfo;
|
||||
|
||||
if (result != null) {
|
||||
this.result = makeWriteConcernResultObject(result);
|
||||
}
|
||||
}
|
||||
|
||||
override get name(): string {
|
||||
return 'MongoWriteConcernError';
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/mongodb/specifications/blob/master/source/retryable-reads/retryable-reads.rst#retryable-error
|
||||
const RETRYABLE_READ_ERROR_CODES = new Set<number>([
|
||||
MONGODB_ERROR_CODES.HostUnreachable,
|
||||
MONGODB_ERROR_CODES.HostNotFound,
|
||||
MONGODB_ERROR_CODES.NetworkTimeout,
|
||||
MONGODB_ERROR_CODES.ShutdownInProgress,
|
||||
MONGODB_ERROR_CODES.PrimarySteppedDown,
|
||||
MONGODB_ERROR_CODES.SocketException,
|
||||
MONGODB_ERROR_CODES.NotWritablePrimary,
|
||||
MONGODB_ERROR_CODES.InterruptedAtShutdown,
|
||||
MONGODB_ERROR_CODES.InterruptedDueToReplStateChange,
|
||||
MONGODB_ERROR_CODES.NotPrimaryNoSecondaryOk,
|
||||
MONGODB_ERROR_CODES.NotPrimaryOrSecondary
|
||||
]);
|
||||
|
||||
// see: https://github.com/mongodb/specifications/blob/master/source/retryable-writes/retryable-writes.rst#terms
|
||||
const RETRYABLE_WRITE_ERROR_CODES = new Set<number>([
|
||||
...RETRYABLE_READ_ERROR_CODES,
|
||||
MONGODB_ERROR_CODES.ExceededTimeLimit
|
||||
]);
|
||||
|
||||
export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): boolean {
|
||||
// pre-4.4 server, then the driver adds an error label for every valid case
|
||||
// execute operation will only inspect the label, code/message logic is handled here
|
||||
if (error instanceof MongoNetworkError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error instanceof MongoError) {
|
||||
if (
|
||||
(maxWireVersion >= 9 || error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) &&
|
||||
!error.hasErrorLabel(MongoErrorLabel.HandshakeError)
|
||||
) {
|
||||
// If we already have the error label no need to add it again. 4.4+ servers add the label.
|
||||
// In the case where we have a handshake error, need to fall down to the logic checking
|
||||
// the codes.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof MongoWriteConcernError) {
|
||||
return RETRYABLE_WRITE_ERROR_CODES.has(error.result?.code ?? error.code ?? 0);
|
||||
}
|
||||
|
||||
if (error instanceof MongoError && typeof error.code === 'number') {
|
||||
return RETRYABLE_WRITE_ERROR_CODES.has(error.code);
|
||||
}
|
||||
|
||||
const isNotWritablePrimaryError = LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE.test(error.message);
|
||||
if (isNotWritablePrimaryError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isNodeIsRecoveringError = NODE_IS_RECOVERING_ERROR_MESSAGE.test(error.message);
|
||||
if (isNodeIsRecoveringError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isRetryableWriteError(error: MongoError): boolean {
|
||||
return error.hasErrorLabel(MongoErrorLabel.RetryableWriteError);
|
||||
}
|
||||
|
||||
/** Determines whether an error is something the driver should attempt to retry */
|
||||
export function isRetryableReadError(error: MongoError): boolean {
|
||||
const hasRetryableErrorCode =
|
||||
typeof error.code === 'number' ? RETRYABLE_READ_ERROR_CODES.has(error.code) : false;
|
||||
if (hasRetryableErrorCode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error instanceof MongoNetworkError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isNotWritablePrimaryError = LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE.test(error.message);
|
||||
if (isNotWritablePrimaryError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isNodeIsRecoveringError = NODE_IS_RECOVERING_ERROR_MESSAGE.test(error.message);
|
||||
if (isNodeIsRecoveringError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const SDAM_RECOVERING_CODES = new Set<number>([
|
||||
MONGODB_ERROR_CODES.ShutdownInProgress,
|
||||
MONGODB_ERROR_CODES.PrimarySteppedDown,
|
||||
MONGODB_ERROR_CODES.InterruptedAtShutdown,
|
||||
MONGODB_ERROR_CODES.InterruptedDueToReplStateChange,
|
||||
MONGODB_ERROR_CODES.NotPrimaryOrSecondary
|
||||
]);
|
||||
|
||||
const SDAM_NOT_PRIMARY_CODES = new Set<number>([
|
||||
MONGODB_ERROR_CODES.NotWritablePrimary,
|
||||
MONGODB_ERROR_CODES.NotPrimaryNoSecondaryOk,
|
||||
MONGODB_ERROR_CODES.LegacyNotPrimary
|
||||
]);
|
||||
|
||||
const SDAM_NODE_SHUTTING_DOWN_ERROR_CODES = new Set<number>([
|
||||
MONGODB_ERROR_CODES.InterruptedAtShutdown,
|
||||
MONGODB_ERROR_CODES.ShutdownInProgress
|
||||
]);
|
||||
|
||||
function isRecoveringError(err: MongoError) {
|
||||
if (typeof err.code === 'number') {
|
||||
// If any error code exists, we ignore the error.message
|
||||
return SDAM_RECOVERING_CODES.has(err.code);
|
||||
}
|
||||
|
||||
return (
|
||||
LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE.test(err.message) ||
|
||||
NODE_IS_RECOVERING_ERROR_MESSAGE.test(err.message)
|
||||
);
|
||||
}
|
||||
|
||||
function isNotWritablePrimaryError(err: MongoError) {
|
||||
if (typeof err.code === 'number') {
|
||||
// If any error code exists, we ignore the error.message
|
||||
return SDAM_NOT_PRIMARY_CODES.has(err.code);
|
||||
}
|
||||
|
||||
if (isRecoveringError(err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE.test(err.message);
|
||||
}
|
||||
|
||||
export function isNodeShuttingDownError(err: MongoError): boolean {
|
||||
return !!(typeof err.code === 'number' && SDAM_NODE_SHUTTING_DOWN_ERROR_CODES.has(err.code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether SDAM can recover from a given error. If it cannot
|
||||
* then the pool will be cleared, and server state will completely reset
|
||||
* locally.
|
||||
*
|
||||
* @see https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#not-master-and-node-is-recovering
|
||||
*/
|
||||
export function isSDAMUnrecoverableError(error: MongoError): boolean {
|
||||
// NOTE: null check is here for a strictly pre-CMAP world, a timeout or
|
||||
// close event are considered unrecoverable
|
||||
if (error instanceof MongoParseError || error == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isRecoveringError(error) || isNotWritablePrimaryError(error);
|
||||
}
|
||||
|
||||
export function isNetworkTimeoutError(err: MongoError): err is MongoNetworkError {
|
||||
return !!(err instanceof MongoNetworkError && err.message.match(/timed out/));
|
||||
}
|
||||
|
||||
export function isResumableError(error?: Error, wireVersion?: number): boolean {
|
||||
if (error == null || !(error instanceof MongoError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (error instanceof MongoNetworkError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wireVersion != null && wireVersion >= 9) {
|
||||
// DRIVERS-1308: For 4.4 drivers running against 4.4 servers, drivers will add a special case to treat the CursorNotFound error code as resumable
|
||||
if (error.code === MONGODB_ERROR_CODES.CursorNotFound) {
|
||||
return true;
|
||||
}
|
||||
return error.hasErrorLabel(MongoErrorLabel.ResumableChangeStreamError);
|
||||
}
|
||||
|
||||
if (typeof error.code === 'number') {
|
||||
return GET_MORE_RESUMABLE_CODES.has(error.code);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { MongoInvalidArgumentError } from './error';
|
||||
|
||||
/** @public */
|
||||
export const ExplainVerbosity = Object.freeze({
|
||||
queryPlanner: 'queryPlanner',
|
||||
queryPlannerExtended: 'queryPlannerExtended',
|
||||
executionStats: 'executionStats',
|
||||
allPlansExecution: 'allPlansExecution'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type ExplainVerbosity = string;
|
||||
|
||||
/**
|
||||
* For backwards compatibility, true is interpreted as "allPlansExecution"
|
||||
* and false as "queryPlanner". Prior to server version 3.6, aggregate()
|
||||
* ignores the verbosity parameter and executes in "queryPlanner".
|
||||
* @public
|
||||
*/
|
||||
export type ExplainVerbosityLike = ExplainVerbosity | boolean;
|
||||
|
||||
/** @public */
|
||||
export interface ExplainOptions {
|
||||
/** Specifies the verbosity mode for the explain output. */
|
||||
explain?: ExplainVerbosityLike;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class Explain {
|
||||
verbosity: ExplainVerbosity;
|
||||
|
||||
constructor(verbosity: ExplainVerbosityLike) {
|
||||
if (typeof verbosity === 'boolean') {
|
||||
this.verbosity = verbosity
|
||||
? ExplainVerbosity.allPlansExecution
|
||||
: ExplainVerbosity.queryPlanner;
|
||||
} else {
|
||||
this.verbosity = verbosity;
|
||||
}
|
||||
}
|
||||
|
||||
static fromOptions(options?: ExplainOptions): Explain | undefined {
|
||||
if (options?.explain == null) return;
|
||||
|
||||
const explain = options.explain;
|
||||
if (typeof explain === 'boolean' || typeof explain === 'string') {
|
||||
return new Explain(explain);
|
||||
}
|
||||
|
||||
throw new MongoInvalidArgumentError('Field "explain" must be a string or a boolean');
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import type { Document, ObjectId } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { FindCursor } from '../cursor/find_cursor';
|
||||
import {
|
||||
MongoGridFSChunkError,
|
||||
MongoGridFSStreamError,
|
||||
MongoInvalidArgumentError,
|
||||
MongoRuntimeError
|
||||
} from '../error';
|
||||
import type { FindOptions } from '../operations/find';
|
||||
import type { ReadPreference } from '../read_preference';
|
||||
import type { Sort } from '../sort';
|
||||
import type { Callback } from '../utils';
|
||||
import type { GridFSChunk } from './upload';
|
||||
|
||||
/** @public */
|
||||
export interface GridFSBucketReadStreamOptions {
|
||||
sort?: Sort;
|
||||
skip?: number;
|
||||
/** 0-based offset in bytes to start streaming from */
|
||||
start?: number;
|
||||
/** 0-based offset in bytes to stop streaming before */
|
||||
end?: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface GridFSBucketReadStreamOptionsWithRevision extends GridFSBucketReadStreamOptions {
|
||||
/** The revision number relative to the oldest file with the given filename. 0
|
||||
* gets you the oldest file, 1 gets you the 2nd oldest, -1 gets you the
|
||||
* newest. */
|
||||
revision?: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface GridFSFile {
|
||||
_id: ObjectId;
|
||||
length: number;
|
||||
chunkSize: number;
|
||||
filename: string;
|
||||
contentType?: string;
|
||||
aliases?: string[];
|
||||
metadata?: Document;
|
||||
uploadDate: Date;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface GridFSBucketReadStreamPrivate {
|
||||
bytesRead: number;
|
||||
bytesToTrim: number;
|
||||
bytesToSkip: number;
|
||||
chunks: Collection<GridFSChunk>;
|
||||
cursor?: FindCursor<GridFSChunk>;
|
||||
expected: number;
|
||||
files: Collection<GridFSFile>;
|
||||
filter: Document;
|
||||
init: boolean;
|
||||
expectedEnd: number;
|
||||
file?: GridFSFile;
|
||||
options: {
|
||||
sort?: Sort;
|
||||
skip?: number;
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
readPreference?: ReadPreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* A readable stream that enables you to read buffers from GridFS.
|
||||
*
|
||||
* Do not instantiate this class directly. Use `openDownloadStream()` instead.
|
||||
* @public
|
||||
*/
|
||||
export class GridFSBucketReadStream extends Readable implements NodeJS.ReadableStream {
|
||||
/** @internal */
|
||||
s: GridFSBucketReadStreamPrivate;
|
||||
|
||||
/**
|
||||
* An error occurred
|
||||
* @event
|
||||
*/
|
||||
static readonly ERROR = 'error' as const;
|
||||
/**
|
||||
* Fires when the stream loaded the file document corresponding to the provided id.
|
||||
* @event
|
||||
*/
|
||||
static readonly FILE = 'file' as const;
|
||||
/**
|
||||
* Emitted when a chunk of data is available to be consumed.
|
||||
* @event
|
||||
*/
|
||||
static readonly DATA = 'data' as const;
|
||||
/**
|
||||
* Fired when the stream is exhausted (no more data events).
|
||||
* @event
|
||||
*/
|
||||
static readonly END = 'end' as const;
|
||||
/**
|
||||
* Fired when the stream is exhausted and the underlying cursor is killed
|
||||
* @event
|
||||
*/
|
||||
static readonly CLOSE = 'close' as const;
|
||||
|
||||
/** @internal
|
||||
* @param chunks - Handle for chunks collection
|
||||
* @param files - Handle for files collection
|
||||
* @param readPreference - The read preference to use
|
||||
* @param filter - The filter to use to find the file document
|
||||
*/
|
||||
constructor(
|
||||
chunks: Collection<GridFSChunk>,
|
||||
files: Collection<GridFSFile>,
|
||||
readPreference: ReadPreference | undefined,
|
||||
filter: Document,
|
||||
options?: GridFSBucketReadStreamOptions
|
||||
) {
|
||||
super();
|
||||
this.s = {
|
||||
bytesToTrim: 0,
|
||||
bytesToSkip: 0,
|
||||
bytesRead: 0,
|
||||
chunks,
|
||||
expected: 0,
|
||||
files,
|
||||
filter,
|
||||
init: false,
|
||||
expectedEnd: 0,
|
||||
options: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
...options
|
||||
},
|
||||
readPreference
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from the cursor and pushes to the stream.
|
||||
* Private Impl, do not call directly
|
||||
* @internal
|
||||
*/
|
||||
override _read(): void {
|
||||
if (this.destroyed) return;
|
||||
waitForFile(this, () => doRead(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 0-based offset in bytes to start streaming from. Throws
|
||||
* an error if this stream has entered flowing mode
|
||||
* (e.g. if you've already called `on('data')`)
|
||||
*
|
||||
* @param start - 0-based offset in bytes to start streaming from
|
||||
*/
|
||||
start(start = 0): this {
|
||||
throwIfInitialized(this);
|
||||
this.s.options.start = start;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 0-based offset in bytes to start streaming from. Throws
|
||||
* an error if this stream has entered flowing mode
|
||||
* (e.g. if you've already called `on('data')`)
|
||||
*
|
||||
* @param end - Offset in bytes to stop reading at
|
||||
*/
|
||||
end(end = 0): this {
|
||||
throwIfInitialized(this);
|
||||
this.s.options.end = end;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this stream as aborted (will never push another `data` event)
|
||||
* and kills the underlying cursor. Will emit the 'end' event, and then
|
||||
* the 'close' event once the cursor is successfully killed.
|
||||
*
|
||||
* @param callback - called when the cursor is successfully closed or an error occurred.
|
||||
*/
|
||||
abort(callback?: Callback<void>): void {
|
||||
this.push(null);
|
||||
this.destroyed = true;
|
||||
if (this.s.cursor) {
|
||||
this.s.cursor.close(error => {
|
||||
this.emit(GridFSBucketReadStream.CLOSE);
|
||||
callback && callback(error);
|
||||
});
|
||||
} else {
|
||||
if (!this.s.init) {
|
||||
// If not initialized, fire close event because we will never
|
||||
// get a cursor
|
||||
this.emit(GridFSBucketReadStream.CLOSE);
|
||||
}
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function throwIfInitialized(stream: GridFSBucketReadStream): void {
|
||||
if (stream.s.init) {
|
||||
throw new MongoGridFSStreamError('Options cannot be changed after the stream is initialized');
|
||||
}
|
||||
}
|
||||
|
||||
function doRead(stream: GridFSBucketReadStream): void {
|
||||
if (stream.destroyed) return;
|
||||
if (!stream.s.cursor) return;
|
||||
if (!stream.s.file) return;
|
||||
|
||||
stream.s.cursor.next((error, doc) => {
|
||||
if (stream.destroyed) {
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
stream.emit(GridFSBucketReadStream.ERROR, error);
|
||||
return;
|
||||
}
|
||||
if (!doc) {
|
||||
stream.push(null);
|
||||
|
||||
process.nextTick(() => {
|
||||
if (!stream.s.cursor) return;
|
||||
stream.s.cursor.close(error => {
|
||||
if (error) {
|
||||
stream.emit(GridFSBucketReadStream.ERROR, error);
|
||||
return;
|
||||
}
|
||||
|
||||
stream.emit(GridFSBucketReadStream.CLOSE);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stream.s.file) return;
|
||||
|
||||
const bytesRemaining = stream.s.file.length - stream.s.bytesRead;
|
||||
const expectedN = stream.s.expected++;
|
||||
const expectedLength = Math.min(stream.s.file.chunkSize, bytesRemaining);
|
||||
if (doc.n > expectedN) {
|
||||
return stream.emit(
|
||||
GridFSBucketReadStream.ERROR,
|
||||
new MongoGridFSChunkError(
|
||||
`ChunkIsMissing: Got unexpected n: ${doc.n}, expected: ${expectedN}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (doc.n < expectedN) {
|
||||
return stream.emit(
|
||||
GridFSBucketReadStream.ERROR,
|
||||
new MongoGridFSChunkError(`ExtraChunk: Got unexpected n: ${doc.n}, expected: ${expectedN}`)
|
||||
);
|
||||
}
|
||||
|
||||
let buf = Buffer.isBuffer(doc.data) ? doc.data : doc.data.buffer;
|
||||
|
||||
if (buf.byteLength !== expectedLength) {
|
||||
if (bytesRemaining <= 0) {
|
||||
return stream.emit(
|
||||
GridFSBucketReadStream.ERROR,
|
||||
new MongoGridFSChunkError(
|
||||
`ExtraChunk: Got unexpected n: ${doc.n}, expected file length ${stream.s.file.length} bytes but already read ${stream.s.bytesRead} bytes`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return stream.emit(
|
||||
GridFSBucketReadStream.ERROR,
|
||||
new MongoGridFSChunkError(
|
||||
`ChunkIsWrongSize: Got unexpected length: ${buf.byteLength}, expected: ${expectedLength}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
stream.s.bytesRead += buf.byteLength;
|
||||
|
||||
if (buf.byteLength === 0) {
|
||||
return stream.push(null);
|
||||
}
|
||||
|
||||
let sliceStart = null;
|
||||
let sliceEnd = null;
|
||||
|
||||
if (stream.s.bytesToSkip != null) {
|
||||
sliceStart = stream.s.bytesToSkip;
|
||||
stream.s.bytesToSkip = 0;
|
||||
}
|
||||
|
||||
const atEndOfStream = expectedN === stream.s.expectedEnd - 1;
|
||||
const bytesLeftToRead = stream.s.options.end - stream.s.bytesToSkip;
|
||||
if (atEndOfStream && stream.s.bytesToTrim != null) {
|
||||
sliceEnd = stream.s.file.chunkSize - stream.s.bytesToTrim;
|
||||
} else if (stream.s.options.end && bytesLeftToRead < doc.data.byteLength) {
|
||||
sliceEnd = bytesLeftToRead;
|
||||
}
|
||||
|
||||
if (sliceStart != null || sliceEnd != null) {
|
||||
buf = buf.slice(sliceStart || 0, sliceEnd || buf.byteLength);
|
||||
}
|
||||
|
||||
stream.push(buf);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
function init(stream: GridFSBucketReadStream): void {
|
||||
const findOneOptions: FindOptions = {};
|
||||
if (stream.s.readPreference) {
|
||||
findOneOptions.readPreference = stream.s.readPreference;
|
||||
}
|
||||
if (stream.s.options && stream.s.options.sort) {
|
||||
findOneOptions.sort = stream.s.options.sort;
|
||||
}
|
||||
if (stream.s.options && stream.s.options.skip) {
|
||||
findOneOptions.skip = stream.s.options.skip;
|
||||
}
|
||||
|
||||
stream.s.files.findOne(stream.s.filter, findOneOptions, (error, doc) => {
|
||||
if (error) {
|
||||
return stream.emit(GridFSBucketReadStream.ERROR, error);
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
const identifier = stream.s.filter._id
|
||||
? stream.s.filter._id.toString()
|
||||
: stream.s.filter.filename;
|
||||
const errmsg = `FileNotFound: file ${identifier} was not found`;
|
||||
// TODO(NODE-3483)
|
||||
const err = new MongoRuntimeError(errmsg);
|
||||
err.code = 'ENOENT'; // TODO: NODE-3338 set property as part of constructor
|
||||
return stream.emit(GridFSBucketReadStream.ERROR, err);
|
||||
}
|
||||
|
||||
// If document is empty, kill the stream immediately and don't
|
||||
// execute any reads
|
||||
if (doc.length <= 0) {
|
||||
stream.push(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.destroyed) {
|
||||
// If user destroys the stream before we have a cursor, wait
|
||||
// until the query is done to say we're 'closed' because we can't
|
||||
// cancel a query.
|
||||
stream.emit(GridFSBucketReadStream.CLOSE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
stream.s.bytesToSkip = handleStartOption(stream, doc, stream.s.options);
|
||||
} catch (error) {
|
||||
return stream.emit(GridFSBucketReadStream.ERROR, error);
|
||||
}
|
||||
|
||||
const filter: Document = { files_id: doc._id };
|
||||
|
||||
// Currently (MongoDB 3.4.4) skip function does not support the index,
|
||||
// it needs to retrieve all the documents first and then skip them. (CS-25811)
|
||||
// As work around we use $gte on the "n" field.
|
||||
if (stream.s.options && stream.s.options.start != null) {
|
||||
const skip = Math.floor(stream.s.options.start / doc.chunkSize);
|
||||
if (skip > 0) {
|
||||
filter['n'] = { $gte: skip };
|
||||
}
|
||||
}
|
||||
stream.s.cursor = stream.s.chunks.find(filter).sort({ n: 1 });
|
||||
|
||||
if (stream.s.readPreference) {
|
||||
stream.s.cursor.withReadPreference(stream.s.readPreference);
|
||||
}
|
||||
|
||||
stream.s.expectedEnd = Math.ceil(doc.length / doc.chunkSize);
|
||||
stream.s.file = doc as GridFSFile;
|
||||
|
||||
try {
|
||||
stream.s.bytesToTrim = handleEndOption(stream, doc, stream.s.cursor, stream.s.options);
|
||||
} catch (error) {
|
||||
return stream.emit(GridFSBucketReadStream.ERROR, error);
|
||||
}
|
||||
|
||||
stream.emit(GridFSBucketReadStream.FILE, doc);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
function waitForFile(stream: GridFSBucketReadStream, callback: Callback): void {
|
||||
if (stream.s.file) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!stream.s.init) {
|
||||
init(stream);
|
||||
stream.s.init = true;
|
||||
}
|
||||
|
||||
stream.once('file', () => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function handleStartOption(
|
||||
stream: GridFSBucketReadStream,
|
||||
doc: Document,
|
||||
options: GridFSBucketReadStreamOptions
|
||||
): number {
|
||||
if (options && options.start != null) {
|
||||
if (options.start > doc.length) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Stream start (${options.start}) must not be more than the length of the file (${doc.length})`
|
||||
);
|
||||
}
|
||||
if (options.start < 0) {
|
||||
throw new MongoInvalidArgumentError(`Stream start (${options.start}) must not be negative`);
|
||||
}
|
||||
if (options.end != null && options.end < options.start) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Stream start (${options.start}) must not be greater than stream end (${options.end})`
|
||||
);
|
||||
}
|
||||
|
||||
stream.s.bytesRead = Math.floor(options.start / doc.chunkSize) * doc.chunkSize;
|
||||
stream.s.expected = Math.floor(options.start / doc.chunkSize);
|
||||
|
||||
return options.start - stream.s.bytesRead;
|
||||
}
|
||||
throw new MongoInvalidArgumentError('Start option must be defined');
|
||||
}
|
||||
|
||||
function handleEndOption(
|
||||
stream: GridFSBucketReadStream,
|
||||
doc: Document,
|
||||
cursor: FindCursor<GridFSChunk>,
|
||||
options: GridFSBucketReadStreamOptions
|
||||
) {
|
||||
if (options && options.end != null) {
|
||||
if (options.end > doc.length) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Stream end (${options.end}) must not be more than the length of the file (${doc.length})`
|
||||
);
|
||||
}
|
||||
if (options.start == null || options.start < 0) {
|
||||
throw new MongoInvalidArgumentError(`Stream end (${options.end}) must not be negative`);
|
||||
}
|
||||
|
||||
const start = options.start != null ? Math.floor(options.start / doc.chunkSize) : 0;
|
||||
|
||||
cursor.limit(Math.ceil(options.end / doc.chunkSize) - start);
|
||||
|
||||
stream.s.expectedEnd = Math.ceil(options.end / doc.chunkSize);
|
||||
|
||||
return Math.ceil(options.end / doc.chunkSize) * doc.chunkSize - options.end;
|
||||
}
|
||||
throw new MongoInvalidArgumentError('End option must be defined');
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
import type { ObjectId } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { FindCursor } from '../cursor/find_cursor';
|
||||
import type { Db } from '../db';
|
||||
import { MongoRuntimeError } from '../error';
|
||||
import type { Logger } from '../logger';
|
||||
import { Filter, TypedEventEmitter } from '../mongo_types';
|
||||
import type { ReadPreference } from '../read_preference';
|
||||
import type { Sort } from '../sort';
|
||||
import { Callback, maybePromise } from '../utils';
|
||||
import { WriteConcern, WriteConcernOptions } from '../write_concern';
|
||||
import type { FindOptions } from './../operations/find';
|
||||
import {
|
||||
GridFSBucketReadStream,
|
||||
GridFSBucketReadStreamOptions,
|
||||
GridFSBucketReadStreamOptionsWithRevision,
|
||||
GridFSFile
|
||||
} from './download';
|
||||
import { GridFSBucketWriteStream, GridFSBucketWriteStreamOptions, GridFSChunk } from './upload';
|
||||
|
||||
const DEFAULT_GRIDFS_BUCKET_OPTIONS: {
|
||||
bucketName: string;
|
||||
chunkSizeBytes: number;
|
||||
} = {
|
||||
bucketName: 'fs',
|
||||
chunkSizeBytes: 255 * 1024
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface GridFSBucketOptions extends WriteConcernOptions {
|
||||
/** The 'files' and 'chunks' collections will be prefixed with the bucket name followed by a dot. */
|
||||
bucketName?: string;
|
||||
/** Number of bytes stored in each chunk. Defaults to 255KB */
|
||||
chunkSizeBytes?: number;
|
||||
/** Read preference to be passed to read operations */
|
||||
readPreference?: ReadPreference;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface GridFSBucketPrivate {
|
||||
db: Db;
|
||||
options: {
|
||||
bucketName: string;
|
||||
chunkSizeBytes: number;
|
||||
readPreference?: ReadPreference;
|
||||
writeConcern: WriteConcern | undefined;
|
||||
};
|
||||
_chunksCollection: Collection<GridFSChunk>;
|
||||
_filesCollection: Collection<GridFSFile>;
|
||||
checkedIndexes: boolean;
|
||||
calledOpenUploadStream: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type GridFSBucketEvents = {
|
||||
index(): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for a streaming GridFS interface
|
||||
* @public
|
||||
*/
|
||||
export class GridFSBucket extends TypedEventEmitter<GridFSBucketEvents> {
|
||||
/** @internal */
|
||||
s: GridFSBucketPrivate;
|
||||
|
||||
/**
|
||||
* When the first call to openUploadStream is made, the upload stream will
|
||||
* check to see if it needs to create the proper indexes on the chunks and
|
||||
* files collections. This event is fired either when 1) it determines that
|
||||
* no index creation is necessary, 2) when it successfully creates the
|
||||
* necessary indexes.
|
||||
* @event
|
||||
*/
|
||||
static readonly INDEX = 'index' as const;
|
||||
|
||||
constructor(db: Db, options?: GridFSBucketOptions) {
|
||||
super();
|
||||
this.setMaxListeners(0);
|
||||
const privateOptions = {
|
||||
...DEFAULT_GRIDFS_BUCKET_OPTIONS,
|
||||
...options,
|
||||
writeConcern: WriteConcern.fromOptions(options)
|
||||
};
|
||||
this.s = {
|
||||
db,
|
||||
options: privateOptions,
|
||||
_chunksCollection: db.collection<GridFSChunk>(privateOptions.bucketName + '.chunks'),
|
||||
_filesCollection: db.collection<GridFSFile>(privateOptions.bucketName + '.files'),
|
||||
checkedIndexes: false,
|
||||
calledOpenUploadStream: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a writable stream (GridFSBucketWriteStream) for writing
|
||||
* buffers to GridFS. The stream's 'id' property contains the resulting
|
||||
* file's id.
|
||||
*
|
||||
* @param filename - The value of the 'filename' key in the files doc
|
||||
* @param options - Optional settings.
|
||||
*/
|
||||
|
||||
openUploadStream(
|
||||
filename: string,
|
||||
options?: GridFSBucketWriteStreamOptions
|
||||
): GridFSBucketWriteStream {
|
||||
return new GridFSBucketWriteStream(this, filename, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a writable stream (GridFSBucketWriteStream) for writing
|
||||
* buffers to GridFS for a custom file id. The stream's 'id' property contains the resulting
|
||||
* file's id.
|
||||
*/
|
||||
openUploadStreamWithId(
|
||||
id: ObjectId,
|
||||
filename: string,
|
||||
options?: GridFSBucketWriteStreamOptions
|
||||
): GridFSBucketWriteStream {
|
||||
return new GridFSBucketWriteStream(this, filename, { ...options, id });
|
||||
}
|
||||
|
||||
/** Returns a readable stream (GridFSBucketReadStream) for streaming file data from GridFS. */
|
||||
openDownloadStream(
|
||||
id: ObjectId,
|
||||
options?: GridFSBucketReadStreamOptions
|
||||
): GridFSBucketReadStream {
|
||||
return new GridFSBucketReadStream(
|
||||
this.s._chunksCollection,
|
||||
this.s._filesCollection,
|
||||
this.s.options.readPreference,
|
||||
{ _id: id },
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file with the given id
|
||||
*
|
||||
* @param id - The id of the file doc
|
||||
*/
|
||||
delete(id: ObjectId): Promise<void>;
|
||||
delete(id: ObjectId, callback: Callback<void>): void;
|
||||
delete(id: ObjectId, callback?: Callback<void>): Promise<void> | void {
|
||||
return maybePromise(callback, callback => {
|
||||
return this.s._filesCollection.deleteOne({ _id: id }, (error, res) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return this.s._chunksCollection.deleteMany({ files_id: id }, error => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// Delete orphaned chunks before returning FileNotFound
|
||||
if (!res?.deletedCount) {
|
||||
// TODO(NODE-3483): Replace with more appropriate error
|
||||
// Consider creating new error MongoGridFSFileNotFoundError
|
||||
return callback(new MongoRuntimeError(`File not found for id ${id}`));
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Convenience wrapper around find on the files collection */
|
||||
find(filter?: Filter<GridFSFile>, options?: FindOptions): FindCursor<GridFSFile> {
|
||||
filter ??= {};
|
||||
options = options ?? {};
|
||||
return this.s._filesCollection.find(filter, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a readable stream (GridFSBucketReadStream) for streaming the
|
||||
* file with the given name from GridFS. If there are multiple files with
|
||||
* the same name, this will stream the most recent file with the given name
|
||||
* (as determined by the `uploadDate` field). You can set the `revision`
|
||||
* option to change this behavior.
|
||||
*/
|
||||
openDownloadStreamByName(
|
||||
filename: string,
|
||||
options?: GridFSBucketReadStreamOptionsWithRevision
|
||||
): GridFSBucketReadStream {
|
||||
let sort: Sort = { uploadDate: -1 };
|
||||
let skip = undefined;
|
||||
if (options && options.revision != null) {
|
||||
if (options.revision >= 0) {
|
||||
sort = { uploadDate: 1 };
|
||||
skip = options.revision;
|
||||
} else {
|
||||
skip = -options.revision - 1;
|
||||
}
|
||||
}
|
||||
return new GridFSBucketReadStream(
|
||||
this.s._chunksCollection,
|
||||
this.s._filesCollection,
|
||||
this.s.options.readPreference,
|
||||
{ filename },
|
||||
{ ...options, sort, skip }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the file with the given _id to the given string
|
||||
*
|
||||
* @param id - the id of the file to rename
|
||||
* @param filename - new name for the file
|
||||
*/
|
||||
rename(id: ObjectId, filename: string): Promise<void>;
|
||||
rename(id: ObjectId, filename: string, callback: Callback<void>): void;
|
||||
rename(id: ObjectId, filename: string, callback?: Callback<void>): Promise<void> | void {
|
||||
return maybePromise(callback, callback => {
|
||||
const filter = { _id: id };
|
||||
const update = { $set: { filename } };
|
||||
return this.s._filesCollection.updateOne(filter, update, (error?, res?) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (!res?.matchedCount) {
|
||||
return callback(new MongoRuntimeError(`File with id ${id} not found`));
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes this bucket's files collection, followed by its chunks collection. */
|
||||
drop(): Promise<void>;
|
||||
drop(callback: Callback<void>): void;
|
||||
drop(callback?: Callback<void>): Promise<void> | void {
|
||||
return maybePromise(callback, callback => {
|
||||
return this.s._filesCollection.drop(error => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
return this.s._chunksCollection.drop(error => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Get the Db scoped logger. */
|
||||
getLogger(): Logger {
|
||||
return this.s.db.s.logger;
|
||||
}
|
||||
}
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
import { Writable } from 'stream';
|
||||
|
||||
import type { Document } from '../bson';
|
||||
import { ObjectId } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { AnyError, MongoAPIError, MONGODB_ERROR_CODES, MongoError } from '../error';
|
||||
import { Callback, maybePromise } from '../utils';
|
||||
import type { WriteConcernOptions } from '../write_concern';
|
||||
import { WriteConcern } from './../write_concern';
|
||||
import type { GridFSFile } from './download';
|
||||
import type { GridFSBucket } from './index';
|
||||
|
||||
/** @public */
|
||||
export interface GridFSChunk {
|
||||
_id: ObjectId;
|
||||
files_id: ObjectId;
|
||||
n: number;
|
||||
data: Buffer | Uint8Array;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface GridFSBucketWriteStreamOptions extends WriteConcernOptions {
|
||||
/** Overwrite this bucket's chunkSizeBytes for this file */
|
||||
chunkSizeBytes?: number;
|
||||
/** Custom file id for the GridFS file. */
|
||||
id?: ObjectId;
|
||||
/** Object to store in the file document's `metadata` field */
|
||||
metadata?: Document;
|
||||
/** String to store in the file document's `contentType` field */
|
||||
contentType?: string;
|
||||
/** Array of strings to store in the file document's `aliases` field */
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A writable stream that enables you to write buffers to GridFS.
|
||||
*
|
||||
* Do not instantiate this class directly. Use `openUploadStream()` instead.
|
||||
* @public
|
||||
*/
|
||||
export class GridFSBucketWriteStream extends Writable implements NodeJS.WritableStream {
|
||||
bucket: GridFSBucket;
|
||||
chunks: Collection<GridFSChunk>;
|
||||
filename: string;
|
||||
files: Collection<GridFSFile>;
|
||||
options: GridFSBucketWriteStreamOptions;
|
||||
done: boolean;
|
||||
id: ObjectId;
|
||||
chunkSizeBytes: number;
|
||||
bufToStore: Buffer;
|
||||
length: number;
|
||||
n: number;
|
||||
pos: number;
|
||||
state: {
|
||||
streamEnd: boolean;
|
||||
outstandingRequests: number;
|
||||
errored: boolean;
|
||||
aborted: boolean;
|
||||
};
|
||||
writeConcern?: WriteConcern;
|
||||
|
||||
/** @event */
|
||||
static readonly CLOSE = 'close';
|
||||
/** @event */
|
||||
static readonly ERROR = 'error';
|
||||
/**
|
||||
* `end()` was called and the write stream successfully wrote the file metadata and all the chunks to MongoDB.
|
||||
* @event
|
||||
*/
|
||||
static readonly FINISH = 'finish';
|
||||
|
||||
/** @internal
|
||||
* @param bucket - Handle for this stream's corresponding bucket
|
||||
* @param filename - The value of the 'filename' key in the files doc
|
||||
* @param options - Optional settings.
|
||||
*/
|
||||
constructor(bucket: GridFSBucket, filename: string, options?: GridFSBucketWriteStreamOptions) {
|
||||
super();
|
||||
|
||||
options = options ?? {};
|
||||
this.bucket = bucket;
|
||||
this.chunks = bucket.s._chunksCollection;
|
||||
this.filename = filename;
|
||||
this.files = bucket.s._filesCollection;
|
||||
this.options = options;
|
||||
this.writeConcern = WriteConcern.fromOptions(options) || bucket.s.options.writeConcern;
|
||||
// Signals the write is all done
|
||||
this.done = false;
|
||||
|
||||
this.id = options.id ? options.id : new ObjectId();
|
||||
// properly inherit the default chunksize from parent
|
||||
this.chunkSizeBytes = options.chunkSizeBytes || this.bucket.s.options.chunkSizeBytes;
|
||||
this.bufToStore = Buffer.alloc(this.chunkSizeBytes);
|
||||
this.length = 0;
|
||||
this.n = 0;
|
||||
this.pos = 0;
|
||||
this.state = {
|
||||
streamEnd: false,
|
||||
outstandingRequests: 0,
|
||||
errored: false,
|
||||
aborted: false
|
||||
};
|
||||
|
||||
if (!this.bucket.s.calledOpenUploadStream) {
|
||||
this.bucket.s.calledOpenUploadStream = true;
|
||||
|
||||
checkIndexes(this, () => {
|
||||
this.bucket.s.checkedIndexes = true;
|
||||
this.bucket.emit('index');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a buffer to the stream.
|
||||
*
|
||||
* @param chunk - Buffer to write
|
||||
* @param encodingOrCallback - Optional encoding for the buffer
|
||||
* @param callback - Function to call when the chunk was added to the buffer, or if the entire chunk was persisted to MongoDB if this chunk caused a flush.
|
||||
* @returns False if this write required flushing a chunk to MongoDB. True otherwise.
|
||||
*/
|
||||
override write(chunk: Buffer | string): boolean;
|
||||
override write(chunk: Buffer | string, callback: Callback<void>): boolean;
|
||||
override write(chunk: Buffer | string, encoding: BufferEncoding | undefined): boolean;
|
||||
override write(
|
||||
chunk: Buffer | string,
|
||||
encoding: BufferEncoding | undefined,
|
||||
callback: Callback<void>
|
||||
): boolean;
|
||||
override write(
|
||||
chunk: Buffer | string,
|
||||
encodingOrCallback?: Callback<void> | BufferEncoding,
|
||||
callback?: Callback<void>
|
||||
): boolean {
|
||||
const encoding = typeof encodingOrCallback === 'function' ? undefined : encodingOrCallback;
|
||||
callback = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
|
||||
return waitForIndexes(this, () => doWrite(this, chunk, encoding, callback));
|
||||
}
|
||||
|
||||
// TODO(NODE-3405): Refactor this with maybePromise and MongoStreamClosedError
|
||||
/**
|
||||
* Places this write stream into an aborted state (all future writes fail)
|
||||
* and deletes all chunks that have already been written.
|
||||
*
|
||||
* @param callback - called when chunks are successfully removed or error occurred
|
||||
*/
|
||||
abort(): Promise<void>;
|
||||
abort(callback: Callback<void>): void;
|
||||
abort(callback?: Callback<void>): Promise<void> | void {
|
||||
return maybePromise(callback, callback => {
|
||||
if (this.state.streamEnd) {
|
||||
// TODO(NODE-3485): Replace with MongoGridFSStreamClosed
|
||||
return callback(new MongoAPIError('Cannot abort a stream that has already completed'));
|
||||
}
|
||||
|
||||
if (this.state.aborted) {
|
||||
// TODO(NODE-3485): Replace with MongoGridFSStreamClosed
|
||||
return callback(new MongoAPIError('Cannot call abort() on a stream twice'));
|
||||
}
|
||||
|
||||
this.state.aborted = true;
|
||||
this.chunks.deleteMany({ files_id: this.id }, error => callback(error));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the stream that no more data will be coming in. The stream will
|
||||
* persist the remaining data to MongoDB, write the files document, and
|
||||
* then emit a 'finish' event.
|
||||
*
|
||||
* @param chunk - Buffer to write
|
||||
* @param encoding - Optional encoding for the buffer
|
||||
* @param callback - Function to call when all files and chunks have been persisted to MongoDB
|
||||
*/
|
||||
override end(): this;
|
||||
override end(chunk: Buffer): this;
|
||||
override end(callback: Callback<GridFSFile | void>): this;
|
||||
override end(chunk: Buffer, callback: Callback<GridFSFile | void>): this;
|
||||
override end(chunk: Buffer, encoding: BufferEncoding): this;
|
||||
override end(
|
||||
chunk: Buffer,
|
||||
encoding: BufferEncoding | undefined,
|
||||
callback: Callback<GridFSFile | void>
|
||||
): this;
|
||||
override end(
|
||||
chunkOrCallback?: Buffer | Callback<GridFSFile | void>,
|
||||
encodingOrCallback?: BufferEncoding | Callback<GridFSFile | void>,
|
||||
callback?: Callback<GridFSFile | void>
|
||||
): this {
|
||||
const chunk = typeof chunkOrCallback === 'function' ? undefined : chunkOrCallback;
|
||||
const encoding = typeof encodingOrCallback === 'function' ? undefined : encodingOrCallback;
|
||||
callback =
|
||||
typeof chunkOrCallback === 'function'
|
||||
? chunkOrCallback
|
||||
: typeof encodingOrCallback === 'function'
|
||||
? encodingOrCallback
|
||||
: callback;
|
||||
|
||||
if (this.state.streamEnd || checkAborted(this, callback)) return this;
|
||||
|
||||
this.state.streamEnd = true;
|
||||
|
||||
if (callback) {
|
||||
this.once(GridFSBucketWriteStream.FINISH, (result: GridFSFile) => {
|
||||
if (callback) callback(undefined, result);
|
||||
});
|
||||
}
|
||||
|
||||
if (!chunk) {
|
||||
waitForIndexes(this, () => !!writeRemnant(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
this.write(chunk, encoding, () => {
|
||||
writeRemnant(this);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function __handleError(
|
||||
stream: GridFSBucketWriteStream,
|
||||
error: AnyError,
|
||||
callback?: Callback
|
||||
): void {
|
||||
if (stream.state.errored) {
|
||||
return;
|
||||
}
|
||||
stream.state.errored = true;
|
||||
if (callback) {
|
||||
return callback(error);
|
||||
}
|
||||
stream.emit(GridFSBucketWriteStream.ERROR, error);
|
||||
}
|
||||
|
||||
function createChunkDoc(filesId: ObjectId, n: number, data: Buffer): GridFSChunk {
|
||||
return {
|
||||
_id: new ObjectId(),
|
||||
files_id: filesId,
|
||||
n,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function checkChunksIndex(stream: GridFSBucketWriteStream, callback: Callback): void {
|
||||
stream.chunks.listIndexes().toArray((error?: AnyError, indexes?: Document[]) => {
|
||||
let index: { files_id: number; n: number };
|
||||
if (error) {
|
||||
// Collection doesn't exist so create index
|
||||
if (error instanceof MongoError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound) {
|
||||
index = { files_id: 1, n: 1 };
|
||||
stream.chunks.createIndex(index, { background: false, unique: true }, error => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
let hasChunksIndex = false;
|
||||
if (indexes) {
|
||||
indexes.forEach((index: Document) => {
|
||||
if (index.key) {
|
||||
const keys = Object.keys(index.key);
|
||||
if (keys.length === 2 && index.key.files_id === 1 && index.key.n === 1) {
|
||||
hasChunksIndex = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hasChunksIndex) {
|
||||
callback();
|
||||
} else {
|
||||
index = { files_id: 1, n: 1 };
|
||||
const writeConcernOptions = getWriteOptions(stream);
|
||||
|
||||
stream.chunks.createIndex(
|
||||
index,
|
||||
{
|
||||
...writeConcernOptions,
|
||||
background: true,
|
||||
unique: true
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkDone(stream: GridFSBucketWriteStream, callback?: Callback): boolean {
|
||||
if (stream.done) return true;
|
||||
if (stream.state.streamEnd && stream.state.outstandingRequests === 0 && !stream.state.errored) {
|
||||
// Set done so we do not trigger duplicate createFilesDoc
|
||||
stream.done = true;
|
||||
// Create a new files doc
|
||||
const filesDoc = createFilesDoc(
|
||||
stream.id,
|
||||
stream.length,
|
||||
stream.chunkSizeBytes,
|
||||
stream.filename,
|
||||
stream.options.contentType,
|
||||
stream.options.aliases,
|
||||
stream.options.metadata
|
||||
);
|
||||
|
||||
if (checkAborted(stream, callback)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.files.insertOne(filesDoc, getWriteOptions(stream), (error?: AnyError) => {
|
||||
if (error) {
|
||||
return __handleError(stream, error, callback);
|
||||
}
|
||||
stream.emit(GridFSBucketWriteStream.FINISH, filesDoc);
|
||||
stream.emit(GridFSBucketWriteStream.CLOSE);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkIndexes(stream: GridFSBucketWriteStream, callback: Callback): void {
|
||||
stream.files.findOne({}, { projection: { _id: 1 } }, (error, doc) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
if (doc) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
stream.files.listIndexes().toArray((error?: AnyError, indexes?: Document) => {
|
||||
let index: { filename: number; uploadDate: number };
|
||||
if (error) {
|
||||
// Collection doesn't exist so create index
|
||||
if (error instanceof MongoError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound) {
|
||||
index = { filename: 1, uploadDate: 1 };
|
||||
stream.files.createIndex(index, { background: false }, (error?: AnyError) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
checkChunksIndex(stream, callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
let hasFileIndex = false;
|
||||
if (indexes) {
|
||||
indexes.forEach((index: Document) => {
|
||||
const keys = Object.keys(index.key);
|
||||
if (keys.length === 2 && index.key.filename === 1 && index.key.uploadDate === 1) {
|
||||
hasFileIndex = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFileIndex) {
|
||||
checkChunksIndex(stream, callback);
|
||||
} else {
|
||||
index = { filename: 1, uploadDate: 1 };
|
||||
|
||||
const writeConcernOptions = getWriteOptions(stream);
|
||||
|
||||
stream.files.createIndex(
|
||||
index,
|
||||
{
|
||||
...writeConcernOptions,
|
||||
background: false
|
||||
},
|
||||
(error?: AnyError) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
checkChunksIndex(stream, callback);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createFilesDoc(
|
||||
_id: ObjectId,
|
||||
length: number,
|
||||
chunkSize: number,
|
||||
filename: string,
|
||||
contentType?: string,
|
||||
aliases?: string[],
|
||||
metadata?: Document
|
||||
): GridFSFile {
|
||||
const ret: GridFSFile = {
|
||||
_id,
|
||||
length,
|
||||
chunkSize,
|
||||
uploadDate: new Date(),
|
||||
filename
|
||||
};
|
||||
|
||||
if (contentType) {
|
||||
ret.contentType = contentType;
|
||||
}
|
||||
|
||||
if (aliases) {
|
||||
ret.aliases = aliases;
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
ret.metadata = metadata;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function doWrite(
|
||||
stream: GridFSBucketWriteStream,
|
||||
chunk: Buffer | string,
|
||||
encoding?: BufferEncoding,
|
||||
callback?: Callback<void>
|
||||
): boolean {
|
||||
if (checkAborted(stream, callback)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
||||
|
||||
stream.length += inputBuf.length;
|
||||
|
||||
// Input is small enough to fit in our buffer
|
||||
if (stream.pos + inputBuf.length < stream.chunkSizeBytes) {
|
||||
inputBuf.copy(stream.bufToStore, stream.pos);
|
||||
stream.pos += inputBuf.length;
|
||||
|
||||
callback && callback();
|
||||
|
||||
// Note that we reverse the typical semantics of write's return value
|
||||
// to be compatible with node's `.pipe()` function.
|
||||
// True means client can keep writing.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, buffer is too big for current chunk, so we need to flush
|
||||
// to MongoDB.
|
||||
let inputBufRemaining = inputBuf.length;
|
||||
let spaceRemaining: number = stream.chunkSizeBytes - stream.pos;
|
||||
let numToCopy = Math.min(spaceRemaining, inputBuf.length);
|
||||
let outstandingRequests = 0;
|
||||
while (inputBufRemaining > 0) {
|
||||
const inputBufPos = inputBuf.length - inputBufRemaining;
|
||||
inputBuf.copy(stream.bufToStore, stream.pos, inputBufPos, inputBufPos + numToCopy);
|
||||
stream.pos += numToCopy;
|
||||
spaceRemaining -= numToCopy;
|
||||
let doc: GridFSChunk;
|
||||
if (spaceRemaining === 0) {
|
||||
doc = createChunkDoc(stream.id, stream.n, Buffer.from(stream.bufToStore));
|
||||
++stream.state.outstandingRequests;
|
||||
++outstandingRequests;
|
||||
|
||||
if (checkAborted(stream, callback)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.chunks.insertOne(doc, getWriteOptions(stream), (error?: AnyError) => {
|
||||
if (error) {
|
||||
return __handleError(stream, error);
|
||||
}
|
||||
--stream.state.outstandingRequests;
|
||||
--outstandingRequests;
|
||||
|
||||
if (!outstandingRequests) {
|
||||
stream.emit('drain', doc);
|
||||
callback && callback();
|
||||
checkDone(stream);
|
||||
}
|
||||
});
|
||||
|
||||
spaceRemaining = stream.chunkSizeBytes;
|
||||
stream.pos = 0;
|
||||
++stream.n;
|
||||
}
|
||||
inputBufRemaining -= numToCopy;
|
||||
numToCopy = Math.min(spaceRemaining, inputBufRemaining);
|
||||
}
|
||||
|
||||
// Note that we reverse the typical semantics of write's return value
|
||||
// to be compatible with node's `.pipe()` function.
|
||||
// False means the client should wait for the 'drain' event.
|
||||
return false;
|
||||
}
|
||||
|
||||
function getWriteOptions(stream: GridFSBucketWriteStream): WriteConcernOptions {
|
||||
const obj: WriteConcernOptions = {};
|
||||
if (stream.writeConcern) {
|
||||
obj.writeConcern = {
|
||||
w: stream.writeConcern.w,
|
||||
wtimeout: stream.writeConcern.wtimeout,
|
||||
j: stream.writeConcern.j
|
||||
};
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function waitForIndexes(
|
||||
stream: GridFSBucketWriteStream,
|
||||
callback: (res: boolean) => boolean
|
||||
): boolean {
|
||||
if (stream.bucket.s.checkedIndexes) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
stream.bucket.once('index', () => {
|
||||
callback(true);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function writeRemnant(stream: GridFSBucketWriteStream, callback?: Callback): boolean {
|
||||
// Buffer is empty, so don't bother to insert
|
||||
if (stream.pos === 0) {
|
||||
return checkDone(stream, callback);
|
||||
}
|
||||
|
||||
++stream.state.outstandingRequests;
|
||||
|
||||
// Create a new buffer to make sure the buffer isn't bigger than it needs
|
||||
// to be.
|
||||
const remnant = Buffer.alloc(stream.pos);
|
||||
stream.bufToStore.copy(remnant, 0, 0, stream.pos);
|
||||
const doc = createChunkDoc(stream.id, stream.n, remnant);
|
||||
|
||||
// If the stream was aborted, do not write remnant
|
||||
if (checkAborted(stream, callback)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.chunks.insertOne(doc, getWriteOptions(stream), (error?: AnyError) => {
|
||||
if (error) {
|
||||
return __handleError(stream, error);
|
||||
}
|
||||
--stream.state.outstandingRequests;
|
||||
checkDone(stream);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkAborted(stream: GridFSBucketWriteStream, callback?: Callback<void>): boolean {
|
||||
if (stream.state.aborted) {
|
||||
if (typeof callback === 'function') {
|
||||
// TODO(NODE-3485): Replace with MongoGridFSStreamClosedError
|
||||
callback(new MongoAPIError('Stream has been aborted'));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
+477
@@ -0,0 +1,477 @@
|
||||
import { Admin } from './admin';
|
||||
import { ObjectId } from './bson';
|
||||
import { ChangeStream } from './change_stream';
|
||||
import { Collection } from './collection';
|
||||
import { AbstractCursor } from './cursor/abstract_cursor';
|
||||
import { AggregationCursor } from './cursor/aggregation_cursor';
|
||||
import { FindCursor } from './cursor/find_cursor';
|
||||
import { ListCollectionsCursor } from './cursor/list_collections_cursor';
|
||||
import { ListIndexesCursor } from './cursor/list_indexes_cursor';
|
||||
import { Db } from './db';
|
||||
import { GridFSBucket } from './gridfs';
|
||||
import { Logger } from './logger';
|
||||
import { MongoClient } from './mongo_client';
|
||||
import { CancellationToken } from './mongo_types';
|
||||
import { PromiseProvider } from './promise_provider';
|
||||
|
||||
/** @internal */
|
||||
export { BSON } from './bson';
|
||||
export {
|
||||
Binary,
|
||||
BSONRegExp,
|
||||
BSONSymbol,
|
||||
Code,
|
||||
DBRef,
|
||||
Decimal128,
|
||||
Double,
|
||||
Int32,
|
||||
Long,
|
||||
Map,
|
||||
MaxKey,
|
||||
MinKey,
|
||||
ObjectId,
|
||||
Timestamp
|
||||
} from './bson';
|
||||
export { ChangeStreamCursor } from './cursor/change_stream_cursor';
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please use `ObjectId`
|
||||
*/
|
||||
export const ObjectID = ObjectId;
|
||||
|
||||
export { AnyBulkWriteOperation, BulkWriteOptions, MongoBulkWriteError } from './bulk/common';
|
||||
export {
|
||||
MongoAPIError,
|
||||
MongoAWSError,
|
||||
MongoBatchReExecutionError,
|
||||
MongoChangeStreamError,
|
||||
MongoCompatibilityError,
|
||||
MongoCursorExhaustedError,
|
||||
MongoCursorInUseError,
|
||||
MongoDecompressionError,
|
||||
MongoDriverError,
|
||||
MongoError,
|
||||
MongoExpiredSessionError,
|
||||
MongoGridFSChunkError,
|
||||
MongoGridFSStreamError,
|
||||
MongoInvalidArgumentError,
|
||||
MongoKerberosError,
|
||||
MongoMissingCredentialsError,
|
||||
MongoMissingDependencyError,
|
||||
MongoNetworkError,
|
||||
MongoNetworkTimeoutError,
|
||||
MongoNotConnectedError,
|
||||
MongoParseError,
|
||||
MongoRuntimeError,
|
||||
MongoServerClosedError,
|
||||
MongoServerError,
|
||||
MongoServerSelectionError,
|
||||
MongoSystemError,
|
||||
MongoTailableCursorError,
|
||||
MongoTopologyClosedError,
|
||||
MongoTransactionError,
|
||||
MongoUnexpectedServerResponseError,
|
||||
MongoWriteConcernError
|
||||
} from './error';
|
||||
export {
|
||||
AbstractCursor,
|
||||
// Actual driver classes exported
|
||||
Admin,
|
||||
AggregationCursor,
|
||||
CancellationToken,
|
||||
ChangeStream,
|
||||
Collection,
|
||||
Db,
|
||||
FindCursor,
|
||||
GridFSBucket,
|
||||
ListCollectionsCursor,
|
||||
ListIndexesCursor,
|
||||
Logger,
|
||||
MongoClient,
|
||||
// Utils
|
||||
PromiseProvider as Promise
|
||||
};
|
||||
|
||||
// enums
|
||||
export { BatchType } from './bulk/common';
|
||||
export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi';
|
||||
export { AuthMechanism } from './cmap/auth/providers';
|
||||
export { Compressor } from './cmap/wire_protocol/compression';
|
||||
export { CURSOR_FLAGS } from './cursor/abstract_cursor';
|
||||
export { AutoEncryptionLoggerLevel } from './deps';
|
||||
export { MongoErrorLabel } from './error';
|
||||
export { ExplainVerbosity } from './explain';
|
||||
export { LoggerLevel } from './logger';
|
||||
export { ServerApiVersion } from './mongo_client';
|
||||
export { BSONType } from './mongo_types';
|
||||
export { ReturnDocument } from './operations/find_and_modify';
|
||||
export { ProfilingLevel } from './operations/set_profiling_level';
|
||||
export { ReadConcernLevel } from './read_concern';
|
||||
export { ReadPreferenceMode } from './read_preference';
|
||||
export { ServerType, TopologyType } from './sdam/common';
|
||||
|
||||
// Helper classes
|
||||
export { ReadConcern } from './read_concern';
|
||||
export { ReadPreference } from './read_preference';
|
||||
export { WriteConcern } from './write_concern';
|
||||
|
||||
// events
|
||||
export {
|
||||
CommandFailedEvent,
|
||||
CommandStartedEvent,
|
||||
CommandSucceededEvent
|
||||
} from './cmap/command_monitoring_events';
|
||||
export {
|
||||
ConnectionCheckedInEvent,
|
||||
ConnectionCheckedOutEvent,
|
||||
ConnectionCheckOutFailedEvent,
|
||||
ConnectionCheckOutStartedEvent,
|
||||
ConnectionClosedEvent,
|
||||
ConnectionCreatedEvent,
|
||||
ConnectionPoolClearedEvent,
|
||||
ConnectionPoolClosedEvent,
|
||||
ConnectionPoolCreatedEvent,
|
||||
ConnectionPoolMonitoringEvent,
|
||||
ConnectionReadyEvent
|
||||
} from './cmap/connection_pool_events';
|
||||
export {
|
||||
ServerClosedEvent,
|
||||
ServerDescriptionChangedEvent,
|
||||
ServerHeartbeatFailedEvent,
|
||||
ServerHeartbeatStartedEvent,
|
||||
ServerHeartbeatSucceededEvent,
|
||||
ServerOpeningEvent,
|
||||
TopologyClosedEvent,
|
||||
TopologyDescriptionChangedEvent,
|
||||
TopologyOpeningEvent
|
||||
} from './sdam/events';
|
||||
export { SrvPollingEvent } from './sdam/srv_polling';
|
||||
|
||||
// type only exports below, these are removed from emitted JS
|
||||
export type { AdminPrivate } from './admin';
|
||||
export type { BSONSerializeOptions, Document } from './bson';
|
||||
export type { deserialize, serialize } from './bson';
|
||||
export type {
|
||||
BulkResult,
|
||||
BulkWriteOperationError,
|
||||
BulkWriteResult,
|
||||
DeleteManyModel,
|
||||
DeleteOneModel,
|
||||
InsertOneModel,
|
||||
ReplaceOneModel,
|
||||
UpdateManyModel,
|
||||
UpdateOneModel,
|
||||
WriteConcernError,
|
||||
WriteError
|
||||
} from './bulk/common';
|
||||
export type {
|
||||
Batch,
|
||||
BulkOperationBase,
|
||||
BulkOperationPrivate,
|
||||
FindOperators,
|
||||
WriteConcernErrorData
|
||||
} from './bulk/common';
|
||||
export type { OrderedBulkOperation } from './bulk/ordered';
|
||||
export type { UnorderedBulkOperation } from './bulk/unordered';
|
||||
export type {
|
||||
ChangeStreamCollModDocument,
|
||||
ChangeStreamCreateDocument,
|
||||
ChangeStreamCreateIndexDocument,
|
||||
ChangeStreamDeleteDocument,
|
||||
ChangeStreamDocument,
|
||||
ChangeStreamDocumentCollectionUUID,
|
||||
ChangeStreamDocumentCommon,
|
||||
ChangeStreamDocumentKey,
|
||||
ChangeStreamDocumentOperationDescription,
|
||||
ChangeStreamDropDatabaseDocument,
|
||||
ChangeStreamDropDocument,
|
||||
ChangeStreamDropIndexDocument,
|
||||
ChangeStreamEvents,
|
||||
ChangeStreamInsertDocument,
|
||||
ChangeStreamInvalidateDocument,
|
||||
ChangeStreamNameSpace,
|
||||
ChangeStreamOptions,
|
||||
ChangeStreamRefineCollectionShardKeyDocument,
|
||||
ChangeStreamRenameDocument,
|
||||
ChangeStreamReplaceDocument,
|
||||
ChangeStreamReshardCollectionDocument,
|
||||
ChangeStreamShardCollectionDocument,
|
||||
ChangeStreamUpdateDocument,
|
||||
OperationTime,
|
||||
PipeOptions,
|
||||
ResumeOptions,
|
||||
ResumeToken,
|
||||
UpdateDescription
|
||||
} from './change_stream';
|
||||
export type {
|
||||
AuthMechanismProperties,
|
||||
MongoCredentials,
|
||||
MongoCredentialsOptions
|
||||
} from './cmap/auth/mongo_credentials';
|
||||
export type {
|
||||
BinMsg,
|
||||
MessageHeader,
|
||||
Msg,
|
||||
OpMsgOptions,
|
||||
OpQueryOptions,
|
||||
OpResponseOptions,
|
||||
Query,
|
||||
Response,
|
||||
WriteProtocolMessageType
|
||||
} from './cmap/commands';
|
||||
export type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS, Stream } from './cmap/connect';
|
||||
export type {
|
||||
CommandOptions,
|
||||
Connection,
|
||||
ConnectionEvents,
|
||||
ConnectionOptions,
|
||||
DestroyOptions,
|
||||
GetMoreOptions,
|
||||
ProxyOptions,
|
||||
QueryOptions
|
||||
} from './cmap/connection';
|
||||
export type {
|
||||
CloseOptions,
|
||||
ConnectionPool,
|
||||
ConnectionPoolEvents,
|
||||
ConnectionPoolOptions,
|
||||
PoolState,
|
||||
WaitQueueMember,
|
||||
WithConnectionCallback
|
||||
} from './cmap/connection_pool';
|
||||
export type {
|
||||
MessageStream,
|
||||
MessageStreamOptions,
|
||||
OperationDescription
|
||||
} from './cmap/message_stream';
|
||||
export type { ConnectionPoolMetrics } from './cmap/metrics';
|
||||
export type { StreamDescription, StreamDescriptionOptions } from './cmap/stream_description';
|
||||
export type { CompressorName } from './cmap/wire_protocol/compression';
|
||||
export type { CollectionOptions, CollectionPrivate, ModifyResult } from './collection';
|
||||
export type { MONGO_CLIENT_EVENTS } from './constants';
|
||||
export type {
|
||||
AbstractCursorEvents,
|
||||
AbstractCursorOptions,
|
||||
CursorCloseOptions,
|
||||
CursorFlag,
|
||||
CursorStreamOptions
|
||||
} from './cursor/abstract_cursor';
|
||||
export type { InternalAbstractCursorOptions } from './cursor/abstract_cursor';
|
||||
export type { AggregationCursorOptions } from './cursor/aggregation_cursor';
|
||||
export type {
|
||||
ChangeStreamAggregateRawResult,
|
||||
ChangeStreamCursorOptions
|
||||
} from './cursor/change_stream_cursor';
|
||||
export type { DbOptions, DbPrivate } from './db';
|
||||
export type { AutoEncrypter, AutoEncryptionOptions, AutoEncryptionTlsOptions } from './deps';
|
||||
export type { Encrypter, EncrypterOptions } from './encrypter';
|
||||
export type { AnyError, ErrorDescription, MongoNetworkErrorOptions } from './error';
|
||||
export type { Explain, ExplainOptions, ExplainVerbosityLike } from './explain';
|
||||
export type {
|
||||
GridFSBucketReadStream,
|
||||
GridFSBucketReadStreamOptions,
|
||||
GridFSBucketReadStreamOptionsWithRevision,
|
||||
GridFSBucketReadStreamPrivate,
|
||||
GridFSFile
|
||||
} from './gridfs/download';
|
||||
export type { GridFSBucketEvents, GridFSBucketOptions, GridFSBucketPrivate } from './gridfs/index';
|
||||
export type {
|
||||
GridFSBucketWriteStream,
|
||||
GridFSBucketWriteStreamOptions,
|
||||
GridFSChunk
|
||||
} from './gridfs/upload';
|
||||
export type { LoggerFunction, LoggerOptions } from './logger';
|
||||
export type {
|
||||
Auth,
|
||||
DriverInfo,
|
||||
MongoClientEvents,
|
||||
MongoClientOptions,
|
||||
MongoClientPrivate,
|
||||
MongoOptions,
|
||||
PkFactory,
|
||||
ServerApi,
|
||||
SupportedNodeConnectionOptions,
|
||||
SupportedSocketOptions,
|
||||
SupportedTLSConnectionOptions,
|
||||
SupportedTLSSocketOptions,
|
||||
WithSessionCallback
|
||||
} from './mongo_client';
|
||||
export type {
|
||||
CommonEvents,
|
||||
EventsDescription,
|
||||
GenericListener,
|
||||
TypedEventEmitter
|
||||
} from './mongo_types';
|
||||
export type {
|
||||
AcceptedFields,
|
||||
AddToSetOperators,
|
||||
AlternativeType,
|
||||
ArrayElement,
|
||||
ArrayOperator,
|
||||
BitwiseFilter,
|
||||
BSONTypeAlias,
|
||||
Condition,
|
||||
EnhancedOmit,
|
||||
Filter,
|
||||
FilterOperations,
|
||||
FilterOperators,
|
||||
Flatten,
|
||||
InferIdType,
|
||||
IntegerType,
|
||||
IsAny,
|
||||
Join,
|
||||
KeysOfAType,
|
||||
KeysOfOtherType,
|
||||
MatchKeysAndValues,
|
||||
NestedPaths,
|
||||
NestedPathsOfType,
|
||||
NonObjectIdLikeDocument,
|
||||
NotAcceptedFields,
|
||||
NumericType,
|
||||
OneOrMore,
|
||||
OnlyFieldsOfType,
|
||||
OptionalId,
|
||||
OptionalUnlessRequiredId,
|
||||
Projection,
|
||||
ProjectionOperators,
|
||||
PropertyType,
|
||||
PullAllOperator,
|
||||
PullOperator,
|
||||
PushOperator,
|
||||
RegExpOrString,
|
||||
RootFilterOperators,
|
||||
SchemaMember,
|
||||
SetFields,
|
||||
UpdateFilter,
|
||||
WithId,
|
||||
WithoutId
|
||||
} from './mongo_types';
|
||||
export type { AddUserOptions, RoleSpecification } from './operations/add_user';
|
||||
export type {
|
||||
AggregateOperation,
|
||||
AggregateOptions,
|
||||
DB_AGGREGATE_COLLECTION
|
||||
} from './operations/aggregate';
|
||||
export type {
|
||||
CollationOptions,
|
||||
CommandOperation,
|
||||
CommandOperationOptions,
|
||||
OperationParent
|
||||
} from './operations/command';
|
||||
export type { IndexInformationOptions } from './operations/common_functions';
|
||||
export type { CountOptions } from './operations/count';
|
||||
export type { CountDocumentsOptions } from './operations/count_documents';
|
||||
export type {
|
||||
ClusteredCollectionOptions,
|
||||
CreateCollectionOptions,
|
||||
TimeSeriesCollectionOptions
|
||||
} from './operations/create_collection';
|
||||
export type { DeleteOptions, DeleteResult, DeleteStatement } from './operations/delete';
|
||||
export type { DistinctOptions } from './operations/distinct';
|
||||
export type { DropCollectionOptions, DropDatabaseOptions } from './operations/drop';
|
||||
export type { EstimatedDocumentCountOptions } from './operations/estimated_document_count';
|
||||
export type { EvalOptions } from './operations/eval';
|
||||
export type { ExecutionResult } from './operations/execute_operation';
|
||||
export type { FindOptions } from './operations/find';
|
||||
export type {
|
||||
FindOneAndDeleteOptions,
|
||||
FindOneAndReplaceOptions,
|
||||
FindOneAndUpdateOptions
|
||||
} from './operations/find_and_modify';
|
||||
export type {
|
||||
CreateIndexesOptions,
|
||||
DropIndexesOptions,
|
||||
IndexDescription,
|
||||
IndexDirection,
|
||||
IndexSpecification,
|
||||
ListIndexesOptions
|
||||
} from './operations/indexes';
|
||||
export type { InsertManyResult, InsertOneOptions, InsertOneResult } from './operations/insert';
|
||||
export type { CollectionInfo, ListCollectionsOptions } from './operations/list_collections';
|
||||
export type { ListDatabasesOptions, ListDatabasesResult } from './operations/list_databases';
|
||||
export type {
|
||||
FinalizeFunction,
|
||||
MapFunction,
|
||||
MapReduceOptions,
|
||||
ReduceFunction
|
||||
} from './operations/map_reduce';
|
||||
export type { AbstractOperation, Hint, OperationOptions } from './operations/operation';
|
||||
export type { ProfilingLevelOptions } from './operations/profiling_level';
|
||||
export type { RemoveUserOptions } from './operations/remove_user';
|
||||
export type { RenameOptions } from './operations/rename';
|
||||
export type { RunCommandOptions } from './operations/run_command';
|
||||
export type { SetProfilingLevelOptions } from './operations/set_profiling_level';
|
||||
export type {
|
||||
CollStats,
|
||||
CollStatsOptions,
|
||||
DbStatsOptions,
|
||||
WiredTigerData
|
||||
} from './operations/stats';
|
||||
export type {
|
||||
ReplaceOptions,
|
||||
UpdateOptions,
|
||||
UpdateResult,
|
||||
UpdateStatement
|
||||
} from './operations/update';
|
||||
export type { ValidateCollectionOptions } from './operations/validate_collection';
|
||||
export type { ReadConcernLike } from './read_concern';
|
||||
export type {
|
||||
HedgeOptions,
|
||||
ReadPreferenceFromOptions,
|
||||
ReadPreferenceLike,
|
||||
ReadPreferenceLikeOptions,
|
||||
ReadPreferenceOptions
|
||||
} from './read_preference';
|
||||
export type { ClusterTime, TimerQueue } from './sdam/common';
|
||||
export type {
|
||||
Monitor,
|
||||
MonitorEvents,
|
||||
MonitorOptions,
|
||||
MonitorPrivate,
|
||||
RTTPinger,
|
||||
RTTPingerOptions
|
||||
} from './sdam/monitor';
|
||||
export type { Server, ServerEvents, ServerOptions, ServerPrivate } from './sdam/server';
|
||||
export type {
|
||||
ServerDescription,
|
||||
ServerDescriptionOptions,
|
||||
TagSet,
|
||||
TopologyVersion
|
||||
} from './sdam/server_description';
|
||||
export type { ServerSelector } from './sdam/server_selection';
|
||||
export type { SrvPoller, SrvPollerEvents, SrvPollerOptions } from './sdam/srv_polling';
|
||||
export type {
|
||||
ConnectOptions,
|
||||
SelectServerOptions,
|
||||
ServerCapabilities,
|
||||
ServerSelectionCallback,
|
||||
ServerSelectionRequest,
|
||||
Topology,
|
||||
TopologyEvents,
|
||||
TopologyOptions,
|
||||
TopologyPrivate
|
||||
} from './sdam/topology';
|
||||
export type { TopologyDescription, TopologyDescriptionOptions } from './sdam/topology_description';
|
||||
export type {
|
||||
ClientSession,
|
||||
ClientSessionEvents,
|
||||
ClientSessionOptions,
|
||||
EndSessionOptions,
|
||||
ServerSession,
|
||||
ServerSessionId,
|
||||
ServerSessionPool,
|
||||
WithTransactionCallback
|
||||
} from './sessions';
|
||||
export type { Sort, SortDirection, SortDirectionForCmd, SortForCmd } from './sort';
|
||||
export type { Transaction, TransactionOptions, TxnState } from './transactions';
|
||||
export type {
|
||||
BufferPool,
|
||||
Callback,
|
||||
ClientMetadata,
|
||||
ClientMetadataOptions,
|
||||
EventEmitterWithState,
|
||||
HostAddress,
|
||||
InterruptibleAsyncInterval,
|
||||
MongoDBNamespace
|
||||
} from './utils';
|
||||
export type { W, WriteConcernOptions, WriteConcernSettings } from './write_concern';
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
import { format } from 'util';
|
||||
|
||||
import { MongoInvalidArgumentError } from './error';
|
||||
import { enumToString } from './utils';
|
||||
|
||||
// Filters for classes
|
||||
const classFilters: any = {};
|
||||
let filteredClasses: any = {};
|
||||
let level: LoggerLevel;
|
||||
|
||||
// Save the process id
|
||||
const pid = process.pid;
|
||||
|
||||
// current logger
|
||||
// eslint-disable-next-line no-console
|
||||
let currentLogger: LoggerFunction = console.warn;
|
||||
|
||||
/** @public */
|
||||
export const LoggerLevel = Object.freeze({
|
||||
ERROR: 'error',
|
||||
WARN: 'warn',
|
||||
INFO: 'info',
|
||||
DEBUG: 'debug',
|
||||
error: 'error',
|
||||
warn: 'warn',
|
||||
info: 'info',
|
||||
debug: 'debug'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type LoggerLevel = typeof LoggerLevel[keyof typeof LoggerLevel];
|
||||
|
||||
/** @public */
|
||||
export type LoggerFunction = (message?: any, ...optionalParams: any[]) => void;
|
||||
|
||||
/** @public */
|
||||
export interface LoggerOptions {
|
||||
logger?: LoggerFunction;
|
||||
loggerLevel?: LoggerLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class Logger {
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* Creates a new Logger instance
|
||||
*
|
||||
* @param className - The Class name associated with the logging instance
|
||||
* @param options - Optional logging settings
|
||||
*/
|
||||
constructor(className: string, options?: LoggerOptions) {
|
||||
options = options ?? {};
|
||||
|
||||
// Current reference
|
||||
this.className = className;
|
||||
|
||||
// Current logger
|
||||
if (!(options.logger instanceof Logger) && typeof options.logger === 'function') {
|
||||
currentLogger = options.logger;
|
||||
}
|
||||
|
||||
// Set level of logging, default is error
|
||||
if (options.loggerLevel) {
|
||||
level = options.loggerLevel || LoggerLevel.ERROR;
|
||||
}
|
||||
|
||||
// Add all class names
|
||||
if (filteredClasses[this.className] == null) {
|
||||
classFilters[this.className] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message at the debug level
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param object - Additional meta data to log
|
||||
*/
|
||||
debug(message: string, object?: unknown): void {
|
||||
if (
|
||||
this.isDebug() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
const dateTime = new Date().getTime();
|
||||
const msg = format('[%s-%s:%s] %s %s', 'DEBUG', this.className, pid, dateTime, message);
|
||||
const state = {
|
||||
type: LoggerLevel.DEBUG,
|
||||
message,
|
||||
className: this.className,
|
||||
pid,
|
||||
date: dateTime
|
||||
} as any;
|
||||
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message at the warn level
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param object - Additional meta data to log
|
||||
*/
|
||||
warn(message: string, object?: unknown): void {
|
||||
if (
|
||||
this.isWarn() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
const dateTime = new Date().getTime();
|
||||
const msg = format('[%s-%s:%s] %s %s', 'WARN', this.className, pid, dateTime, message);
|
||||
const state = {
|
||||
type: LoggerLevel.WARN,
|
||||
message,
|
||||
className: this.className,
|
||||
pid,
|
||||
date: dateTime
|
||||
} as any;
|
||||
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message at the info level
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param object - Additional meta data to log
|
||||
*/
|
||||
info(message: string, object?: unknown): void {
|
||||
if (
|
||||
this.isInfo() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
const dateTime = new Date().getTime();
|
||||
const msg = format('[%s-%s:%s] %s %s', 'INFO', this.className, pid, dateTime, message);
|
||||
const state = {
|
||||
type: LoggerLevel.INFO,
|
||||
message,
|
||||
className: this.className,
|
||||
pid,
|
||||
date: dateTime
|
||||
} as any;
|
||||
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message at the error level
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param object - Additional meta data to log
|
||||
*/
|
||||
error(message: string, object?: unknown): void {
|
||||
if (
|
||||
this.isError() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
const dateTime = new Date().getTime();
|
||||
const msg = format('[%s-%s:%s] %s %s', 'ERROR', this.className, pid, dateTime, message);
|
||||
const state = {
|
||||
type: LoggerLevel.ERROR,
|
||||
message,
|
||||
className: this.className,
|
||||
pid,
|
||||
date: dateTime
|
||||
} as any;
|
||||
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
}
|
||||
|
||||
/** Is the logger set at info level */
|
||||
isInfo(): boolean {
|
||||
return level === LoggerLevel.INFO || level === LoggerLevel.DEBUG;
|
||||
}
|
||||
|
||||
/** Is the logger set at error level */
|
||||
isError(): boolean {
|
||||
return level === LoggerLevel.ERROR || level === LoggerLevel.INFO || level === LoggerLevel.DEBUG;
|
||||
}
|
||||
|
||||
/** Is the logger set at error level */
|
||||
isWarn(): boolean {
|
||||
return (
|
||||
level === LoggerLevel.ERROR ||
|
||||
level === LoggerLevel.WARN ||
|
||||
level === LoggerLevel.INFO ||
|
||||
level === LoggerLevel.DEBUG
|
||||
);
|
||||
}
|
||||
|
||||
/** Is the logger set at debug level */
|
||||
isDebug(): boolean {
|
||||
return level === LoggerLevel.DEBUG;
|
||||
}
|
||||
|
||||
/** Resets the logger to default settings, error and no filtered classes */
|
||||
static reset(): void {
|
||||
level = LoggerLevel.ERROR;
|
||||
filteredClasses = {};
|
||||
}
|
||||
|
||||
/** Get the current logger function */
|
||||
static currentLogger(): LoggerFunction {
|
||||
return currentLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current logger function
|
||||
*
|
||||
* @param logger - Custom logging function
|
||||
*/
|
||||
static setCurrentLogger(logger: LoggerFunction): void {
|
||||
if (typeof logger !== 'function') {
|
||||
throw new MongoInvalidArgumentError('Current logger must be a function');
|
||||
}
|
||||
|
||||
currentLogger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter log messages for a particular class
|
||||
*
|
||||
* @param type - The type of filter (currently only class)
|
||||
* @param values - The filters to apply
|
||||
*/
|
||||
static filter(type: string, values: string[]): void {
|
||||
if (type === 'class' && Array.isArray(values)) {
|
||||
filteredClasses = {};
|
||||
values.forEach(x => (filteredClasses[x] = true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current log level
|
||||
*
|
||||
* @param newLevel - Set current log level (debug, warn, info, error)
|
||||
*/
|
||||
static setLevel(newLevel: LoggerLevel): void {
|
||||
if (
|
||||
newLevel !== LoggerLevel.INFO &&
|
||||
newLevel !== LoggerLevel.ERROR &&
|
||||
newLevel !== LoggerLevel.DEBUG &&
|
||||
newLevel !== LoggerLevel.WARN
|
||||
) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Argument "newLevel" should be one of ${enumToString(LoggerLevel)}`
|
||||
);
|
||||
}
|
||||
|
||||
level = newLevel;
|
||||
}
|
||||
}
|
||||
+797
@@ -0,0 +1,797 @@
|
||||
import type { TcpNetConnectOpts } from 'net';
|
||||
import type { ConnectionOptions as TLSConnectionOptions, TLSSocketOptions } from 'tls';
|
||||
|
||||
import { BSONSerializeOptions, Document, resolveBSONOptions } from './bson';
|
||||
import { ChangeStream, ChangeStreamDocument, ChangeStreamOptions } from './change_stream';
|
||||
import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mongo_credentials';
|
||||
import type { AuthMechanism } from './cmap/auth/providers';
|
||||
import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect';
|
||||
import type { Connection } from './cmap/connection';
|
||||
import type { CompressorName } from './cmap/wire_protocol/compression';
|
||||
import { parseOptions } from './connection_string';
|
||||
import type { MONGO_CLIENT_EVENTS } from './constants';
|
||||
import { Db, DbOptions } from './db';
|
||||
import type { AutoEncrypter, AutoEncryptionOptions } from './deps';
|
||||
import type { Encrypter } from './encrypter';
|
||||
import { MongoInvalidArgumentError } from './error';
|
||||
import type { Logger, LoggerLevel } from './logger';
|
||||
import { TypedEventEmitter } from './mongo_types';
|
||||
import { connect } from './operations/connect';
|
||||
import { PromiseProvider } from './promise_provider';
|
||||
import type { ReadConcern, ReadConcernLevel, ReadConcernLike } from './read_concern';
|
||||
import { ReadPreference, ReadPreferenceMode } from './read_preference';
|
||||
import type { TagSet } from './sdam/server_description';
|
||||
import { readPreferenceServerSelector } from './sdam/server_selection';
|
||||
import type { SrvPoller } from './sdam/srv_polling';
|
||||
import type { Topology, TopologyEvents } from './sdam/topology';
|
||||
import { ClientSession, ClientSessionOptions, ServerSessionPool } from './sessions';
|
||||
import {
|
||||
Callback,
|
||||
ClientMetadata,
|
||||
HostAddress,
|
||||
maybePromise,
|
||||
MongoDBNamespace,
|
||||
ns,
|
||||
resolveOptions
|
||||
} from './utils';
|
||||
import type { W, WriteConcern, WriteConcernSettings } from './write_concern';
|
||||
|
||||
/** @public */
|
||||
export const ServerApiVersion = Object.freeze({
|
||||
v1: '1'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type ServerApiVersion = typeof ServerApiVersion[keyof typeof ServerApiVersion];
|
||||
|
||||
/** @public */
|
||||
export interface ServerApi {
|
||||
version: ServerApiVersion;
|
||||
strict?: boolean;
|
||||
deprecationErrors?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface DriverInfo {
|
||||
name?: string;
|
||||
version?: string;
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Auth {
|
||||
/** The username for auth */
|
||||
username?: string;
|
||||
/** The password for auth */
|
||||
password?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface PkFactory {
|
||||
createPk(): any; // TODO: when js-bson is typed, function should return some BSON type
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type SupportedTLSConnectionOptions = Pick<
|
||||
TLSConnectionOptions,
|
||||
Extract<keyof TLSConnectionOptions, typeof LEGAL_TLS_SOCKET_OPTIONS[number]>
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
export type SupportedTLSSocketOptions = Pick<
|
||||
TLSSocketOptions,
|
||||
Extract<keyof TLSSocketOptions, typeof LEGAL_TLS_SOCKET_OPTIONS[number]>
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
export type SupportedSocketOptions = Pick<
|
||||
TcpNetConnectOpts,
|
||||
typeof LEGAL_TCP_SOCKET_OPTIONS[number]
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
export type SupportedNodeConnectionOptions = SupportedTLSConnectionOptions &
|
||||
SupportedTLSSocketOptions &
|
||||
SupportedSocketOptions;
|
||||
|
||||
/**
|
||||
* Describes all possible URI query options for the mongo client
|
||||
* @public
|
||||
* @see https://docs.mongodb.com/manual/reference/connection-string
|
||||
*/
|
||||
export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeConnectionOptions {
|
||||
/** Specifies the name of the replica set, if the mongod is a member of a replica set. */
|
||||
replicaSet?: string;
|
||||
/** Enables or disables TLS/SSL for the connection. */
|
||||
tls?: boolean;
|
||||
/** A boolean to enable or disables TLS/SSL for the connection. (The ssl option is equivalent to the tls option.) */
|
||||
ssl?: boolean;
|
||||
/** Specifies the location of a local TLS Certificate */
|
||||
tlsCertificateFile?: string;
|
||||
/** Specifies the location of a local .pem file that contains either the client's TLS/SSL certificate and key or only the client's TLS/SSL key when tlsCertificateFile is used to provide the certificate. */
|
||||
tlsCertificateKeyFile?: string;
|
||||
/** Specifies the password to de-crypt the tlsCertificateKeyFile. */
|
||||
tlsCertificateKeyFilePassword?: string;
|
||||
/** Specifies the location of a local .pem file that contains the root certificate chain from the Certificate Authority. This file is used to validate the certificate presented by the mongod/mongos instance. */
|
||||
tlsCAFile?: string;
|
||||
/** Bypasses validation of the certificates presented by the mongod/mongos instance */
|
||||
tlsAllowInvalidCertificates?: boolean;
|
||||
/** Disables hostname validation of the certificate presented by the mongod/mongos instance. */
|
||||
tlsAllowInvalidHostnames?: boolean;
|
||||
/** Disables various certificate validations. */
|
||||
tlsInsecure?: boolean;
|
||||
/** The time in milliseconds to attempt a connection before timing out. */
|
||||
connectTimeoutMS?: number;
|
||||
/** The time in milliseconds to attempt a send or receive on a socket before the attempt times out. */
|
||||
socketTimeoutMS?: number;
|
||||
/** An array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. */
|
||||
compressors?: CompressorName[] | string;
|
||||
/** An integer that specifies the compression level if using zlib for network compression. */
|
||||
zlibCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | undefined;
|
||||
/** The maximum number of hosts to connect to when using an srv connection string, a setting of `0` means unlimited hosts */
|
||||
srvMaxHosts?: number;
|
||||
/**
|
||||
* Modifies the srv URI to look like:
|
||||
*
|
||||
* `_{srvServiceName}._tcp.{hostname}.{domainname}`
|
||||
*
|
||||
* Querying this DNS URI is expected to respond with SRV records
|
||||
*/
|
||||
srvServiceName?: string;
|
||||
/** The maximum number of connections in the connection pool. */
|
||||
maxPoolSize?: number;
|
||||
/** The minimum number of connections in the connection pool. */
|
||||
minPoolSize?: number;
|
||||
/** The maximum number of connections that may be in the process of being established concurrently by the connection pool. */
|
||||
maxConnecting?: number;
|
||||
/** The maximum number of milliseconds that a connection can remain idle in the pool before being removed and closed. */
|
||||
maxIdleTimeMS?: number;
|
||||
/** The maximum time in milliseconds that a thread can wait for a connection to become available. */
|
||||
waitQueueTimeoutMS?: number;
|
||||
/** Specify a read concern for the collection (only MongoDB 3.2 or higher supported) */
|
||||
readConcern?: ReadConcernLike;
|
||||
/** The level of isolation */
|
||||
readConcernLevel?: ReadConcernLevel;
|
||||
/** Specifies the read preferences for this connection */
|
||||
readPreference?: ReadPreferenceMode | ReadPreference;
|
||||
/** Specifies, in seconds, how stale a secondary can be before the client stops using it for read operations. */
|
||||
maxStalenessSeconds?: number;
|
||||
/** Specifies the tags document as a comma-separated list of colon-separated key-value pairs. */
|
||||
readPreferenceTags?: TagSet[];
|
||||
/** The auth settings for when connection to server. */
|
||||
auth?: Auth;
|
||||
/** Specify the database name associated with the user’s credentials. */
|
||||
authSource?: string;
|
||||
/** Specify the authentication mechanism that MongoDB will use to authenticate the connection. */
|
||||
authMechanism?: AuthMechanism;
|
||||
/** Specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. */
|
||||
authMechanismProperties?: AuthMechanismProperties;
|
||||
/** The size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. */
|
||||
localThresholdMS?: number;
|
||||
/** Specifies how long (in milliseconds) to block for server selection before throwing an exception. */
|
||||
serverSelectionTimeoutMS?: number;
|
||||
/** heartbeatFrequencyMS controls when the driver checks the state of the MongoDB deployment. Specify the interval (in milliseconds) between checks, counted from the end of the previous check until the beginning of the next one. */
|
||||
heartbeatFrequencyMS?: number;
|
||||
/** Sets the minimum heartbeat frequency. In the event that the driver has to frequently re-check a server's availability, it will wait at least this long since the previous check to avoid wasted effort. */
|
||||
minHeartbeatFrequencyMS?: number;
|
||||
/** The name of the application that created this MongoClient instance. MongoDB 3.4 and newer will print this value in the server log upon establishing each connection. It is also recorded in the slow query log and profile collections */
|
||||
appName?: string;
|
||||
/** Enables retryable reads. */
|
||||
retryReads?: boolean;
|
||||
/** Enable retryable writes. */
|
||||
retryWrites?: boolean;
|
||||
/** Allow a driver to force a Single topology type with a connection string containing one host */
|
||||
directConnection?: boolean;
|
||||
/** Instruct the driver it is connecting to a load balancer fronting a mongos like service */
|
||||
loadBalanced?: boolean;
|
||||
/**
|
||||
* The write concern w value
|
||||
* @deprecated Please use the `writeConcern` option instead
|
||||
*/
|
||||
w?: W;
|
||||
/**
|
||||
* The write concern timeout
|
||||
* @deprecated Please use the `writeConcern` option instead
|
||||
*/
|
||||
wtimeoutMS?: number;
|
||||
/**
|
||||
* The journal write concern
|
||||
* @deprecated Please use the `writeConcern` option instead
|
||||
*/
|
||||
journal?: boolean;
|
||||
/**
|
||||
* A MongoDB WriteConcern, which describes the level of acknowledgement
|
||||
* requested from MongoDB for write operations.
|
||||
*
|
||||
* @see https://docs.mongodb.com/manual/reference/write-concern/
|
||||
*/
|
||||
writeConcern?: WriteConcern | WriteConcernSettings;
|
||||
/** Validate mongod server certificate against Certificate Authority */
|
||||
sslValidate?: boolean;
|
||||
/** SSL Certificate file path. */
|
||||
sslCA?: string;
|
||||
/** SSL Certificate file path. */
|
||||
sslCert?: string;
|
||||
/** SSL Key file file path. */
|
||||
sslKey?: string;
|
||||
/** SSL Certificate pass phrase. */
|
||||
sslPass?: string;
|
||||
/** SSL Certificate revocation list file path. */
|
||||
sslCRL?: string;
|
||||
/** TCP Connection no delay */
|
||||
noDelay?: boolean;
|
||||
/** TCP Connection keep alive enabled */
|
||||
keepAlive?: boolean;
|
||||
/** The number of milliseconds to wait before initiating keepAlive on the TCP socket */
|
||||
keepAliveInitialDelay?: number;
|
||||
/** Force server to assign `_id` values instead of driver */
|
||||
forceServerObjectId?: boolean;
|
||||
/** Return document results as raw BSON buffers */
|
||||
raw?: boolean;
|
||||
/** A primary key factory function for generation of custom `_id` keys */
|
||||
pkFactory?: PkFactory;
|
||||
/** A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible */
|
||||
promiseLibrary?: any;
|
||||
/** The logging level */
|
||||
loggerLevel?: LoggerLevel;
|
||||
/** Custom logger object */
|
||||
logger?: Logger;
|
||||
/** Enable command monitoring for this client */
|
||||
monitorCommands?: boolean;
|
||||
/** Server API version */
|
||||
serverApi?: ServerApi | ServerApiVersion;
|
||||
/**
|
||||
* Optionally enable client side auto encryption
|
||||
*
|
||||
* @remarks
|
||||
* Automatic encryption is an enterprise only feature that only applies to operations on a collection. Automatic encryption is not supported for operations on a database or view, and operations that are not bypassed will result in error
|
||||
* (see [libmongocrypt: Auto Encryption Allow-List](https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#libmongocrypt-auto-encryption-allow-list)). To bypass automatic encryption for all operations, set bypassAutoEncryption=true in AutoEncryptionOpts.
|
||||
*
|
||||
* Automatic encryption requires the authenticated user to have the [listCollections privilege action](https://docs.mongodb.com/manual/reference/command/listCollections/#dbcmd.listCollections).
|
||||
*
|
||||
* If a MongoClient with a limited connection pool size (i.e a non-zero maxPoolSize) is configured with AutoEncryptionOptions, a separate internal MongoClient is created if any of the following are true:
|
||||
* - AutoEncryptionOptions.keyVaultClient is not passed.
|
||||
* - AutoEncryptionOptions.bypassAutomaticEncryption is false.
|
||||
*
|
||||
* If an internal MongoClient is created, it is configured with the same options as the parent MongoClient except minPoolSize is set to 0 and AutoEncryptionOptions is omitted.
|
||||
*/
|
||||
autoEncryption?: AutoEncryptionOptions;
|
||||
/** Allows a wrapping driver to amend the client metadata generated by the driver to include information about the wrapping driver */
|
||||
driverInfo?: DriverInfo;
|
||||
/** Configures a Socks5 proxy host used for creating TCP connections. */
|
||||
proxyHost?: string;
|
||||
/** Configures a Socks5 proxy port used for creating TCP connections. */
|
||||
proxyPort?: number;
|
||||
/** Configures a Socks5 proxy username when the proxy in proxyHost requires username/password authentication. */
|
||||
proxyUsername?: string;
|
||||
/** Configures a Socks5 proxy password when the proxy in proxyHost requires username/password authentication. */
|
||||
proxyPassword?: string;
|
||||
|
||||
/** @internal */
|
||||
srvPoller?: SrvPoller;
|
||||
/** @internal */
|
||||
connectionType?: typeof Connection;
|
||||
|
||||
/** @internal */
|
||||
[featureFlag: symbol]: any;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type WithSessionCallback = (session: ClientSession) => Promise<any>;
|
||||
|
||||
/** @internal */
|
||||
export interface MongoClientPrivate {
|
||||
url: string;
|
||||
bsonOptions: BSONSerializeOptions;
|
||||
namespace: MongoDBNamespace;
|
||||
hasBeenClosed: boolean;
|
||||
/**
|
||||
* We keep a reference to the sessions that are acquired from the pool.
|
||||
* - used to track and close all sessions in client.close() (which is non-standard behavior)
|
||||
* - used to notify the leak checker in our tests if test author forgot to clean up explicit sessions
|
||||
*/
|
||||
readonly activeSessions: Set<ClientSession>;
|
||||
readonly sessionPool: ServerSessionPool;
|
||||
readonly options: MongoOptions;
|
||||
readonly readConcern?: ReadConcern;
|
||||
readonly writeConcern?: WriteConcern;
|
||||
readonly readPreference: ReadPreference;
|
||||
readonly logger: Logger;
|
||||
readonly isMongoClient: true;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type MongoClientEvents = Pick<TopologyEvents, typeof MONGO_CLIENT_EVENTS[number]> & {
|
||||
// In previous versions the open event emitted a topology, in an effort to no longer
|
||||
// expose internals but continue to expose this useful event API, it now emits a mongoClient
|
||||
open(mongoClient: MongoClient): void;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
const kOptions = Symbol('options');
|
||||
|
||||
/**
|
||||
* The **MongoClient** class is a class that allows for making Connections to MongoDB.
|
||||
* @public
|
||||
*
|
||||
* @remarks
|
||||
* The programmatically provided options take precedence over the URI options.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* // Connect using a MongoClient instance
|
||||
* const MongoClient = require('mongodb').MongoClient;
|
||||
* const test = require('assert');
|
||||
* // Connection url
|
||||
* const url = 'mongodb://localhost:27017';
|
||||
* // Database Name
|
||||
* const dbName = 'test';
|
||||
* // Connect using MongoClient
|
||||
* const mongoClient = new MongoClient(url);
|
||||
* mongoClient.connect(function(err, client) {
|
||||
* const db = client.db(dbName);
|
||||
* client.close();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* // Connect using the MongoClient.connect static method
|
||||
* const MongoClient = require('mongodb').MongoClient;
|
||||
* const test = require('assert');
|
||||
* // Connection url
|
||||
* const url = 'mongodb://localhost:27017';
|
||||
* // Database Name
|
||||
* const dbName = 'test';
|
||||
* // Connect using MongoClient
|
||||
* MongoClient.connect(url, function(err, client) {
|
||||
* const db = client.db(dbName);
|
||||
* client.close();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
|
||||
/** @internal */
|
||||
s: MongoClientPrivate;
|
||||
/** @internal */
|
||||
topology?: Topology;
|
||||
|
||||
/**
|
||||
* The consolidate, parsed, transformed and merged options.
|
||||
* @internal
|
||||
*/
|
||||
[kOptions]: MongoOptions;
|
||||
|
||||
constructor(url: string, options?: MongoClientOptions) {
|
||||
super();
|
||||
|
||||
this[kOptions] = parseOptions(url, this, options);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const client = this;
|
||||
|
||||
// The internal state
|
||||
this.s = {
|
||||
url,
|
||||
bsonOptions: resolveBSONOptions(this[kOptions]),
|
||||
namespace: ns('admin'),
|
||||
hasBeenClosed: false,
|
||||
sessionPool: new ServerSessionPool(this),
|
||||
activeSessions: new Set(),
|
||||
|
||||
get options() {
|
||||
return client[kOptions];
|
||||
},
|
||||
get readConcern() {
|
||||
return client[kOptions].readConcern;
|
||||
},
|
||||
get writeConcern() {
|
||||
return client[kOptions].writeConcern;
|
||||
},
|
||||
get readPreference() {
|
||||
return client[kOptions].readPreference;
|
||||
},
|
||||
get logger() {
|
||||
return client[kOptions].logger;
|
||||
},
|
||||
get isMongoClient(): true {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get options(): Readonly<MongoOptions> {
|
||||
return Object.freeze({ ...this[kOptions] });
|
||||
}
|
||||
|
||||
get serverApi(): Readonly<ServerApi | undefined> {
|
||||
return this[kOptions].serverApi && Object.freeze({ ...this[kOptions].serverApi });
|
||||
}
|
||||
/**
|
||||
* Intended for APM use only
|
||||
* @internal
|
||||
*/
|
||||
get monitorCommands(): boolean {
|
||||
return this[kOptions].monitorCommands;
|
||||
}
|
||||
set monitorCommands(value: boolean) {
|
||||
this[kOptions].monitorCommands = value;
|
||||
}
|
||||
|
||||
get autoEncrypter(): AutoEncrypter | undefined {
|
||||
return this[kOptions].autoEncrypter;
|
||||
}
|
||||
|
||||
get readConcern(): ReadConcern | undefined {
|
||||
return this.s.readConcern;
|
||||
}
|
||||
|
||||
get writeConcern(): WriteConcern | undefined {
|
||||
return this.s.writeConcern;
|
||||
}
|
||||
|
||||
get readPreference(): ReadPreference {
|
||||
return this.s.readPreference;
|
||||
}
|
||||
|
||||
get bsonOptions(): BSONSerializeOptions {
|
||||
return this.s.bsonOptions;
|
||||
}
|
||||
|
||||
get logger(): Logger {
|
||||
return this.s.logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to MongoDB using a url
|
||||
*
|
||||
* @see docs.mongodb.org/manual/reference/connection-string/
|
||||
*/
|
||||
connect(): Promise<this>;
|
||||
connect(callback: Callback<this>): void;
|
||||
connect(callback?: Callback<this>): Promise<this> | void {
|
||||
if (callback && typeof callback !== 'function') {
|
||||
throw new MongoInvalidArgumentError('Method `connect` only accepts a callback');
|
||||
}
|
||||
|
||||
return maybePromise(callback, cb => {
|
||||
connect(this, this[kOptions], err => {
|
||||
if (err) return cb(err);
|
||||
cb(undefined, this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the db and its underlying connections
|
||||
*
|
||||
* @param force - Force close, emitting no events
|
||||
* @param callback - An optional callback, a Promise will be returned if none is provided
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
close(callback: Callback<void>): void;
|
||||
close(force: boolean): Promise<void>;
|
||||
close(force: boolean, callback: Callback<void>): void;
|
||||
close(
|
||||
forceOrCallback?: boolean | Callback<void>,
|
||||
callback?: Callback<void>
|
||||
): Promise<void> | 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;
|
||||
}
|
||||
|
||||
const force = typeof forceOrCallback === 'boolean' ? forceOrCallback : false;
|
||||
|
||||
return maybePromise(callback, callback => {
|
||||
if (this.topology == null) {
|
||||
// Do not connect just to end sessions
|
||||
return callback();
|
||||
}
|
||||
|
||||
const activeSessionEnds = Array.from(this.s.activeSessions, session => session.endSession());
|
||||
this.s.activeSessions.clear();
|
||||
|
||||
Promise.all(activeSessionEnds)
|
||||
.then(() => {
|
||||
if (this.topology == null) {
|
||||
return;
|
||||
}
|
||||
// If we would attempt to select a server and get nothing back we short circuit
|
||||
// to avoid the server selection timeout.
|
||||
const selector = readPreferenceServerSelector(ReadPreference.primaryPreferred);
|
||||
const topologyDescription = this.topology.description;
|
||||
const serverDescriptions = Array.from(topologyDescription.servers.values());
|
||||
const servers = selector(topologyDescription, serverDescriptions);
|
||||
if (servers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endSessions = Array.from(this.s.sessionPool.sessions, ({ id }) => id);
|
||||
if (endSessions.length === 0) return;
|
||||
return this.db('admin')
|
||||
.command(
|
||||
{ endSessions },
|
||||
{ readPreference: ReadPreference.primaryPreferred, noResponse: true }
|
||||
)
|
||||
.catch(() => null); // outcome does not matter
|
||||
})
|
||||
.then(() => {
|
||||
if (this.topology == null) {
|
||||
return;
|
||||
}
|
||||
// clear out references to old topology
|
||||
const topology = this.topology;
|
||||
this.topology = undefined;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
topology.close({ force }, error => {
|
||||
if (error) return reject(error);
|
||||
const { encrypter } = this[kOptions];
|
||||
if (encrypter) {
|
||||
return encrypter.close(this, force, error => {
|
||||
if (error) return reject(error);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(
|
||||
() => callback(),
|
||||
error => callback(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Db instance sharing the current socket connections.
|
||||
*
|
||||
* @param dbName - The name of the database we want to use. If not provided, use database name from connection string.
|
||||
* @param options - Optional settings for Db construction
|
||||
*/
|
||||
db(dbName?: string, options?: DbOptions): Db {
|
||||
options = options ?? {};
|
||||
|
||||
// Default to db from connection string if not provided
|
||||
if (!dbName) {
|
||||
dbName = this.options.dbName;
|
||||
}
|
||||
|
||||
// Copy the options and add out internal override of the not shared flag
|
||||
const finalOptions = Object.assign({}, this[kOptions], options);
|
||||
|
||||
// Return the db object
|
||||
const db = new Db(this, dbName, finalOptions);
|
||||
|
||||
// Return the database
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to MongoDB using a url
|
||||
*
|
||||
* @remarks
|
||||
* The programmatically provided options take precedence over the URI options.
|
||||
*
|
||||
* @see https://docs.mongodb.org/manual/reference/connection-string/
|
||||
*/
|
||||
static connect(url: string): Promise<MongoClient>;
|
||||
static connect(url: string, callback: Callback<MongoClient>): void;
|
||||
static connect(url: string, options: MongoClientOptions): Promise<MongoClient>;
|
||||
static connect(url: string, options: MongoClientOptions, callback: Callback<MongoClient>): void;
|
||||
static connect(
|
||||
url: string,
|
||||
options?: MongoClientOptions | Callback<MongoClient>,
|
||||
callback?: Callback<MongoClient>
|
||||
): Promise<MongoClient> | void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options ?? {};
|
||||
|
||||
try {
|
||||
// Create client
|
||||
const mongoClient = new MongoClient(url, options);
|
||||
// Execute the connect method
|
||||
if (callback) {
|
||||
return mongoClient.connect(callback);
|
||||
} else {
|
||||
return mongoClient.connect();
|
||||
}
|
||||
} catch (error) {
|
||||
if (callback) return callback(error);
|
||||
else return PromiseProvider.get().reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts a new session on the server */
|
||||
startSession(): ClientSession;
|
||||
startSession(options: ClientSessionOptions): ClientSession;
|
||||
startSession(options?: ClientSessionOptions): ClientSession {
|
||||
const session = new ClientSession(
|
||||
this,
|
||||
this.s.sessionPool,
|
||||
{ explicit: true, ...options },
|
||||
this[kOptions]
|
||||
);
|
||||
this.s.activeSessions.add(session);
|
||||
session.once('ended', () => {
|
||||
this.s.activeSessions.delete(session);
|
||||
});
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a given operation with an implicitly created session. The lifetime of the session
|
||||
* will be handled without the need for user interaction.
|
||||
*
|
||||
* NOTE: presently the operation MUST return a Promise (either explicit or implicitly as an async function)
|
||||
*
|
||||
* @param options - Optional settings for the command
|
||||
* @param callback - An callback to execute with an implicitly created session
|
||||
*/
|
||||
withSession(callback: WithSessionCallback): Promise<void>;
|
||||
withSession(options: ClientSessionOptions, callback: WithSessionCallback): Promise<void>;
|
||||
withSession(
|
||||
optionsOrOperation?: ClientSessionOptions | WithSessionCallback,
|
||||
callback?: WithSessionCallback
|
||||
): Promise<void> {
|
||||
const options = {
|
||||
// Always define an owner
|
||||
owner: Symbol(),
|
||||
// If it's an object inherit the options
|
||||
...(typeof optionsOrOperation === 'object' ? optionsOrOperation : {})
|
||||
};
|
||||
|
||||
const withSessionCallback =
|
||||
typeof optionsOrOperation === 'function' ? optionsOrOperation : callback;
|
||||
|
||||
if (withSessionCallback == null) {
|
||||
throw new MongoInvalidArgumentError('Missing required callback parameter');
|
||||
}
|
||||
|
||||
const session = this.startSession(options);
|
||||
const Promise = PromiseProvider.get();
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => withSessionCallback(session))
|
||||
.then(() => {
|
||||
// Do not return the result of callback
|
||||
})
|
||||
.finally(() => {
|
||||
session.endSession().catch(() => null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Change Stream, watching for new changes (insertions, updates,
|
||||
* replacements, deletions, and invalidations) in this cluster. Will ignore all
|
||||
* changes to system collections, as well as the local, admin, and config databases.
|
||||
*
|
||||
* @remarks
|
||||
* watch() accepts two generic arguments for distinct usecases:
|
||||
* - The first is to provide the schema that may be defined for all the data within the current cluster
|
||||
* - The second is to override the shape of the change stream document entirely, if it is not provided the type will default to ChangeStreamDocument of the first argument
|
||||
*
|
||||
* @param pipeline - An array of {@link https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents.
|
||||
* @param options - Optional settings for the command
|
||||
* @typeParam TSchema - Type of the data being detected by the change stream
|
||||
* @typeParam TChange - Type of the whole change stream document emitted
|
||||
*/
|
||||
watch<
|
||||
TSchema extends Document = Document,
|
||||
TChange extends Document = ChangeStreamDocument<TSchema>
|
||||
>(pipeline: Document[] = [], options: ChangeStreamOptions = {}): ChangeStream<TSchema, TChange> {
|
||||
// Allow optionally not specifying a pipeline
|
||||
if (!Array.isArray(pipeline)) {
|
||||
options = pipeline;
|
||||
pipeline = [];
|
||||
}
|
||||
|
||||
return new ChangeStream<TSchema, TChange>(this, pipeline, resolveOptions(this, options));
|
||||
}
|
||||
|
||||
/** Return the mongo client logger */
|
||||
getLogger(): Logger {
|
||||
return this.s.logger;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mongo Client Options
|
||||
* @public
|
||||
*/
|
||||
export interface MongoOptions
|
||||
extends Required<
|
||||
Pick<
|
||||
MongoClientOptions,
|
||||
| 'autoEncryption'
|
||||
| 'connectTimeoutMS'
|
||||
| 'directConnection'
|
||||
| 'driverInfo'
|
||||
| 'forceServerObjectId'
|
||||
| 'minHeartbeatFrequencyMS'
|
||||
| 'heartbeatFrequencyMS'
|
||||
| 'keepAlive'
|
||||
| 'keepAliveInitialDelay'
|
||||
| 'localThresholdMS'
|
||||
| 'logger'
|
||||
| 'maxConnecting'
|
||||
| 'maxIdleTimeMS'
|
||||
| 'maxPoolSize'
|
||||
| 'minPoolSize'
|
||||
| 'monitorCommands'
|
||||
| 'noDelay'
|
||||
| 'pkFactory'
|
||||
| 'promiseLibrary'
|
||||
| 'raw'
|
||||
| 'replicaSet'
|
||||
| 'retryReads'
|
||||
| 'retryWrites'
|
||||
| 'serverSelectionTimeoutMS'
|
||||
| 'socketTimeoutMS'
|
||||
| 'srvMaxHosts'
|
||||
| 'srvServiceName'
|
||||
| 'tlsAllowInvalidCertificates'
|
||||
| 'tlsAllowInvalidHostnames'
|
||||
| 'tlsInsecure'
|
||||
| 'waitQueueTimeoutMS'
|
||||
| 'zlibCompressionLevel'
|
||||
>
|
||||
>,
|
||||
SupportedNodeConnectionOptions {
|
||||
hosts: HostAddress[];
|
||||
srvHost?: string;
|
||||
credentials?: MongoCredentials;
|
||||
readPreference: ReadPreference;
|
||||
readConcern: ReadConcern;
|
||||
loadBalanced: boolean;
|
||||
serverApi: ServerApi;
|
||||
compressors: CompressorName[];
|
||||
writeConcern: WriteConcern;
|
||||
dbName: string;
|
||||
metadata: ClientMetadata;
|
||||
autoEncrypter?: AutoEncrypter;
|
||||
proxyHost?: string;
|
||||
proxyPort?: number;
|
||||
proxyUsername?: string;
|
||||
proxyPassword?: string;
|
||||
/** @internal */
|
||||
connectionType?: typeof Connection;
|
||||
|
||||
/** @internal */
|
||||
encrypter: Encrypter;
|
||||
/** @internal */
|
||||
userSpecifiedAuthSource: boolean;
|
||||
/** @internal */
|
||||
userSpecifiedReplicaSet: boolean;
|
||||
|
||||
/**
|
||||
* # NOTE ABOUT TLS Options
|
||||
*
|
||||
* If set TLS enabled, equivalent to setting the ssl option.
|
||||
*
|
||||
* ### Additional options:
|
||||
*
|
||||
* | nodejs option | MongoDB equivalent | type |
|
||||
* |:---------------------|--------------------------------------------------------- |:---------------------------------------|
|
||||
* | `ca` | `sslCA`, `tlsCAFile` | `string \| Buffer \| Buffer[]` |
|
||||
* | `crl` | `sslCRL` | `string \| Buffer \| Buffer[]` |
|
||||
* | `cert` | `sslCert`, `tlsCertificateFile`, `tlsCertificateKeyFile` | `string \| Buffer \| Buffer[]` |
|
||||
* | `key` | `sslKey`, `tlsCertificateKeyFile` | `string \| Buffer \| KeyObject[]` |
|
||||
* | `passphrase` | `sslPass`, `tlsCertificateKeyFilePassword` | `string` |
|
||||
* | `rejectUnauthorized` | `sslValidate` | `boolean` |
|
||||
*
|
||||
*/
|
||||
tls: boolean;
|
||||
|
||||
/** @internal */
|
||||
[featureFlag: symbol]: any;
|
||||
}
|
||||
+548
@@ -0,0 +1,548 @@
|
||||
import type { ObjectIdLike } from 'bson';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import type {
|
||||
Binary,
|
||||
BSONRegExp,
|
||||
Decimal128,
|
||||
Document,
|
||||
Double,
|
||||
Int32,
|
||||
Long,
|
||||
ObjectId,
|
||||
Timestamp
|
||||
} from './bson';
|
||||
import type { Sort } from './sort';
|
||||
|
||||
/** @internal */
|
||||
export type TODO_NODE_3286 = any;
|
||||
|
||||
/** Given an object shaped type, return the type of the _id field or default to ObjectId @public */
|
||||
export type InferIdType<TSchema> = TSchema extends { _id: infer IdType }
|
||||
? // user has defined a type for _id
|
||||
Record<any, never> extends IdType
|
||||
? never // explicitly forbid empty objects as the type of _id
|
||||
: IdType
|
||||
: TSchema extends { _id?: infer IdType }
|
||||
? // optional _id defined - return ObjectId | IdType
|
||||
unknown extends IdType
|
||||
? ObjectId // infer the _id type as ObjectId if the type of _id is unknown
|
||||
: IdType
|
||||
: ObjectId; // user has not defined _id on schema
|
||||
|
||||
/** Add an _id field to an object shaped type @public */
|
||||
export type WithId<TSchema> = EnhancedOmit<TSchema, '_id'> & { _id: InferIdType<TSchema> };
|
||||
|
||||
/**
|
||||
* Add an optional _id field to an object shaped type
|
||||
* @public
|
||||
*/
|
||||
export type OptionalId<TSchema> = EnhancedOmit<TSchema, '_id'> & { _id?: InferIdType<TSchema> };
|
||||
|
||||
/**
|
||||
* Adds an optional _id field to an object shaped type, unless the _id field is requried on that type.
|
||||
* In the case _id is required, this method continues to require_id.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @privateRemarks
|
||||
* `ObjectId extends TSchema['_id']` is a confusing ordering at first glance. Rather than ask
|
||||
* `TSchema['_id'] extends ObjectId` which translated to "Is the _id property ObjectId?"
|
||||
* we instead ask "Does ObjectId look like (have the same shape) as the _id?"
|
||||
*/
|
||||
export type OptionalUnlessRequiredId<TSchema> = TSchema extends { _id: any }
|
||||
? TSchema
|
||||
: OptionalId<TSchema>;
|
||||
|
||||
/** TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type, and breaks discriminated unions @public */
|
||||
export type EnhancedOmit<TRecordOrUnion, KeyUnion> = string extends keyof TRecordOrUnion
|
||||
? TRecordOrUnion // TRecordOrUnion has indexed type e.g. { _id: string; [k: string]: any; } or it is "any"
|
||||
: TRecordOrUnion extends any
|
||||
? Pick<TRecordOrUnion, Exclude<keyof TRecordOrUnion, KeyUnion>> // discriminated unions
|
||||
: never;
|
||||
|
||||
/** Remove the _id field from an object shaped type @public */
|
||||
export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
|
||||
|
||||
/** A MongoDB filter can be some portion of the schema or a set of operators @public */
|
||||
export type Filter<TSchema> =
|
||||
| Partial<TSchema>
|
||||
| ({
|
||||
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
|
||||
PropertyType<WithId<TSchema>, Property>
|
||||
>;
|
||||
} & RootFilterOperators<WithId<TSchema>>);
|
||||
|
||||
/** @public */
|
||||
export type Condition<T> = AlternativeType<T> | FilterOperators<AlternativeType<T>>;
|
||||
|
||||
/**
|
||||
* It is possible to search using alternative types in mongodb e.g.
|
||||
* string types can be searched using a regex in mongo
|
||||
* array types can be searched using their element type
|
||||
* @public
|
||||
*/
|
||||
export type AlternativeType<T> = T extends ReadonlyArray<infer U>
|
||||
? T | RegExpOrString<U>
|
||||
: RegExpOrString<T>;
|
||||
|
||||
/** @public */
|
||||
export type RegExpOrString<T> = T extends string ? BSONRegExp | RegExp | T : T;
|
||||
|
||||
/** @public */
|
||||
export interface RootFilterOperators<TSchema> extends Document {
|
||||
$and?: Filter<TSchema>[];
|
||||
$nor?: Filter<TSchema>[];
|
||||
$or?: Filter<TSchema>[];
|
||||
$text?: {
|
||||
$search: string;
|
||||
$language?: string;
|
||||
$caseSensitive?: boolean;
|
||||
$diacriticSensitive?: boolean;
|
||||
};
|
||||
$where?: string | ((this: TSchema) => boolean);
|
||||
$comment?: string | Document;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A type that extends Document but forbids anything that "looks like" an object id.
|
||||
*/
|
||||
export type NonObjectIdLikeDocument = {
|
||||
[key in keyof ObjectIdLike]?: never;
|
||||
} & Document;
|
||||
|
||||
/** @public */
|
||||
export interface FilterOperators<TValue> extends NonObjectIdLikeDocument {
|
||||
// Comparison
|
||||
$eq?: TValue;
|
||||
$gt?: TValue;
|
||||
$gte?: TValue;
|
||||
$in?: ReadonlyArray<TValue>;
|
||||
$lt?: TValue;
|
||||
$lte?: TValue;
|
||||
$ne?: TValue;
|
||||
$nin?: ReadonlyArray<TValue>;
|
||||
// Logical
|
||||
$not?: TValue extends string ? FilterOperators<TValue> | RegExp : FilterOperators<TValue>;
|
||||
// Element
|
||||
/**
|
||||
* When `true`, `$exists` matches the documents that contain the field,
|
||||
* including documents where the field value is null.
|
||||
*/
|
||||
$exists?: boolean;
|
||||
$type?: BSONType | BSONTypeAlias;
|
||||
// Evaluation
|
||||
$expr?: Record<string, any>;
|
||||
$jsonSchema?: Record<string, any>;
|
||||
$mod?: TValue extends number ? [number, number] : never;
|
||||
$regex?: TValue extends string ? RegExp | BSONRegExp | string : never;
|
||||
$options?: TValue extends string ? string : never;
|
||||
// Geospatial
|
||||
$geoIntersects?: { $geometry: Document };
|
||||
$geoWithin?: Document;
|
||||
$near?: Document;
|
||||
$nearSphere?: Document;
|
||||
$maxDistance?: number;
|
||||
// Array
|
||||
$all?: ReadonlyArray<any>;
|
||||
$elemMatch?: Document;
|
||||
$size?: TValue extends ReadonlyArray<any> ? number : never;
|
||||
// Bitwise
|
||||
$bitsAllClear?: BitwiseFilter;
|
||||
$bitsAllSet?: BitwiseFilter;
|
||||
$bitsAnyClear?: BitwiseFilter;
|
||||
$bitsAnySet?: BitwiseFilter;
|
||||
$rand?: Record<string, never>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type BitwiseFilter =
|
||||
| number /** numeric bit mask */
|
||||
| Binary /** BinData bit mask */
|
||||
| ReadonlyArray<number>; /** `[ <position1>, <position2>, ... ]` */
|
||||
|
||||
/** @public */
|
||||
export const BSONType = Object.freeze({
|
||||
double: 1,
|
||||
string: 2,
|
||||
object: 3,
|
||||
array: 4,
|
||||
binData: 5,
|
||||
undefined: 6,
|
||||
objectId: 7,
|
||||
bool: 8,
|
||||
date: 9,
|
||||
null: 10,
|
||||
regex: 11,
|
||||
dbPointer: 12,
|
||||
javascript: 13,
|
||||
symbol: 14,
|
||||
javascriptWithScope: 15,
|
||||
int: 16,
|
||||
timestamp: 17,
|
||||
long: 18,
|
||||
decimal: 19,
|
||||
minKey: -1,
|
||||
maxKey: 127
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type BSONType = typeof BSONType[keyof typeof BSONType];
|
||||
/** @public */
|
||||
export type BSONTypeAlias = keyof typeof BSONType;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Projection is flexible to permit the wide array of aggregation operators
|
||||
* @deprecated since v4.1.0: Since projections support all aggregation operations we have no plans to narrow this type further
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type Projection<TSchema extends Document = Document> = Document;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated since v4.1.0: Since projections support all aggregation operations we have no plans to narrow this type further
|
||||
*/
|
||||
export type ProjectionOperators = Document;
|
||||
|
||||
/** @public */
|
||||
export type IsAny<Type, ResultIfAny, ResultIfNotAny> = true extends false & Type
|
||||
? ResultIfAny
|
||||
: ResultIfNotAny;
|
||||
|
||||
/** @public */
|
||||
export type Flatten<Type> = Type extends ReadonlyArray<infer Item> ? Item : Type;
|
||||
|
||||
/** @public */
|
||||
export type ArrayElement<Type> = Type extends ReadonlyArray<infer Item> ? Item : never;
|
||||
|
||||
/** @public */
|
||||
export type SchemaMember<T, V> = { [P in keyof T]?: V } | { [key: string]: V };
|
||||
|
||||
/** @public */
|
||||
export type IntegerType = number | Int32 | Long;
|
||||
|
||||
/** @public */
|
||||
export type NumericType = IntegerType | Decimal128 | Double;
|
||||
|
||||
/** @public */
|
||||
export type FilterOperations<T> = T extends Record<string, any>
|
||||
? { [key in keyof T]?: FilterOperators<T[key]> }
|
||||
: FilterOperators<T>;
|
||||
|
||||
/** @public */
|
||||
export type KeysOfAType<TSchema, Type> = {
|
||||
[key in keyof TSchema]: NonNullable<TSchema[key]> extends Type ? key : never;
|
||||
}[keyof TSchema];
|
||||
|
||||
/** @public */
|
||||
export type KeysOfOtherType<TSchema, Type> = {
|
||||
[key in keyof TSchema]: NonNullable<TSchema[key]> extends Type ? never : key;
|
||||
}[keyof TSchema];
|
||||
|
||||
/** @public */
|
||||
export type AcceptedFields<TSchema, FieldType, AssignableType> = {
|
||||
readonly [key in KeysOfAType<TSchema, FieldType>]?: AssignableType;
|
||||
};
|
||||
|
||||
/** It avoids using fields with not acceptable types @public */
|
||||
export type NotAcceptedFields<TSchema, FieldType> = {
|
||||
readonly [key in KeysOfOtherType<TSchema, FieldType>]?: never;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type OnlyFieldsOfType<TSchema, FieldType = any, AssignableType = FieldType> = IsAny<
|
||||
TSchema[keyof TSchema],
|
||||
Record<string, FieldType>,
|
||||
AcceptedFields<TSchema, FieldType, AssignableType> &
|
||||
NotAcceptedFields<TSchema, FieldType> &
|
||||
Record<string, AssignableType>
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
export type MatchKeysAndValues<TSchema> = Readonly<
|
||||
{
|
||||
[Property in Join<NestedPaths<TSchema>, '.'>]?: PropertyType<TSchema, Property>;
|
||||
} & {
|
||||
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
|
||||
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
|
||||
>;
|
||||
} & {
|
||||
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
|
||||
| `[${string}]`
|
||||
| ''}.${string}`]?: any; // Could be further narrowed
|
||||
}
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
export type AddToSetOperators<Type> = {
|
||||
$each?: Array<Flatten<Type>>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ArrayOperator<Type> = {
|
||||
$each?: Array<Flatten<Type>>;
|
||||
$slice?: number;
|
||||
$position?: number;
|
||||
$sort?: Sort;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type SetFields<TSchema> = ({
|
||||
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any> | undefined>]?:
|
||||
| OptionalId<Flatten<TSchema[key]>>
|
||||
| AddToSetOperators<Array<OptionalId<Flatten<TSchema[key]>>>>;
|
||||
} & NotAcceptedFields<TSchema, ReadonlyArray<any> | undefined>) & {
|
||||
readonly [key: string]: AddToSetOperators<any> | any;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type PushOperator<TSchema> = ({
|
||||
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any>>]?:
|
||||
| Flatten<TSchema[key]>
|
||||
| ArrayOperator<Array<Flatten<TSchema[key]>>>;
|
||||
} & NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
|
||||
readonly [key: string]: ArrayOperator<any> | any;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type PullOperator<TSchema> = ({
|
||||
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any>>]?:
|
||||
| Partial<Flatten<TSchema[key]>>
|
||||
| FilterOperations<Flatten<TSchema[key]>>;
|
||||
} & NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
|
||||
readonly [key: string]: FilterOperators<any> | any;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type PullAllOperator<TSchema> = ({
|
||||
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any>>]?: TSchema[key];
|
||||
} & NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
|
||||
readonly [key: string]: ReadonlyArray<any>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type UpdateFilter<TSchema> = {
|
||||
$currentDate?: OnlyFieldsOfType<
|
||||
TSchema,
|
||||
Date | Timestamp,
|
||||
true | { $type: 'date' | 'timestamp' }
|
||||
>;
|
||||
$inc?: OnlyFieldsOfType<TSchema, NumericType | undefined>;
|
||||
$min?: MatchKeysAndValues<TSchema>;
|
||||
$max?: MatchKeysAndValues<TSchema>;
|
||||
$mul?: OnlyFieldsOfType<TSchema, NumericType | undefined>;
|
||||
$rename?: Record<string, string>;
|
||||
$set?: MatchKeysAndValues<TSchema>;
|
||||
$setOnInsert?: MatchKeysAndValues<TSchema>;
|
||||
$unset?: OnlyFieldsOfType<TSchema, any, '' | true | 1>;
|
||||
$addToSet?: SetFields<TSchema>;
|
||||
$pop?: OnlyFieldsOfType<TSchema, ReadonlyArray<any>, 1 | -1>;
|
||||
$pull?: PullOperator<TSchema>;
|
||||
$push?: PushOperator<TSchema>;
|
||||
$pullAll?: PullAllOperator<TSchema>;
|
||||
$bit?: OnlyFieldsOfType<
|
||||
TSchema,
|
||||
NumericType | undefined,
|
||||
{ and: IntegerType } | { or: IntegerType } | { xor: IntegerType }
|
||||
>;
|
||||
} & Document;
|
||||
|
||||
/** @public */
|
||||
export type Nullable<AnyType> = AnyType | null | undefined;
|
||||
|
||||
/** @public */
|
||||
export type OneOrMore<T> = T | ReadonlyArray<T>;
|
||||
|
||||
/** @public */
|
||||
export type GenericListener = (...args: any[]) => void;
|
||||
|
||||
/**
|
||||
* Event description type
|
||||
* @public
|
||||
*/
|
||||
export type EventsDescription = Record<string, GenericListener>;
|
||||
|
||||
/** @public */
|
||||
export type CommonEvents = 'newListener' | 'removeListener';
|
||||
|
||||
/**
|
||||
* Typescript type safe event emitter
|
||||
* @public
|
||||
*/
|
||||
export declare interface TypedEventEmitter<Events extends EventsDescription> extends EventEmitter {
|
||||
addListener<EventKey extends keyof Events>(event: EventKey, listener: Events[EventKey]): this;
|
||||
addListener(
|
||||
event: CommonEvents,
|
||||
listener: (eventName: string | symbol, listener: GenericListener) => void
|
||||
): this;
|
||||
addListener(event: string | symbol, listener: GenericListener): this;
|
||||
|
||||
on<EventKey extends keyof Events>(event: EventKey, listener: Events[EventKey]): this;
|
||||
on(
|
||||
event: CommonEvents,
|
||||
listener: (eventName: string | symbol, listener: GenericListener) => void
|
||||
): this;
|
||||
on(event: string | symbol, listener: GenericListener): this;
|
||||
|
||||
once<EventKey extends keyof Events>(event: EventKey, listener: Events[EventKey]): this;
|
||||
once(
|
||||
event: CommonEvents,
|
||||
listener: (eventName: string | symbol, listener: GenericListener) => void
|
||||
): this;
|
||||
once(event: string | symbol, listener: GenericListener): this;
|
||||
|
||||
removeListener<EventKey extends keyof Events>(event: EventKey, listener: Events[EventKey]): this;
|
||||
removeListener(
|
||||
event: CommonEvents,
|
||||
listener: (eventName: string | symbol, listener: GenericListener) => void
|
||||
): this;
|
||||
removeListener(event: string | symbol, listener: GenericListener): this;
|
||||
|
||||
off<EventKey extends keyof Events>(event: EventKey, listener: Events[EventKey]): this;
|
||||
off(
|
||||
event: CommonEvents,
|
||||
listener: (eventName: string | symbol, listener: GenericListener) => void
|
||||
): this;
|
||||
off(event: string | symbol, listener: GenericListener): this;
|
||||
|
||||
removeAllListeners<EventKey extends keyof Events>(
|
||||
event?: EventKey | CommonEvents | symbol | string
|
||||
): this;
|
||||
|
||||
listeners<EventKey extends keyof Events>(
|
||||
event: EventKey | CommonEvents | symbol | string
|
||||
): Events[EventKey][];
|
||||
|
||||
rawListeners<EventKey extends keyof Events>(
|
||||
event: EventKey | CommonEvents | symbol | string
|
||||
): Events[EventKey][];
|
||||
|
||||
emit<EventKey extends keyof Events>(
|
||||
event: EventKey | symbol,
|
||||
...args: Parameters<Events[EventKey]>
|
||||
): boolean;
|
||||
|
||||
listenerCount<EventKey extends keyof Events>(
|
||||
type: EventKey | CommonEvents | symbol | string
|
||||
): number;
|
||||
|
||||
prependListener<EventKey extends keyof Events>(event: EventKey, listener: Events[EventKey]): this;
|
||||
prependListener(
|
||||
event: CommonEvents,
|
||||
listener: (eventName: string | symbol, listener: GenericListener) => void
|
||||
): this;
|
||||
prependListener(event: string | symbol, listener: GenericListener): this;
|
||||
|
||||
prependOnceListener<EventKey extends keyof Events>(
|
||||
event: EventKey,
|
||||
listener: Events[EventKey]
|
||||
): this;
|
||||
prependOnceListener(
|
||||
event: CommonEvents,
|
||||
listener: (eventName: string | symbol, listener: GenericListener) => void
|
||||
): this;
|
||||
prependOnceListener(event: string | symbol, listener: GenericListener): this;
|
||||
|
||||
eventNames(): string[];
|
||||
getMaxListeners(): number;
|
||||
setMaxListeners(n: number): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typescript type safe event emitter
|
||||
* @public
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class TypedEventEmitter<Events extends EventsDescription> extends EventEmitter {}
|
||||
|
||||
/** @public */
|
||||
export class CancellationToken extends TypedEventEmitter<{ cancel(): void }> {}
|
||||
|
||||
/**
|
||||
* Helper types for dot-notation filter attributes
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export type Join<T extends unknown[], D extends string> = T extends []
|
||||
? ''
|
||||
: T extends [string | number]
|
||||
? `${T[0]}`
|
||||
: T extends [string | number, ...infer R]
|
||||
? `${T[0]}${D}${Join<R, D>}`
|
||||
: string;
|
||||
|
||||
/** @public */
|
||||
export type PropertyType<Type, Property extends string> = string extends Property
|
||||
? unknown
|
||||
: Property extends keyof Type
|
||||
? Type[Property]
|
||||
: Property extends `${number}`
|
||||
? Type extends ReadonlyArray<infer ArrayType>
|
||||
? ArrayType
|
||||
: unknown
|
||||
: Property extends `${infer Key}.${infer Rest}`
|
||||
? Key extends `${number}`
|
||||
? Type extends ReadonlyArray<infer ArrayType>
|
||||
? PropertyType<ArrayType, Rest>
|
||||
: unknown
|
||||
: Key extends keyof Type
|
||||
? Type[Key] extends Map<string, infer MapType>
|
||||
? MapType
|
||||
: PropertyType<Type[Key], Rest>
|
||||
: unknown
|
||||
: unknown;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* returns tuple of strings (keys to be joined on '.') that represent every path into a schema
|
||||
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
|
||||
*/
|
||||
export type NestedPaths<Type> = Type extends
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Date
|
||||
| RegExp
|
||||
| Buffer
|
||||
| Uint8Array
|
||||
| ((...args: any[]) => any)
|
||||
| { _bsontype: string }
|
||||
? []
|
||||
: Type extends ReadonlyArray<infer ArrayType>
|
||||
? [] | [number, ...NestedPaths<ArrayType>]
|
||||
: Type extends Map<string, any>
|
||||
? [string]
|
||||
: Type extends object
|
||||
? {
|
||||
[Key in Extract<keyof Type, string>]: Type[Key] extends Type // type of value extends the parent
|
||||
? [Key]
|
||||
: // for a recursive union type, the child will never extend the parent type.
|
||||
// but the parent will still extend the child
|
||||
Type extends Type[Key]
|
||||
? [Key]
|
||||
: Type[Key] extends ReadonlyArray<infer ArrayType> // handling recursive types with arrays
|
||||
? Type extends ArrayType // is the type of the parent the same as the type of the array?
|
||||
? [Key] // yes, it's a recursive array type
|
||||
: // for unions, the child type extends the parent
|
||||
ArrayType extends Type
|
||||
? [Key] // we have a recursive array union
|
||||
: // child is an array, but it's not a recursive array
|
||||
[Key, ...NestedPaths<Type[Key]>]
|
||||
: // child is not structured the same as the parent
|
||||
[Key, ...NestedPaths<Type[Key]>] | [Key];
|
||||
}[Extract<keyof Type, string>]
|
||||
: [];
|
||||
|
||||
/**
|
||||
* @public
|
||||
* returns keys (strings) for every path into a schema with a value of type
|
||||
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
|
||||
*/
|
||||
export type NestedPathsOfType<TSchema, Type> = KeysOfAType<
|
||||
{
|
||||
[Property in Join<NestedPaths<TSchema>, '.'>]: PropertyType<TSchema, Property>;
|
||||
},
|
||||
Type
|
||||
>;
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import type { Document } from '../bson';
|
||||
import type { Db } from '../db';
|
||||
import { MongoInvalidArgumentError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, emitWarningOnce, getTopology } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface RoleSpecification {
|
||||
/**
|
||||
* A role grants privileges to perform sets of actions on defined resources.
|
||||
* A given role applies to the database on which it is defined and can grant access down to a collection level of granularity.
|
||||
*/
|
||||
role: string;
|
||||
/** The database this user's role should effect. */
|
||||
db: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface AddUserOptions extends CommandOperationOptions {
|
||||
/** @deprecated Please use db.command('createUser', ...) instead for this option */
|
||||
digestPassword?: null;
|
||||
/** Roles associated with the created user */
|
||||
roles?: string | string[] | RoleSpecification | RoleSpecification[];
|
||||
/** Custom data associated with the user (only Mongodb 2.6 or higher) */
|
||||
customData?: Document;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class AddUserOperation extends CommandOperation<Document> {
|
||||
override options: AddUserOptions;
|
||||
db: Db;
|
||||
username: string;
|
||||
password?: string;
|
||||
|
||||
constructor(db: Db, username: string, password: string | undefined, options?: AddUserOptions) {
|
||||
super(db, options);
|
||||
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.options = options ?? {};
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const db = this.db;
|
||||
const username = this.username;
|
||||
const password = this.password;
|
||||
const options = this.options;
|
||||
|
||||
// Error out if digestPassword set
|
||||
if (options.digestPassword != null) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError(
|
||||
'Option "digestPassword" not supported via addUser, use db.command(...) instead'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let roles;
|
||||
if (!options.roles || (Array.isArray(options.roles) && options.roles.length === 0)) {
|
||||
emitWarningOnce(
|
||||
'Creating a user without roles is deprecated. Defaults to "root" if db is "admin" or "dbOwner" otherwise'
|
||||
);
|
||||
if (db.databaseName.toLowerCase() === 'admin') {
|
||||
roles = ['root'];
|
||||
} else {
|
||||
roles = ['dbOwner'];
|
||||
}
|
||||
} else {
|
||||
roles = Array.isArray(options.roles) ? options.roles : [options.roles];
|
||||
}
|
||||
|
||||
let topology;
|
||||
try {
|
||||
topology = getTopology(db);
|
||||
} catch (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
const digestPassword = topology.lastHello().maxWireVersion >= 7;
|
||||
|
||||
let userPassword = password;
|
||||
|
||||
if (!digestPassword) {
|
||||
// Use node md5 generator
|
||||
const md5 = crypto.createHash('md5');
|
||||
// Generate keys used for authentication
|
||||
md5.update(`${username}:mongo:${password}`);
|
||||
userPassword = md5.digest('hex');
|
||||
}
|
||||
|
||||
// Build the command to execute
|
||||
const command: Document = {
|
||||
createUser: username,
|
||||
customData: options.customData || {},
|
||||
roles: roles,
|
||||
digestPassword
|
||||
};
|
||||
|
||||
// No password
|
||||
if (typeof password === 'string') {
|
||||
command.pwd = userPassword;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(AddUserOperation, [Aspect.WRITE_OPERATION]);
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import type { Document } from '../bson';
|
||||
import { MongoInvalidArgumentError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, maxWireVersion, MongoDBNamespace } from '../utils';
|
||||
import { CollationOptions, CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects, Hint } from './operation';
|
||||
|
||||
/** @internal */
|
||||
export const DB_AGGREGATE_COLLECTION = 1 as const;
|
||||
const MIN_WIRE_VERSION_$OUT_READ_CONCERN_SUPPORT = 8 as const;
|
||||
|
||||
/** @public */
|
||||
export interface AggregateOptions extends CommandOperationOptions {
|
||||
/** allowDiskUse lets the server know if it can use disk to store temporary results for the aggregation (requires mongodb 2.6 \>). */
|
||||
allowDiskUse?: boolean;
|
||||
/** The number of documents to return per batch. See [aggregation documentation](https://docs.mongodb.com/manual/reference/command/aggregate). */
|
||||
batchSize?: number;
|
||||
/** Allow driver to bypass schema validation in MongoDB 3.2 or higher. */
|
||||
bypassDocumentValidation?: boolean;
|
||||
/** Return the query as cursor, on 2.6 \> it returns as a real cursor on pre 2.6 it returns as an emulated cursor. */
|
||||
cursor?: Document;
|
||||
/** specifies a cumulative time limit in milliseconds for processing operations on the cursor. MongoDB interrupts the operation at the earliest following interrupt point. */
|
||||
maxTimeMS?: number;
|
||||
/** The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. */
|
||||
maxAwaitTimeMS?: number;
|
||||
/** Specify collation. */
|
||||
collation?: CollationOptions;
|
||||
/** Add an index selection hint to an aggregation command */
|
||||
hint?: Hint;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
|
||||
out?: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class AggregateOperation<T = Document> extends CommandOperation<T> {
|
||||
override options: AggregateOptions;
|
||||
target: string | typeof DB_AGGREGATE_COLLECTION;
|
||||
pipeline: Document[];
|
||||
hasWriteStage: boolean;
|
||||
|
||||
constructor(ns: MongoDBNamespace, pipeline: Document[], options?: AggregateOptions) {
|
||||
super(undefined, { ...options, dbName: ns.db });
|
||||
|
||||
this.options = options ?? {};
|
||||
|
||||
// Covers when ns.collection is null, undefined or the empty string, use DB_AGGREGATE_COLLECTION
|
||||
this.target = ns.collection || DB_AGGREGATE_COLLECTION;
|
||||
|
||||
this.pipeline = pipeline;
|
||||
|
||||
// determine if we have a write stage, override read preference if so
|
||||
this.hasWriteStage = false;
|
||||
if (typeof options?.out === 'string') {
|
||||
this.pipeline = this.pipeline.concat({ $out: options.out });
|
||||
this.hasWriteStage = true;
|
||||
} else if (pipeline.length > 0) {
|
||||
const finalStage = pipeline[pipeline.length - 1];
|
||||
if (finalStage.$out || finalStage.$merge) {
|
||||
this.hasWriteStage = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasWriteStage) {
|
||||
this.trySecondaryWrite = true;
|
||||
}
|
||||
|
||||
if (this.explain && this.writeConcern) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
'Option "explain" cannot be used on an aggregate call with writeConcern'
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.cursor != null && typeof options.cursor !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Cursor options must be an object');
|
||||
}
|
||||
}
|
||||
|
||||
override get canRetryRead(): boolean {
|
||||
return !this.hasWriteStage;
|
||||
}
|
||||
|
||||
addToPipeline(stage: Document): void {
|
||||
this.pipeline.push(stage);
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<T>
|
||||
): void {
|
||||
const options: AggregateOptions = this.options;
|
||||
const serverWireVersion = maxWireVersion(server);
|
||||
const command: Document = { aggregate: this.target, pipeline: this.pipeline };
|
||||
|
||||
if (this.hasWriteStage && serverWireVersion < MIN_WIRE_VERSION_$OUT_READ_CONCERN_SUPPORT) {
|
||||
this.readConcern = undefined;
|
||||
}
|
||||
|
||||
if (serverWireVersion >= 5) {
|
||||
if (this.hasWriteStage && this.writeConcern) {
|
||||
Object.assign(command, { writeConcern: this.writeConcern });
|
||||
}
|
||||
}
|
||||
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
command.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
if (typeof options.allowDiskUse === 'boolean') {
|
||||
command.allowDiskUse = options.allowDiskUse;
|
||||
}
|
||||
|
||||
if (options.hint) {
|
||||
command.hint = options.hint;
|
||||
}
|
||||
|
||||
if (options.let) {
|
||||
command.let = options.let;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (options.comment !== undefined) {
|
||||
command.comment = options.comment;
|
||||
}
|
||||
|
||||
command.cursor = options.cursor || {};
|
||||
if (options.batchSize && !this.hasWriteStage) {
|
||||
command.cursor.batchSize = options.batchSize;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(AggregateOperation, [
|
||||
Aspect.READ_OPERATION,
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.EXPLAINABLE,
|
||||
Aspect.CURSOR_CREATING
|
||||
]);
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import type {
|
||||
AnyBulkWriteOperation,
|
||||
BulkOperationBase,
|
||||
BulkWriteOptions,
|
||||
BulkWriteResult
|
||||
} from '../bulk/common';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { AbstractOperation, Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @internal */
|
||||
export class BulkWriteOperation extends AbstractOperation<BulkWriteResult> {
|
||||
override options: BulkWriteOptions;
|
||||
collection: Collection;
|
||||
operations: AnyBulkWriteOperation[];
|
||||
|
||||
constructor(
|
||||
collection: Collection,
|
||||
operations: AnyBulkWriteOperation[],
|
||||
options: BulkWriteOptions
|
||||
) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<BulkWriteResult>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
const operations = this.operations;
|
||||
const options = { ...this.options, ...this.bsonOptions, readPreference: this.readPreference };
|
||||
|
||||
// Create the bulk operation
|
||||
const bulk: BulkOperationBase =
|
||||
options.ordered === false
|
||||
? coll.initializeUnorderedBulkOp(options)
|
||||
: coll.initializeOrderedBulkOp(options);
|
||||
|
||||
// for each op go through and add to the bulk
|
||||
try {
|
||||
for (let i = 0; i < operations.length; i++) {
|
||||
bulk.raw(operations[i]);
|
||||
}
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Execute the bulk
|
||||
bulk.execute({ ...options, session }, (err, r) => {
|
||||
// We have connection level error
|
||||
if (!r && err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Return the results
|
||||
callback(undefined, r);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(BulkWriteOperation, [Aspect.WRITE_OPERATION]);
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { Collection } from '../collection';
|
||||
import type { Db } from '../db';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { AbstractOperation, OperationOptions } from './operation';
|
||||
|
||||
export interface CollectionsOptions extends OperationOptions {
|
||||
nameOnly?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class CollectionsOperation extends AbstractOperation<Collection[]> {
|
||||
override options: CollectionsOptions;
|
||||
db: Db;
|
||||
|
||||
constructor(db: Db, options: CollectionsOptions) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Collection[]>
|
||||
): void {
|
||||
const db = this.db;
|
||||
|
||||
// Let's get the collection names
|
||||
db.listCollections(
|
||||
{},
|
||||
{ ...this.options, nameOnly: true, readPreference: this.readPreference, session }
|
||||
).toArray((err, documents) => {
|
||||
if (err || !documents) return callback(err);
|
||||
// Filter collections removing any illegal ones
|
||||
documents = documents.filter(doc => doc.name.indexOf('$') === -1);
|
||||
|
||||
// Return the collection objects
|
||||
callback(
|
||||
undefined,
|
||||
documents.map(d => {
|
||||
return new Collection(db, d.name, db.s.options);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
import type { BSONSerializeOptions, Document } from '../bson';
|
||||
import { MongoCompatibilityError, MongoInvalidArgumentError } from '../error';
|
||||
import { Explain, ExplainOptions } from '../explain';
|
||||
import type { Logger } from '../logger';
|
||||
import { ReadConcern } from '../read_concern';
|
||||
import type { ReadPreference } from '../read_preference';
|
||||
import type { Server } from '../sdam/server';
|
||||
import { MIN_SECONDARY_WRITE_WIRE_VERSION } from '../sdam/server_selection';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import {
|
||||
Callback,
|
||||
commandSupportsReadConcern,
|
||||
decorateWithExplain,
|
||||
maxWireVersion,
|
||||
MongoDBNamespace
|
||||
} from '../utils';
|
||||
import { WriteConcern, WriteConcernOptions } from '../write_concern';
|
||||
import type { ReadConcernLike } from './../read_concern';
|
||||
import { AbstractOperation, Aspect, OperationOptions } from './operation';
|
||||
|
||||
const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5;
|
||||
|
||||
/** @public */
|
||||
export interface CollationOptions {
|
||||
locale: string;
|
||||
caseLevel?: boolean;
|
||||
caseFirst?: string;
|
||||
strength?: number;
|
||||
numericOrdering?: boolean;
|
||||
alternate?: string;
|
||||
maxVariable?: string;
|
||||
backwards?: boolean;
|
||||
normalization?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CommandOperationOptions
|
||||
extends OperationOptions,
|
||||
WriteConcernOptions,
|
||||
ExplainOptions {
|
||||
/** @deprecated This option does nothing */
|
||||
fullResponse?: boolean;
|
||||
/** Specify a read concern and level for the collection. (only MongoDB 3.2 or higher supported) */
|
||||
readConcern?: ReadConcernLike;
|
||||
/** Collation */
|
||||
collation?: CollationOptions;
|
||||
maxTimeMS?: number;
|
||||
/**
|
||||
* Comment to apply to the operation.
|
||||
*
|
||||
* In server versions pre-4.4, 'comment' must be string. A server
|
||||
* error will be thrown if any other type is provided.
|
||||
*
|
||||
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
|
||||
*/
|
||||
comment?: unknown;
|
||||
/** Should retry failed writes */
|
||||
retryWrites?: boolean;
|
||||
|
||||
// Admin command overrides.
|
||||
dbName?: string;
|
||||
authdb?: string;
|
||||
noResponse?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface OperationParent {
|
||||
s: { namespace: MongoDBNamespace };
|
||||
readConcern?: ReadConcern;
|
||||
writeConcern?: WriteConcern;
|
||||
readPreference?: ReadPreference;
|
||||
logger?: Logger;
|
||||
bsonOptions?: BSONSerializeOptions;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export abstract class CommandOperation<T> extends AbstractOperation<T> {
|
||||
override options: CommandOperationOptions;
|
||||
readConcern?: ReadConcern;
|
||||
writeConcern?: WriteConcern;
|
||||
explain?: Explain;
|
||||
logger?: Logger;
|
||||
|
||||
constructor(parent?: OperationParent, options?: CommandOperationOptions) {
|
||||
super(options);
|
||||
this.options = options ?? {};
|
||||
|
||||
// NOTE: this was explicitly added for the add/remove user operations, it's likely
|
||||
// something we'd want to reconsider. Perhaps those commands can use `Admin`
|
||||
// as a parent?
|
||||
const dbNameOverride = options?.dbName || options?.authdb;
|
||||
if (dbNameOverride) {
|
||||
this.ns = new MongoDBNamespace(dbNameOverride, '$cmd');
|
||||
} else {
|
||||
this.ns = parent
|
||||
? parent.s.namespace.withCollection('$cmd')
|
||||
: new MongoDBNamespace('admin', '$cmd');
|
||||
}
|
||||
|
||||
this.readConcern = ReadConcern.fromOptions(options);
|
||||
this.writeConcern = WriteConcern.fromOptions(options);
|
||||
|
||||
// TODO(NODE-2056): make logger another "inheritable" property
|
||||
if (parent && parent.logger) {
|
||||
this.logger = parent.logger;
|
||||
}
|
||||
|
||||
if (this.hasAspect(Aspect.EXPLAINABLE)) {
|
||||
this.explain = Explain.fromOptions(options);
|
||||
} else if (options?.explain != null) {
|
||||
throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`);
|
||||
}
|
||||
}
|
||||
|
||||
override get canRetryWrite(): boolean {
|
||||
if (this.hasAspect(Aspect.EXPLAINABLE)) {
|
||||
return this.explain == null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<T>
|
||||
): void;
|
||||
|
||||
executeCommand(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
cmd: Document,
|
||||
callback: Callback
|
||||
): void {
|
||||
// TODO: consider making this a non-enumerable property
|
||||
this.server = server;
|
||||
|
||||
const options = {
|
||||
...this.options,
|
||||
...this.bsonOptions,
|
||||
readPreference: this.readPreference,
|
||||
session
|
||||
};
|
||||
|
||||
const serverWireVersion = maxWireVersion(server);
|
||||
const inTransaction = this.session && this.session.inTransaction();
|
||||
|
||||
if (this.readConcern && commandSupportsReadConcern(cmd) && !inTransaction) {
|
||||
Object.assign(cmd, { readConcern: this.readConcern });
|
||||
}
|
||||
|
||||
if (this.trySecondaryWrite && serverWireVersion < MIN_SECONDARY_WRITE_WIRE_VERSION) {
|
||||
options.omitReadPreference = true;
|
||||
}
|
||||
|
||||
if (options.collation && serverWireVersion < SUPPORTS_WRITE_CONCERN_AND_COLLATION) {
|
||||
callback(
|
||||
new MongoCompatibilityError(
|
||||
`Server ${server.name}, which reports wire version ${serverWireVersion}, does not support collation`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.writeConcern && this.hasAspect(Aspect.WRITE_OPERATION) && !inTransaction) {
|
||||
Object.assign(cmd, { writeConcern: this.writeConcern });
|
||||
}
|
||||
|
||||
if (serverWireVersion >= SUPPORTS_WRITE_CONCERN_AND_COLLATION) {
|
||||
if (
|
||||
options.collation &&
|
||||
typeof options.collation === 'object' &&
|
||||
!this.hasAspect(Aspect.SKIP_COLLATION)
|
||||
) {
|
||||
Object.assign(cmd, { collation: options.collation });
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.maxTimeMS === 'number') {
|
||||
cmd.maxTimeMS = options.maxTimeMS;
|
||||
}
|
||||
|
||||
if (this.hasAspect(Aspect.EXPLAINABLE) && this.explain) {
|
||||
if (serverWireVersion < 6 && cmd.aggregate) {
|
||||
// Prior to 3.6, with aggregate, verbosity is ignored, and we must pass in "explain: true"
|
||||
cmd.explain = true;
|
||||
} else {
|
||||
cmd = decorateWithExplain(cmd, this.explain);
|
||||
}
|
||||
}
|
||||
|
||||
server.command(this.ns, cmd, options, callback);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Db } from '../db';
|
||||
import { MongoTopologyClosedError } from '../error';
|
||||
import type { ReadPreference } from '../read_preference';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, getTopology } from '../utils';
|
||||
|
||||
/** @public */
|
||||
export interface IndexInformationOptions {
|
||||
full?: boolean;
|
||||
readPreference?: ReadPreference;
|
||||
session?: ClientSession;
|
||||
}
|
||||
/**
|
||||
* Retrieves this collections index info.
|
||||
*
|
||||
* @param db - The Db instance on which to retrieve the index info.
|
||||
* @param name - The name of the collection.
|
||||
*/
|
||||
export function indexInformation(db: Db, name: string, callback: Callback): void;
|
||||
export function indexInformation(
|
||||
db: Db,
|
||||
name: string,
|
||||
options: IndexInformationOptions,
|
||||
callback?: Callback
|
||||
): void;
|
||||
export function indexInformation(
|
||||
db: Db,
|
||||
name: string,
|
||||
_optionsOrCallback: IndexInformationOptions | Callback,
|
||||
_callback?: Callback
|
||||
): void {
|
||||
let options = _optionsOrCallback as IndexInformationOptions;
|
||||
let callback = _callback as Callback;
|
||||
if ('function' === typeof _optionsOrCallback) {
|
||||
callback = _optionsOrCallback;
|
||||
options = {};
|
||||
}
|
||||
// If we specified full information
|
||||
const full = options.full == null ? false : options.full;
|
||||
|
||||
let topology;
|
||||
try {
|
||||
topology = getTopology(db);
|
||||
} catch (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// Did the user destroy the topology
|
||||
if (topology.isDestroyed()) return callback(new MongoTopologyClosedError());
|
||||
// Process all the results from the index command and collection
|
||||
function processResults(indexes: any) {
|
||||
// Contains all the information
|
||||
const info: any = {};
|
||||
// Process all the indexes
|
||||
for (let i = 0; i < indexes.length; i++) {
|
||||
const index = indexes[i];
|
||||
// Let's unpack the object
|
||||
info[index.name] = [];
|
||||
for (const name in index.key) {
|
||||
info[index.name].push([name, index.key[name]]);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// Get the list of indexes of the specified collection
|
||||
db.collection(name)
|
||||
.listIndexes(options)
|
||||
.toArray((err, indexes) => {
|
||||
if (err) return callback(err);
|
||||
if (!Array.isArray(indexes)) return callback(undefined, []);
|
||||
if (full) return callback(undefined, indexes);
|
||||
callback(undefined, processResults(indexes));
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareDocs(
|
||||
coll: Collection,
|
||||
docs: Document[],
|
||||
options: { forceServerObjectId?: boolean }
|
||||
): Document[] {
|
||||
const forceServerObjectId =
|
||||
typeof options.forceServerObjectId === 'boolean'
|
||||
? options.forceServerObjectId
|
||||
: coll.s.db.options?.forceServerObjectId;
|
||||
|
||||
// no need to modify the docs if server sets the ObjectId
|
||||
if (forceServerObjectId === true) {
|
||||
return docs;
|
||||
}
|
||||
|
||||
return docs.map(doc => {
|
||||
if (doc._id == null) {
|
||||
doc._id = coll.s.pkFactory.createPk();
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { resolveSRVRecord } from '../connection_string';
|
||||
import { MONGO_CLIENT_EVENTS } from '../constants';
|
||||
import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
|
||||
import type { MongoClient, MongoOptions } from '../mongo_client';
|
||||
import { Topology } from '../sdam/topology';
|
||||
import type { Callback } from '../utils';
|
||||
|
||||
export function connect(
|
||||
mongoClient: MongoClient,
|
||||
options: MongoOptions,
|
||||
callback: Callback<MongoClient>
|
||||
): void {
|
||||
if (!callback) {
|
||||
throw new MongoInvalidArgumentError('Callback function must be provided');
|
||||
}
|
||||
|
||||
// If a connection already been established, we can terminate early
|
||||
if (mongoClient.topology && mongoClient.topology.isConnected()) {
|
||||
return callback(undefined, mongoClient);
|
||||
}
|
||||
|
||||
const logger = mongoClient.logger;
|
||||
const connectCallback: Callback = err => {
|
||||
const warningMessage =
|
||||
'seed list contains no mongos proxies, replicaset connections requires ' +
|
||||
'the parameter replicaSet to be supplied in the URI or options object, ' +
|
||||
'mongodb://server:port/db?replicaSet=name';
|
||||
if (err && err.message === 'no mongos proxies found in seed list') {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn(warningMessage);
|
||||
}
|
||||
|
||||
// Return a more specific error message for MongoClient.connect
|
||||
// TODO(NODE-3483)
|
||||
return callback(new MongoRuntimeError(warningMessage));
|
||||
}
|
||||
|
||||
callback(err, mongoClient);
|
||||
};
|
||||
|
||||
if (typeof options.srvHost === 'string') {
|
||||
return resolveSRVRecord(options, (err, hosts) => {
|
||||
if (err || !hosts) return callback(err);
|
||||
for (const [index, host] of hosts.entries()) {
|
||||
options.hosts[index] = host;
|
||||
}
|
||||
|
||||
return createTopology(mongoClient, options, connectCallback);
|
||||
});
|
||||
}
|
||||
|
||||
return createTopology(mongoClient, options, connectCallback);
|
||||
}
|
||||
|
||||
function createTopology(
|
||||
mongoClient: MongoClient,
|
||||
options: MongoOptions,
|
||||
callback: Callback<Topology>
|
||||
) {
|
||||
// Create the topology
|
||||
const topology = new Topology(options.hosts, options);
|
||||
// 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));
|
||||
|
||||
for (const event of MONGO_CLIENT_EVENTS) {
|
||||
topology.on(event, (...args: any[]) => mongoClient.emit(event, ...(args as any)));
|
||||
}
|
||||
|
||||
// initialize CSFLE if requested
|
||||
if (mongoClient.autoEncrypter) {
|
||||
mongoClient.autoEncrypter.init(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
topology.connect(options, err => {
|
||||
if (err) {
|
||||
topology.close({ force: true });
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
options.encrypter.connectInternalClient(error => {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(undefined, topology);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise connect normally
|
||||
topology.connect(options, err => {
|
||||
if (err) {
|
||||
topology.close({ force: true });
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, topology);
|
||||
return;
|
||||
});
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback, MongoDBNamespace } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface CountOptions extends CommandOperationOptions {
|
||||
/** The number of documents to skip. */
|
||||
skip?: number;
|
||||
/** The maximum amounts to count before aborting. */
|
||||
limit?: number;
|
||||
/** Number of milliseconds to wait before aborting the query. */
|
||||
maxTimeMS?: number;
|
||||
/** An index name hint for the query. */
|
||||
hint?: string | Document;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class CountOperation extends CommandOperation<number> {
|
||||
override options: CountOptions;
|
||||
collectionName?: string;
|
||||
query: Document;
|
||||
|
||||
constructor(namespace: MongoDBNamespace, filter: Document, options: CountOptions) {
|
||||
super({ s: { namespace: namespace } } as unknown as Collection, options);
|
||||
|
||||
this.options = options;
|
||||
this.collectionName = namespace.collection;
|
||||
this.query = filter;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<number>
|
||||
): void {
|
||||
const options = this.options;
|
||||
const cmd: Document = {
|
||||
count: this.collectionName,
|
||||
query: this.query
|
||||
};
|
||||
|
||||
if (typeof options.limit === 'number') {
|
||||
cmd.limit = options.limit;
|
||||
}
|
||||
|
||||
if (typeof options.skip === 'number') {
|
||||
cmd.skip = options.skip;
|
||||
}
|
||||
|
||||
if (options.hint != null) {
|
||||
cmd.hint = options.hint;
|
||||
}
|
||||
|
||||
if (typeof options.maxTimeMS === 'number') {
|
||||
cmd.maxTimeMS = options.maxTimeMS;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, cmd, (err, result) => {
|
||||
callback(err, result ? result.n : 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(CountOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]);
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { AggregateOperation, AggregateOptions } from './aggregate';
|
||||
|
||||
/** @public */
|
||||
export interface CountDocumentsOptions extends AggregateOptions {
|
||||
/** The number of documents to skip. */
|
||||
skip?: number;
|
||||
/** The maximum amounts to count before aborting. */
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class CountDocumentsOperation extends AggregateOperation<number> {
|
||||
constructor(collection: Collection, query: Document, options: CountDocumentsOptions) {
|
||||
const pipeline = [];
|
||||
pipeline.push({ $match: query });
|
||||
|
||||
if (typeof options.skip === 'number') {
|
||||
pipeline.push({ $skip: options.skip });
|
||||
}
|
||||
|
||||
if (typeof options.limit === 'number') {
|
||||
pipeline.push({ $limit: options.limit });
|
||||
}
|
||||
|
||||
pipeline.push({ $group: { _id: 1, n: { $sum: 1 } } });
|
||||
|
||||
super(collection.s.namespace, pipeline, options);
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<number>
|
||||
): void {
|
||||
super.execute(server, session, (err, result) => {
|
||||
if (err || !result) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: We're avoiding creating a cursor here to reduce the callstack.
|
||||
const response = result as unknown as Document;
|
||||
if (response.cursor == null || response.cursor.firstBatch == null) {
|
||||
callback(undefined, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const docs = response.cursor.firstBatch;
|
||||
callback(undefined, docs.length ? docs[0].n : 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
import type { Document } from '../bson';
|
||||
import { Collection } from '../collection';
|
||||
import type { Db } from '../db';
|
||||
import type { PkFactory } from '../mongo_client';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { CreateIndexOperation } from './indexes';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
const ILLEGAL_COMMAND_FIELDS = new Set([
|
||||
'w',
|
||||
'wtimeout',
|
||||
'j',
|
||||
'fsync',
|
||||
'autoIndexId',
|
||||
'pkFactory',
|
||||
'raw',
|
||||
'readPreference',
|
||||
'session',
|
||||
'readConcern',
|
||||
'writeConcern',
|
||||
'raw',
|
||||
'fieldsAsRaw',
|
||||
'promoteLongs',
|
||||
'promoteValues',
|
||||
'promoteBuffers',
|
||||
'bsonRegExp',
|
||||
'serializeFunctions',
|
||||
'ignoreUndefined',
|
||||
'enableUtf8Validation'
|
||||
]);
|
||||
|
||||
/** @public
|
||||
* Configuration options for timeseries collections
|
||||
* @see https://docs.mongodb.com/manual/core/timeseries-collections/
|
||||
*/
|
||||
export interface TimeSeriesCollectionOptions extends Document {
|
||||
timeField: string;
|
||||
metaField?: string;
|
||||
granularity?: 'seconds' | 'minutes' | 'hours' | string;
|
||||
}
|
||||
|
||||
/** @public
|
||||
* Configuration options for clustered collections
|
||||
* @see https://www.mongodb.com/docs/manual/core/clustered-collections/
|
||||
*/
|
||||
export interface ClusteredCollectionOptions extends Document {
|
||||
name?: string;
|
||||
key: Document;
|
||||
unique: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CreateCollectionOptions extends CommandOperationOptions {
|
||||
/** Returns an error if the collection does not exist */
|
||||
strict?: boolean;
|
||||
/** Create a capped collection */
|
||||
capped?: boolean;
|
||||
/** @deprecated Create an index on the _id field of the document, True by default on MongoDB 2.6 - 3.0 */
|
||||
autoIndexId?: boolean;
|
||||
/** The size of the capped collection in bytes */
|
||||
size?: number;
|
||||
/** The maximum number of documents in the capped collection */
|
||||
max?: number;
|
||||
/** Available for the MMAPv1 storage engine only to set the usePowerOf2Sizes and the noPadding flag */
|
||||
flags?: number;
|
||||
/** Allows users to specify configuration to the storage engine on a per-collection basis when creating a collection on MongoDB 3.0 or higher */
|
||||
storageEngine?: Document;
|
||||
/** Allows users to specify validation rules or expressions for the collection. For more information, see Document Validation on MongoDB 3.2 or higher */
|
||||
validator?: Document;
|
||||
/** Determines how strictly MongoDB applies the validation rules to existing documents during an update on MongoDB 3.2 or higher */
|
||||
validationLevel?: string;
|
||||
/** Determines whether to error on invalid documents or just warn about the violations but allow invalid documents to be inserted on MongoDB 3.2 or higher */
|
||||
validationAction?: string;
|
||||
/** Allows users to specify a default configuration for indexes when creating a collection on MongoDB 3.2 or higher */
|
||||
indexOptionDefaults?: Document;
|
||||
/** The name of the source collection or view from which to create the view. The name is not the full namespace of the collection or view; i.e. does not include the database name and implies the same database as the view to create on MongoDB 3.4 or higher */
|
||||
viewOn?: string;
|
||||
/** An array that consists of the aggregation pipeline stage. Creates the view by applying the specified pipeline to the viewOn collection or view on MongoDB 3.4 or higher */
|
||||
pipeline?: Document[];
|
||||
/** A primary key factory function for generation of custom _id keys. */
|
||||
pkFactory?: PkFactory;
|
||||
/** A document specifying configuration options for timeseries collections. */
|
||||
timeseries?: TimeSeriesCollectionOptions;
|
||||
/** A document specifying configuration options for clustered collections. For MongoDB 5.3 and above. */
|
||||
clusteredIndex?: ClusteredCollectionOptions;
|
||||
/** The number of seconds after which a document in a timeseries or clustered collection expires. */
|
||||
expireAfterSeconds?: number;
|
||||
/** @experimental */
|
||||
encryptedFields?: Document;
|
||||
/**
|
||||
* If set, enables pre-update and post-update document events to be included for any
|
||||
* change streams that listen on this collection.
|
||||
*/
|
||||
changeStreamPreAndPostImages?: { enabled: boolean };
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class CreateCollectionOperation extends CommandOperation<Collection> {
|
||||
override options: CreateCollectionOptions;
|
||||
db: Db;
|
||||
name: string;
|
||||
|
||||
constructor(db: Db, name: string, options: CreateCollectionOptions = {}) {
|
||||
super(db, options);
|
||||
|
||||
this.options = options;
|
||||
this.db = db;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Collection>
|
||||
): void {
|
||||
(async () => {
|
||||
const db = this.db;
|
||||
const name = this.name;
|
||||
const options = this.options;
|
||||
|
||||
const encryptedFields: Document | undefined =
|
||||
options.encryptedFields ??
|
||||
db.s.client.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`];
|
||||
|
||||
if (encryptedFields) {
|
||||
// Create auxilliary collections for queryable encryption support.
|
||||
const escCollection = encryptedFields.escCollection ?? `enxcol_.${name}.esc`;
|
||||
const eccCollection = encryptedFields.eccCollection ?? `enxcol_.${name}.ecc`;
|
||||
const ecocCollection = encryptedFields.ecocCollection ?? `enxcol_.${name}.ecoc`;
|
||||
|
||||
for (const collectionName of [escCollection, eccCollection, ecocCollection]) {
|
||||
const createOp = new CreateCollectionOperation(db, collectionName, {
|
||||
clusteredIndex: {
|
||||
key: { _id: 1 },
|
||||
unique: true
|
||||
}
|
||||
});
|
||||
await createOp.executeWithoutEncryptedFieldsCheck(server, session);
|
||||
}
|
||||
|
||||
if (!options.encryptedFields) {
|
||||
this.options = { ...this.options, encryptedFields };
|
||||
}
|
||||
}
|
||||
|
||||
const coll = await this.executeWithoutEncryptedFieldsCheck(server, session);
|
||||
|
||||
if (encryptedFields) {
|
||||
// Create the required index for queryable encryption support.
|
||||
const createIndexOp = new CreateIndexOperation(db, name, { __safeContent__: 1 }, {});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
createIndexOp.execute(server, session, err => (err ? reject(err) : resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
return coll;
|
||||
})().then(
|
||||
coll => callback(undefined, coll),
|
||||
err => callback(err)
|
||||
);
|
||||
}
|
||||
|
||||
private executeWithoutEncryptedFieldsCheck(
|
||||
server: Server,
|
||||
session: ClientSession | undefined
|
||||
): Promise<Collection> {
|
||||
return new Promise<Collection>((resolve, reject) => {
|
||||
const db = this.db;
|
||||
const name = this.name;
|
||||
const options = this.options;
|
||||
|
||||
const done: Callback = err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(new Collection(db, name, options));
|
||||
};
|
||||
|
||||
const cmd: Document = { create: name };
|
||||
for (const n in options) {
|
||||
if (
|
||||
(options as any)[n] != null &&
|
||||
typeof (options as any)[n] !== 'function' &&
|
||||
!ILLEGAL_COMMAND_FIELDS.has(n)
|
||||
) {
|
||||
cmd[n] = (options as any)[n];
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise just execute the command
|
||||
super.executeCommand(server, session, cmd, done);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(CreateCollectionOperation, [Aspect.WRITE_OPERATION]);
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoCompatibilityError, MongoServerError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, collationNotSupported, maxWireVersion, MongoDBNamespace } from '../utils';
|
||||
import type { WriteConcernOptions } from '../write_concern';
|
||||
import { CollationOptions, CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects, Hint } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface DeleteOptions extends CommandOperationOptions, WriteConcernOptions {
|
||||
/** If true, when an insert fails, don't execute the remaining writes. If false, continue with remaining inserts when one fails. */
|
||||
ordered?: boolean;
|
||||
/** Specifies the collation to use for the operation */
|
||||
collation?: CollationOptions;
|
||||
/** Specify that the update query should only consider plans using the hinted index */
|
||||
hint?: string | Document;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
|
||||
/** @deprecated use `removeOne` or `removeMany` to implicitly specify the limit */
|
||||
single?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface DeleteResult {
|
||||
/** Indicates whether this write result was acknowledged. If not, then all other members of this result will be undefined. */
|
||||
acknowledged: boolean;
|
||||
/** The number of documents that were deleted */
|
||||
deletedCount: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface DeleteStatement {
|
||||
/** The query that matches documents to delete. */
|
||||
q: Document;
|
||||
/** The number of matching documents to delete. */
|
||||
limit: number;
|
||||
/** Specifies the collation to use for the operation. */
|
||||
collation?: CollationOptions;
|
||||
/** A document or string that specifies the index to use to support the query predicate. */
|
||||
hint?: Hint;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class DeleteOperation extends CommandOperation<Document> {
|
||||
override options: DeleteOptions;
|
||||
statements: DeleteStatement[];
|
||||
|
||||
constructor(ns: MongoDBNamespace, statements: DeleteStatement[], options: DeleteOptions) {
|
||||
super(undefined, options);
|
||||
this.options = options;
|
||||
this.ns = ns;
|
||||
this.statements = statements;
|
||||
}
|
||||
|
||||
override get canRetryWrite(): boolean {
|
||||
if (super.canRetryWrite === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.statements.every(op => (op.limit != null ? op.limit > 0 : true));
|
||||
}
|
||||
|
||||
override execute(server: Server, session: ClientSession | undefined, callback: Callback): void {
|
||||
const options = this.options ?? {};
|
||||
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
const command: Document = {
|
||||
delete: this.ns.collection,
|
||||
deletes: this.statements,
|
||||
ordered
|
||||
};
|
||||
|
||||
if (options.let) {
|
||||
command.let = options.let;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (options.comment !== undefined) {
|
||||
command.comment = options.comment;
|
||||
}
|
||||
|
||||
if (options.explain != null && maxWireVersion(server) < 3) {
|
||||
return callback
|
||||
? callback(
|
||||
new MongoCompatibilityError(`Server ${server.name} does not support explain on delete`)
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const unacknowledgedWrite = this.writeConcern && this.writeConcern.w === 0;
|
||||
if (unacknowledgedWrite || maxWireVersion(server) < 5) {
|
||||
if (this.statements.find((o: Document) => o.hint)) {
|
||||
callback(new MongoCompatibilityError(`Servers < 3.4 do not support hint on delete`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const statementWithCollation = this.statements.find(statement => !!statement.collation);
|
||||
if (statementWithCollation && collationNotSupported(server, statementWithCollation)) {
|
||||
callback(new MongoCompatibilityError(`Server ${server.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteOneOperation extends DeleteOperation {
|
||||
constructor(collection: Collection, filter: Document, options: DeleteOptions) {
|
||||
super(collection.s.namespace, [makeDeleteStatement(filter, { ...options, limit: 1 })], options);
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<DeleteResult>
|
||||
): void {
|
||||
super.execute(server, session, (err, res) => {
|
||||
if (err || res == null) return callback(err);
|
||||
if (res.code) return callback(new MongoServerError(res));
|
||||
if (res.writeErrors) return callback(new MongoServerError(res.writeErrors[0]));
|
||||
if (this.explain) return callback(undefined, res);
|
||||
|
||||
callback(undefined, {
|
||||
acknowledged: this.writeConcern?.w !== 0 ?? true,
|
||||
deletedCount: res.n
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteManyOperation extends DeleteOperation {
|
||||
constructor(collection: Collection, filter: Document, options: DeleteOptions) {
|
||||
super(collection.s.namespace, [makeDeleteStatement(filter, options)], options);
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<DeleteResult>
|
||||
): void {
|
||||
super.execute(server, session, (err, res) => {
|
||||
if (err || res == null) return callback(err);
|
||||
if (res.code) return callback(new MongoServerError(res));
|
||||
if (res.writeErrors) return callback(new MongoServerError(res.writeErrors[0]));
|
||||
if (this.explain) return callback(undefined, res);
|
||||
|
||||
callback(undefined, {
|
||||
acknowledged: this.writeConcern?.w !== 0 ?? true,
|
||||
deletedCount: res.n
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function makeDeleteStatement(
|
||||
filter: Document,
|
||||
options: DeleteOptions & { limit?: number }
|
||||
): DeleteStatement {
|
||||
const op: DeleteStatement = {
|
||||
q: filter,
|
||||
limit: typeof options.limit === 'number' ? options.limit : 0
|
||||
};
|
||||
|
||||
if (options.single === true) {
|
||||
op.limit = 1;
|
||||
}
|
||||
|
||||
if (options.collation) {
|
||||
op.collation = options.collation;
|
||||
}
|
||||
|
||||
if (options.hint) {
|
||||
op.hint = options.hint;
|
||||
}
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
defineAspects(DeleteOperation, [Aspect.RETRYABLE, Aspect.WRITE_OPERATION]);
|
||||
defineAspects(DeleteOneOperation, [
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.WRITE_OPERATION,
|
||||
Aspect.EXPLAINABLE,
|
||||
Aspect.SKIP_COLLATION
|
||||
]);
|
||||
defineAspects(DeleteManyOperation, [
|
||||
Aspect.WRITE_OPERATION,
|
||||
Aspect.EXPLAINABLE,
|
||||
Aspect.SKIP_COLLATION
|
||||
]);
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, decorateWithCollation, decorateWithReadConcern } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export type DistinctOptions = CommandOperationOptions;
|
||||
|
||||
/**
|
||||
* Return a list of distinct values for the given key across a collection.
|
||||
* @internal
|
||||
*/
|
||||
export class DistinctOperation extends CommandOperation<any[]> {
|
||||
override options: DistinctOptions;
|
||||
collection: Collection;
|
||||
/** Field of the document to find distinct values for. */
|
||||
key: string;
|
||||
/** The query for filtering the set of documents to which we apply the distinct filter. */
|
||||
query: Document;
|
||||
|
||||
/**
|
||||
* Construct a Distinct operation.
|
||||
*
|
||||
* @param collection - Collection instance.
|
||||
* @param key - Field of the document to find distinct values for.
|
||||
* @param query - The query for filtering the set of documents to which we apply the distinct filter.
|
||||
* @param options - Optional settings. See Collection.prototype.distinct for a list of options.
|
||||
*/
|
||||
constructor(collection: Collection, key: string, query: Document, options?: DistinctOptions) {
|
||||
super(collection, options);
|
||||
|
||||
this.options = options ?? {};
|
||||
this.collection = collection;
|
||||
this.key = key;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<any[]>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
const key = this.key;
|
||||
const query = this.query;
|
||||
const options = this.options;
|
||||
|
||||
// Distinct command
|
||||
const cmd: Document = {
|
||||
distinct: coll.collectionName,
|
||||
key: key,
|
||||
query: query
|
||||
};
|
||||
|
||||
// Add maxTimeMS if defined
|
||||
if (typeof options.maxTimeMS === 'number') {
|
||||
cmd.maxTimeMS = options.maxTimeMS;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (typeof options.comment !== 'undefined') {
|
||||
cmd.comment = options.comment;
|
||||
}
|
||||
|
||||
// Do we have a readConcern specified
|
||||
decorateWithReadConcern(cmd, coll, options);
|
||||
|
||||
// Have we specified collation
|
||||
try {
|
||||
decorateWithCollation(cmd, coll, options);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, cmd, (err, result) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(undefined, this.explain ? result : result.values);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(DistinctOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE, Aspect.EXPLAINABLE]);
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Db } from '../db';
|
||||
import { MONGODB_ERROR_CODES, MongoServerError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface DropCollectionOptions extends CommandOperationOptions {
|
||||
/** @experimental */
|
||||
encryptedFields?: Document;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class DropCollectionOperation extends CommandOperation<boolean> {
|
||||
override options: DropCollectionOptions;
|
||||
db: Db;
|
||||
name: string;
|
||||
|
||||
constructor(db: Db, name: string, options: DropCollectionOptions = {}) {
|
||||
super(db, options);
|
||||
this.db = db;
|
||||
this.options = options;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<boolean>
|
||||
): void {
|
||||
(async () => {
|
||||
const db = this.db;
|
||||
const options = this.options;
|
||||
const name = this.name;
|
||||
|
||||
const encryptedFieldsMap = db.s.client.options.autoEncryption?.encryptedFieldsMap;
|
||||
let encryptedFields: Document | undefined =
|
||||
options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`];
|
||||
|
||||
if (!encryptedFields && encryptedFieldsMap) {
|
||||
// If the MongoClient was configued with an encryptedFieldsMap,
|
||||
// and no encryptedFields config was available in it or explicitly
|
||||
// passed as an argument, the spec tells us to look one up using
|
||||
// listCollections().
|
||||
const listCollectionsResult = await db
|
||||
.listCollections({ name }, { nameOnly: false })
|
||||
.toArray();
|
||||
encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields;
|
||||
}
|
||||
|
||||
if (encryptedFields) {
|
||||
const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`;
|
||||
const eccCollection = encryptedFields.eccCollection || `enxcol_.${name}.ecc`;
|
||||
const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`;
|
||||
|
||||
for (const collectionName of [escCollection, eccCollection, ecocCollection]) {
|
||||
// Drop auxilliary collections, ignoring potential NamespaceNotFound errors.
|
||||
const dropOp = new DropCollectionOperation(db, collectionName);
|
||||
try {
|
||||
await dropOp.executeWithoutEncryptedFieldsCheck(server, session);
|
||||
} catch (err) {
|
||||
if (
|
||||
!(err instanceof MongoServerError) ||
|
||||
err.code !== MONGODB_ERROR_CODES.NamespaceNotFound
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this.executeWithoutEncryptedFieldsCheck(server, session);
|
||||
})().then(
|
||||
result => callback(undefined, result),
|
||||
err => callback(err)
|
||||
);
|
||||
}
|
||||
|
||||
private executeWithoutEncryptedFieldsCheck(
|
||||
server: Server,
|
||||
session: ClientSession | undefined
|
||||
): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
super.executeCommand(server, session, { drop: this.name }, (err, result) => {
|
||||
if (err) return reject(err);
|
||||
resolve(!!result.ok);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type DropDatabaseOptions = CommandOperationOptions;
|
||||
|
||||
/** @internal */
|
||||
export class DropDatabaseOperation extends CommandOperation<boolean> {
|
||||
override options: DropDatabaseOptions;
|
||||
|
||||
constructor(db: Db, options: DropDatabaseOptions) {
|
||||
super(db, options);
|
||||
this.options = options;
|
||||
}
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<boolean>
|
||||
): void {
|
||||
super.executeCommand(server, session, { dropDatabase: 1 }, (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result.ok) return callback(undefined, true);
|
||||
callback(undefined, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(DropCollectionOperation, [Aspect.WRITE_OPERATION]);
|
||||
defineAspects(DropDatabaseOperation, [Aspect.WRITE_OPERATION]);
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface EstimatedDocumentCountOptions extends CommandOperationOptions {
|
||||
/**
|
||||
* The maximum amount of time to allow the operation to run.
|
||||
*
|
||||
* This option is sent only if the caller explicitly provides a value. The default is to not send a value.
|
||||
*/
|
||||
maxTimeMS?: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class EstimatedDocumentCountOperation extends CommandOperation<number> {
|
||||
override options: EstimatedDocumentCountOptions;
|
||||
collectionName: string;
|
||||
|
||||
constructor(collection: Collection, options: EstimatedDocumentCountOptions = {}) {
|
||||
super(collection, options);
|
||||
this.options = options;
|
||||
this.collectionName = collection.collectionName;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<number>
|
||||
): void {
|
||||
const cmd: Document = { count: this.collectionName };
|
||||
|
||||
if (typeof this.options.maxTimeMS === 'number') {
|
||||
cmd.maxTimeMS = this.options.maxTimeMS;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (this.options.comment !== undefined) {
|
||||
cmd.comment = this.options.comment;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, cmd, (err, response) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(undefined, response?.n || 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(EstimatedDocumentCountOperation, [
|
||||
Aspect.READ_OPERATION,
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.CURSOR_CREATING
|
||||
]);
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { Code, Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Db } from '../db';
|
||||
import { MongoServerError } from '../error';
|
||||
import { ReadPreference } from '../read_preference';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
|
||||
/** @public */
|
||||
export interface EvalOptions extends CommandOperationOptions {
|
||||
nolock?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class EvalOperation extends CommandOperation<Document> {
|
||||
override options: EvalOptions;
|
||||
code: Code;
|
||||
parameters?: Document | Document[];
|
||||
|
||||
constructor(
|
||||
db: Db | Collection,
|
||||
code: Code,
|
||||
parameters?: Document | Document[],
|
||||
options?: EvalOptions
|
||||
) {
|
||||
super(db, options);
|
||||
|
||||
this.options = options ?? {};
|
||||
this.code = code;
|
||||
this.parameters = parameters;
|
||||
// force primary read preference
|
||||
Object.defineProperty(this, 'readPreference', {
|
||||
value: ReadPreference.primary,
|
||||
configurable: false,
|
||||
writable: false
|
||||
});
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
let finalCode = this.code;
|
||||
let finalParameters: Document[] = [];
|
||||
|
||||
// If not a code object translate to one
|
||||
if (!(finalCode && (finalCode as unknown as { _bsontype: string })._bsontype === 'Code')) {
|
||||
finalCode = new Code(finalCode as never);
|
||||
}
|
||||
|
||||
// Ensure the parameters are correct
|
||||
if (this.parameters != null && typeof this.parameters !== 'function') {
|
||||
finalParameters = Array.isArray(this.parameters) ? this.parameters : [this.parameters];
|
||||
}
|
||||
|
||||
// Create execution selector
|
||||
const cmd: Document = { $eval: finalCode, args: finalParameters };
|
||||
|
||||
// Check if the nolock parameter is passed in
|
||||
if (this.options.nolock) {
|
||||
cmd.nolock = this.options.nolock;
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
super.executeCommand(server, session, cmd, (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result && result.ok === 1) {
|
||||
return callback(undefined, result.retval);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
callback(new MongoServerError({ message: `eval failed: ${result.errmsg}` }));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err, result);
|
||||
});
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
import type { Document } from '../bson';
|
||||
import {
|
||||
isRetryableReadError,
|
||||
isRetryableWriteError,
|
||||
MongoCompatibilityError,
|
||||
MONGODB_ERROR_CODES,
|
||||
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 {
|
||||
sameServerSelector,
|
||||
secondaryWritableServerSelector,
|
||||
ServerSelector
|
||||
} from '../sdam/server_selection';
|
||||
import type { Topology } from '../sdam/topology';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, maybePromise, supportsRetryableWrites } from '../utils';
|
||||
import { AbstractOperation, Aspect } from './operation';
|
||||
|
||||
const MMAPv1_RETRY_WRITES_ERROR_CODE = MONGODB_ERROR_CODES.IllegalOperation;
|
||||
const MMAPv1_RETRY_WRITES_ERROR_MESSAGE =
|
||||
'This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.';
|
||||
|
||||
type ResultTypeFromOperation<TOperation> = TOperation extends AbstractOperation<infer K>
|
||||
? K
|
||||
: never;
|
||||
|
||||
/** @internal */
|
||||
export interface ExecutionResult {
|
||||
/** The server selected for the operation */
|
||||
server: Server;
|
||||
/** The session used for this operation, may be implicitly created */
|
||||
session?: ClientSession;
|
||||
/** The raw server response for the operation */
|
||||
response: Document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given operation with provided arguments.
|
||||
* @internal
|
||||
*
|
||||
* @remarks
|
||||
* This method reduces large amounts of duplication in the entire codebase by providing
|
||||
* a single point for determining whether callbacks or promises should be used. Additionally
|
||||
* it allows for a single point of entry to provide features such as implicit sessions, which
|
||||
* are required by the Driver Sessions specification in the event that a ClientSession is
|
||||
* not provided
|
||||
*
|
||||
* @param topology - The topology to execute this operation on
|
||||
* @param operation - The operation to execute
|
||||
* @param callback - The command result callback
|
||||
*/
|
||||
export function executeOperation<
|
||||
T extends AbstractOperation<TResult>,
|
||||
TResult = ResultTypeFromOperation<T>
|
||||
>(client: MongoClient, operation: T): Promise<TResult>;
|
||||
export function executeOperation<
|
||||
T extends AbstractOperation<TResult>,
|
||||
TResult = ResultTypeFromOperation<T>
|
||||
>(client: MongoClient, operation: T, callback: Callback<TResult>): void;
|
||||
export function executeOperation<
|
||||
T extends AbstractOperation<TResult>,
|
||||
TResult = ResultTypeFromOperation<T>
|
||||
>(client: MongoClient, operation: T, callback?: Callback<TResult>): Promise<TResult> | void;
|
||||
export function executeOperation<
|
||||
T extends AbstractOperation<TResult>,
|
||||
TResult = ResultTypeFromOperation<T>
|
||||
>(client: MongoClient, operation: T, callback?: Callback<TResult>): Promise<TResult> | void {
|
||||
if (!(operation instanceof AbstractOperation)) {
|
||||
// TODO(NODE-3483): Extend MongoRuntimeError
|
||||
throw new MongoRuntimeError('This method requires a valid operation instance');
|
||||
}
|
||||
|
||||
return maybePromise(callback, callback => {
|
||||
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<T, TResult>(client, operation, callback);
|
||||
});
|
||||
}
|
||||
|
||||
if (topology.shouldCheckForSessionSupport()) {
|
||||
return topology.selectServer(ReadPreference.primaryPreferred, {}, err => {
|
||||
if (err) return callback(err);
|
||||
|
||||
executeOperation<T, TResult>(client, operation, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// The driver sessions spec mandates that we implicitly create sessions for operations
|
||||
// that are not explicitly provided with a session.
|
||||
let session = operation.session;
|
||||
let owner: symbol | undefined;
|
||||
if (topology.hasSessionSupport()) {
|
||||
if (session == null) {
|
||||
owner = Symbol();
|
||||
session = client.startSession({ owner, explicit: false });
|
||||
} else if (session.hasEnded) {
|
||||
return callback(new MongoExpiredSessionError('Use of expired sessions is not permitted'));
|
||||
} else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) {
|
||||
return callback(new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later'));
|
||||
}
|
||||
} else {
|
||||
// no session support
|
||||
if (session && session.explicit) {
|
||||
// If the user passed an explicit session and we are still, after server selection,
|
||||
// trying to run against a topology that doesn't support sessions we error out.
|
||||
return callback(new MongoCompatibilityError('Current topology does not support sessions'));
|
||||
} else if (session && !session.explicit) {
|
||||
// We do not have to worry about ending the session because the server session has not been acquired yet
|
||||
delete operation.options.session;
|
||||
operation.clearSession();
|
||||
session = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
executeWithServerSelection<TResult>(topology, session, operation, (error, result) => {
|
||||
if (session?.owner != null && session.owner === owner) {
|
||||
return session.endSession(endSessionError => callback(endSessionError ?? error, result));
|
||||
}
|
||||
|
||||
callback(error, result);
|
||||
});
|
||||
} catch (error) {
|
||||
if (session?.owner != null && session.owner === owner) {
|
||||
session.endSession().catch(() => null);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeWithServerSelection<TResult>(
|
||||
topology: Topology,
|
||||
session: ClientSession | undefined,
|
||||
operation: AbstractOperation,
|
||||
callback: Callback<TResult>
|
||||
) {
|
||||
const readPreference = operation.readPreference ?? ReadPreference.primary;
|
||||
const inTransaction = !!session?.inTransaction();
|
||||
|
||||
if (inTransaction && !readPreference.equals(ReadPreference.primary)) {
|
||||
return callback(
|
||||
new MongoTransactionError(
|
||||
`Read preference in a transaction must be primary, not: ${readPreference.mode}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (session?.isPinned && session.transaction.isCommitted && !operation.bypassPinningCheck) {
|
||||
session.unpin();
|
||||
}
|
||||
|
||||
let selector: ReadPreference | ServerSelector;
|
||||
|
||||
if (operation.hasAspect(Aspect.MUST_SELECT_SAME_SERVER)) {
|
||||
// GetMore and KillCursor operations must always select the same server, but run through
|
||||
// server selection to potentially force monitor checks if the server is
|
||||
// in an unknown state.
|
||||
selector = sameServerSelector(operation.server?.description);
|
||||
} else if (operation.trySecondaryWrite) {
|
||||
// If operation should try to write to secondary use the custom server selector
|
||||
// otherwise provide the read preference.
|
||||
selector = secondaryWritableServerSelector(topology.commonWireVersion, readPreference);
|
||||
} else {
|
||||
selector = readPreference;
|
||||
}
|
||||
|
||||
const serverSelectionOptions = { session };
|
||||
function retryOperation(originalError: MongoError) {
|
||||
const isWriteOperation = operation.hasAspect(Aspect.WRITE_OPERATION);
|
||||
const isReadOperation = operation.hasAspect(Aspect.READ_OPERATION);
|
||||
|
||||
if (isWriteOperation && originalError.code === MMAPv1_RETRY_WRITES_ERROR_CODE) {
|
||||
return callback(
|
||||
new MongoServerError({
|
||||
message: MMAPv1_RETRY_WRITES_ERROR_MESSAGE,
|
||||
errmsg: MMAPv1_RETRY_WRITES_ERROR_MESSAGE,
|
||||
originalError
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isWriteOperation && !isRetryableWriteError(originalError)) {
|
||||
return callback(originalError);
|
||||
}
|
||||
|
||||
if (isReadOperation && !isRetryableReadError(originalError)) {
|
||||
return callback(originalError);
|
||||
}
|
||||
|
||||
if (
|
||||
originalError instanceof MongoNetworkError &&
|
||||
session?.isPinned &&
|
||||
!session.inTransaction() &&
|
||||
operation.hasAspect(Aspect.CURSOR_CREATING)
|
||||
) {
|
||||
// If we have a cursor and the initial command fails with a network error,
|
||||
// we can retry it on another connection. So we need to check it back in, clear the
|
||||
// pool for the service id, and retry again.
|
||||
session.unpin({ force: true, forceClear: true });
|
||||
}
|
||||
|
||||
// select a new server, and attempt to retry the operation
|
||||
topology.selectServer(selector, serverSelectionOptions, (error?: Error, server?: Server) => {
|
||||
if (!error && isWriteOperation && !supportsRetryableWrites(server)) {
|
||||
return callback(
|
||||
new MongoUnexpectedServerResponseError(
|
||||
'Selected server does not support retryable writes'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !server) {
|
||||
return callback(
|
||||
error ?? new MongoUnexpectedServerResponseError('Server selection failed without error')
|
||||
);
|
||||
}
|
||||
|
||||
operation.execute(server, session, callback);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
readPreference &&
|
||||
!readPreference.equals(ReadPreference.primary) &&
|
||||
session?.inTransaction()
|
||||
) {
|
||||
callback(
|
||||
new MongoTransactionError(
|
||||
`Read preference in a transaction must be primary, not: ${readPreference.mode}`
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// select a server, and execute the operation against it
|
||||
topology.selectServer(selector, serverSelectionOptions, (error, server) => {
|
||||
if (error || !server) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (session && operation.hasAspect(Aspect.RETRYABLE)) {
|
||||
const willRetryRead =
|
||||
topology.s.options.retryReads && !inTransaction && operation.canRetryRead;
|
||||
|
||||
const willRetryWrite =
|
||||
topology.s.options.retryWrites &&
|
||||
!inTransaction &&
|
||||
supportsRetryableWrites(server) &&
|
||||
operation.canRetryWrite;
|
||||
|
||||
const hasReadAspect = operation.hasAspect(Aspect.READ_OPERATION);
|
||||
const hasWriteAspect = operation.hasAspect(Aspect.WRITE_OPERATION);
|
||||
|
||||
if ((hasReadAspect && willRetryRead) || (hasWriteAspect && willRetryWrite)) {
|
||||
if (hasWriteAspect && willRetryWrite) {
|
||||
operation.options.willRetryWrite = true;
|
||||
session.incrementTransactionNumber();
|
||||
}
|
||||
|
||||
return operation.execute(server, session, (error, result) => {
|
||||
if (error instanceof MongoError) {
|
||||
return retryOperation(error);
|
||||
} else if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
callback(undefined, result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return operation.execute(server, session, callback);
|
||||
});
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoCompatibilityError, MongoInvalidArgumentError } from '../error';
|
||||
import { ReadConcern } from '../read_concern';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { formatSort, Sort } from '../sort';
|
||||
import {
|
||||
Callback,
|
||||
decorateWithExplain,
|
||||
maxWireVersion,
|
||||
MongoDBNamespace,
|
||||
normalizeHintField
|
||||
} from '../utils';
|
||||
import { CollationOptions, CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects, Hint } from './operation';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @typeParam TSchema - Unused schema definition, deprecated usage, only specify `FindOptions` with no generic
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export interface FindOptions<TSchema extends Document = Document> extends CommandOperationOptions {
|
||||
/** Sets the limit of documents returned in the query. */
|
||||
limit?: number;
|
||||
/** Set to sort the documents coming back from the query. Array of indexes, `[['a', 1]]` etc. */
|
||||
sort?: Sort;
|
||||
/** The fields to return in the query. Object of fields to either include or exclude (one of, not both), `{'a':1, 'b': 1}` **or** `{'a': 0, 'b': 0}` */
|
||||
projection?: Document;
|
||||
/** Set to skip N documents ahead in your query (useful for pagination). */
|
||||
skip?: number;
|
||||
/** Tell the query to use specific indexes in the query. Object of indexes to use, `{'_id':1}` */
|
||||
hint?: Hint;
|
||||
/** Specify if the cursor can timeout. */
|
||||
timeout?: boolean;
|
||||
/** Specify if the cursor is tailable. */
|
||||
tailable?: boolean;
|
||||
/** Specify if the cursor is a tailable-await cursor. Requires `tailable` to be true */
|
||||
awaitData?: boolean;
|
||||
/** Set the batchSize for the getMoreCommand when iterating over the query results. */
|
||||
batchSize?: number;
|
||||
/** If true, returns only the index keys in the resulting documents. */
|
||||
returnKey?: boolean;
|
||||
/** The inclusive lower bound for a specific index */
|
||||
min?: Document;
|
||||
/** The exclusive upper bound for a specific index */
|
||||
max?: Document;
|
||||
/** Number of milliseconds to wait before aborting the query. */
|
||||
maxTimeMS?: number;
|
||||
/** The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. Requires `tailable` and `awaitData` to be true */
|
||||
maxAwaitTimeMS?: number;
|
||||
/** The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use. Set this option to prevent that. */
|
||||
noCursorTimeout?: boolean;
|
||||
/** Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields). */
|
||||
collation?: CollationOptions;
|
||||
/** Allows disk use for blocking sort operations exceeding 100MB memory. (MongoDB 3.2 or higher) */
|
||||
allowDiskUse?: boolean;
|
||||
/** Determines whether to close the cursor after the first batch. Defaults to false. */
|
||||
singleBatch?: boolean;
|
||||
/** For queries against a sharded collection, allows the command (or subsequent getMore commands) to return partial results, rather than an error, if one or more queried shards are unavailable. */
|
||||
allowPartialResults?: boolean;
|
||||
/** Determines whether to return the record identifier for each document. If true, adds a field $recordId to the returned documents. */
|
||||
showRecordId?: boolean;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
/**
|
||||
* Option to enable an optimized code path for queries looking for a particular range of `ts` values in the oplog. Requires `tailable` to be true.
|
||||
* @deprecated Starting from MongoDB 4.4 this flag is not needed and will be ignored.
|
||||
*/
|
||||
oplogReplay?: boolean;
|
||||
}
|
||||
|
||||
const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5;
|
||||
|
||||
/** @internal */
|
||||
export class FindOperation extends CommandOperation<Document> {
|
||||
override options: FindOptions;
|
||||
filter: Document;
|
||||
|
||||
constructor(
|
||||
collection: Collection | undefined,
|
||||
ns: MongoDBNamespace,
|
||||
filter: Document = {},
|
||||
options: FindOptions = {}
|
||||
) {
|
||||
super(collection, options);
|
||||
|
||||
this.options = options;
|
||||
this.ns = ns;
|
||||
|
||||
if (typeof filter !== 'object' || Array.isArray(filter)) {
|
||||
throw new MongoInvalidArgumentError('Query filter must be a plain object or ObjectId');
|
||||
}
|
||||
|
||||
// If the filter is a buffer, validate that is a valid BSON document
|
||||
if (Buffer.isBuffer(filter)) {
|
||||
const objectSize = filter[0] | (filter[1] << 8) | (filter[2] << 16) | (filter[3] << 24);
|
||||
if (objectSize !== filter.length) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Query filter raw message size does not match message header size [${filter.length}] != [${objectSize}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// special case passing in an ObjectId as a filter
|
||||
this.filter = filter != null && filter._bsontype === 'ObjectID' ? { _id: filter } : filter;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
this.server = server;
|
||||
|
||||
const serverWireVersion = maxWireVersion(server);
|
||||
const options = this.options;
|
||||
if (options.allowDiskUse != null && serverWireVersion < 4) {
|
||||
callback(
|
||||
new MongoCompatibilityError('Option "allowDiskUse" is not supported on MongoDB < 3.2')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.collation && serverWireVersion < SUPPORTS_WRITE_CONCERN_AND_COLLATION) {
|
||||
callback(
|
||||
new MongoCompatibilityError(
|
||||
`Server ${server.name}, which reports wire version ${serverWireVersion}, does not support collation`
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let findCommand = makeFindCommand(this.ns, this.filter, options);
|
||||
if (this.explain) {
|
||||
findCommand = decorateWithExplain(findCommand, this.explain);
|
||||
}
|
||||
|
||||
server.command(
|
||||
this.ns,
|
||||
findCommand,
|
||||
{
|
||||
...this.options,
|
||||
...this.bsonOptions,
|
||||
documentsReturnedIn: 'firstBatch',
|
||||
session
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function makeFindCommand(ns: MongoDBNamespace, filter: Document, options: FindOptions): Document {
|
||||
const findCommand: Document = {
|
||||
find: ns.collection,
|
||||
filter
|
||||
};
|
||||
|
||||
if (options.sort) {
|
||||
findCommand.sort = formatSort(options.sort);
|
||||
}
|
||||
|
||||
if (options.projection) {
|
||||
let projection = options.projection;
|
||||
if (projection && Array.isArray(projection)) {
|
||||
projection = projection.length
|
||||
? projection.reduce((result, field) => {
|
||||
result[field] = 1;
|
||||
return result;
|
||||
}, {})
|
||||
: { _id: 1 };
|
||||
}
|
||||
|
||||
findCommand.projection = projection;
|
||||
}
|
||||
|
||||
if (options.hint) {
|
||||
findCommand.hint = normalizeHintField(options.hint);
|
||||
}
|
||||
|
||||
if (typeof options.skip === 'number') {
|
||||
findCommand.skip = options.skip;
|
||||
}
|
||||
|
||||
if (typeof options.limit === 'number') {
|
||||
if (options.limit < 0) {
|
||||
findCommand.limit = -options.limit;
|
||||
findCommand.singleBatch = true;
|
||||
} else {
|
||||
findCommand.limit = options.limit;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.batchSize === 'number') {
|
||||
if (options.batchSize < 0) {
|
||||
if (
|
||||
options.limit &&
|
||||
options.limit !== 0 &&
|
||||
Math.abs(options.batchSize) < Math.abs(options.limit)
|
||||
) {
|
||||
findCommand.limit = -options.batchSize;
|
||||
}
|
||||
|
||||
findCommand.singleBatch = true;
|
||||
} else {
|
||||
findCommand.batchSize = options.batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.singleBatch === 'boolean') {
|
||||
findCommand.singleBatch = options.singleBatch;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (options.comment !== undefined) {
|
||||
findCommand.comment = options.comment;
|
||||
}
|
||||
|
||||
if (typeof options.maxTimeMS === 'number') {
|
||||
findCommand.maxTimeMS = options.maxTimeMS;
|
||||
}
|
||||
|
||||
const readConcern = ReadConcern.fromOptions(options);
|
||||
if (readConcern) {
|
||||
findCommand.readConcern = readConcern.toJSON();
|
||||
}
|
||||
|
||||
if (options.max) {
|
||||
findCommand.max = options.max;
|
||||
}
|
||||
|
||||
if (options.min) {
|
||||
findCommand.min = options.min;
|
||||
}
|
||||
|
||||
if (typeof options.returnKey === 'boolean') {
|
||||
findCommand.returnKey = options.returnKey;
|
||||
}
|
||||
|
||||
if (typeof options.showRecordId === 'boolean') {
|
||||
findCommand.showRecordId = options.showRecordId;
|
||||
}
|
||||
|
||||
if (typeof options.tailable === 'boolean') {
|
||||
findCommand.tailable = options.tailable;
|
||||
}
|
||||
|
||||
if (typeof options.oplogReplay === 'boolean') {
|
||||
findCommand.oplogReplay = options.oplogReplay;
|
||||
}
|
||||
|
||||
if (typeof options.timeout === 'boolean') {
|
||||
findCommand.noCursorTimeout = !options.timeout;
|
||||
} else if (typeof options.noCursorTimeout === 'boolean') {
|
||||
findCommand.noCursorTimeout = options.noCursorTimeout;
|
||||
}
|
||||
|
||||
if (typeof options.awaitData === 'boolean') {
|
||||
findCommand.awaitData = options.awaitData;
|
||||
}
|
||||
|
||||
if (typeof options.allowPartialResults === 'boolean') {
|
||||
findCommand.allowPartialResults = options.allowPartialResults;
|
||||
}
|
||||
|
||||
if (options.collation) {
|
||||
findCommand.collation = options.collation;
|
||||
}
|
||||
|
||||
if (typeof options.allowDiskUse === 'boolean') {
|
||||
findCommand.allowDiskUse = options.allowDiskUse;
|
||||
}
|
||||
|
||||
if (options.let) {
|
||||
findCommand.let = options.let;
|
||||
}
|
||||
|
||||
return findCommand;
|
||||
}
|
||||
|
||||
defineAspects(FindOperation, [
|
||||
Aspect.READ_OPERATION,
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.EXPLAINABLE,
|
||||
Aspect.CURSOR_CREATING
|
||||
]);
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoCompatibilityError, MongoInvalidArgumentError } from '../error';
|
||||
import { ReadPreference } from '../read_preference';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { formatSort, Sort, SortForCmd } from '../sort';
|
||||
import { Callback, decorateWithCollation, hasAtomicOperators, maxWireVersion } from '../utils';
|
||||
import type { WriteConcern, WriteConcernSettings } from '../write_concern';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export const ReturnDocument = Object.freeze({
|
||||
BEFORE: 'before',
|
||||
AFTER: 'after'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type ReturnDocument = typeof ReturnDocument[keyof typeof ReturnDocument];
|
||||
|
||||
/** @public */
|
||||
export interface FindOneAndDeleteOptions extends CommandOperationOptions {
|
||||
/** An optional hint for query optimization. See the {@link https://docs.mongodb.com/manual/reference/command/update/#update-command-hint|update command} reference for more information.*/
|
||||
hint?: Document;
|
||||
/** Limits the fields to return for all matching documents. */
|
||||
projection?: Document;
|
||||
/** Determines which document the operation modifies if the query selects multiple documents. */
|
||||
sort?: Sort;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface FindOneAndReplaceOptions extends CommandOperationOptions {
|
||||
/** Allow driver to bypass schema validation in MongoDB 3.2 or higher. */
|
||||
bypassDocumentValidation?: boolean;
|
||||
/** An optional hint for query optimization. See the {@link https://docs.mongodb.com/manual/reference/command/update/#update-command-hint|update command} reference for more information.*/
|
||||
hint?: Document;
|
||||
/** Limits the fields to return for all matching documents. */
|
||||
projection?: Document;
|
||||
/** When set to 'after', returns the updated document rather than the original. The default is 'before'. */
|
||||
returnDocument?: ReturnDocument;
|
||||
/** Determines which document the operation modifies if the query selects multiple documents. */
|
||||
sort?: Sort;
|
||||
/** Upsert the document if it does not exist. */
|
||||
upsert?: boolean;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface FindOneAndUpdateOptions extends CommandOperationOptions {
|
||||
/** Optional list of array filters referenced in filtered positional operators */
|
||||
arrayFilters?: Document[];
|
||||
/** Allow driver to bypass schema validation in MongoDB 3.2 or higher. */
|
||||
bypassDocumentValidation?: boolean;
|
||||
/** An optional hint for query optimization. See the {@link https://docs.mongodb.com/manual/reference/command/update/#update-command-hint|update command} reference for more information.*/
|
||||
hint?: Document;
|
||||
/** Limits the fields to return for all matching documents. */
|
||||
projection?: Document;
|
||||
/** When set to 'after', returns the updated document rather than the original. The default is 'before'. */
|
||||
returnDocument?: ReturnDocument;
|
||||
/** Determines which document the operation modifies if the query selects multiple documents. */
|
||||
sort?: Sort;
|
||||
/** Upsert the document if it does not exist. */
|
||||
upsert?: boolean;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
interface FindAndModifyCmdBase {
|
||||
remove: boolean;
|
||||
new: boolean;
|
||||
upsert: boolean;
|
||||
update?: Document;
|
||||
sort?: SortForCmd;
|
||||
fields?: Document;
|
||||
bypassDocumentValidation?: boolean;
|
||||
arrayFilters?: Document[];
|
||||
maxTimeMS?: number;
|
||||
let?: Document;
|
||||
writeConcern?: WriteConcern | WriteConcernSettings;
|
||||
/**
|
||||
* Comment to apply to the operation.
|
||||
*
|
||||
* In server versions pre-4.4, 'comment' must be string. A server
|
||||
* error will be thrown if any other type is provided.
|
||||
*
|
||||
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
|
||||
*/
|
||||
comment?: unknown;
|
||||
}
|
||||
|
||||
function configureFindAndModifyCmdBaseUpdateOpts(
|
||||
cmdBase: FindAndModifyCmdBase,
|
||||
options: FindOneAndReplaceOptions | FindOneAndUpdateOptions
|
||||
): FindAndModifyCmdBase {
|
||||
cmdBase.new = options.returnDocument === ReturnDocument.AFTER;
|
||||
cmdBase.upsert = options.upsert === true;
|
||||
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
cmdBase.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
return cmdBase;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
class FindAndModifyOperation extends CommandOperation<Document> {
|
||||
override options: FindOneAndReplaceOptions | FindOneAndUpdateOptions | FindOneAndDeleteOptions;
|
||||
cmdBase: FindAndModifyCmdBase;
|
||||
collection: Collection;
|
||||
query: Document;
|
||||
doc?: Document;
|
||||
|
||||
constructor(
|
||||
collection: Collection,
|
||||
query: Document,
|
||||
options: FindOneAndReplaceOptions | FindOneAndUpdateOptions | FindOneAndDeleteOptions
|
||||
) {
|
||||
super(collection, options);
|
||||
this.options = options ?? {};
|
||||
this.cmdBase = {
|
||||
remove: false,
|
||||
new: false,
|
||||
upsert: false
|
||||
};
|
||||
|
||||
const sort = formatSort(options.sort);
|
||||
if (sort) {
|
||||
this.cmdBase.sort = sort;
|
||||
}
|
||||
|
||||
if (options.projection) {
|
||||
this.cmdBase.fields = options.projection;
|
||||
}
|
||||
|
||||
if (options.maxTimeMS) {
|
||||
this.cmdBase.maxTimeMS = options.maxTimeMS;
|
||||
}
|
||||
|
||||
// Decorate the findAndModify command with the write Concern
|
||||
if (options.writeConcern) {
|
||||
this.cmdBase.writeConcern = options.writeConcern;
|
||||
}
|
||||
|
||||
if (options.let) {
|
||||
this.cmdBase.let = options.let;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (options.comment !== undefined) {
|
||||
this.cmdBase.comment = options.comment;
|
||||
}
|
||||
|
||||
// force primary read preference
|
||||
this.readPreference = ReadPreference.primary;
|
||||
|
||||
this.collection = collection;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
const query = this.query;
|
||||
const options = { ...this.options, ...this.bsonOptions };
|
||||
|
||||
// Create findAndModify command object
|
||||
const cmd: Document = {
|
||||
findAndModify: coll.collectionName,
|
||||
query: query,
|
||||
...this.cmdBase
|
||||
};
|
||||
|
||||
// Have we specified collation
|
||||
try {
|
||||
decorateWithCollation(cmd, coll, options);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (options.hint) {
|
||||
// TODO: once this method becomes a CommandOperation we will have the server
|
||||
// in place to check.
|
||||
const unacknowledgedWrite = this.writeConcern?.w === 0;
|
||||
if (unacknowledgedWrite || maxWireVersion(server) < 8) {
|
||||
callback(
|
||||
new MongoCompatibilityError(
|
||||
'The current topology does not support a hint on findAndModify commands'
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cmd.hint = options.hint;
|
||||
}
|
||||
|
||||
if (this.explain && maxWireVersion(server) < 4) {
|
||||
callback(
|
||||
new MongoCompatibilityError(
|
||||
`Server ${server.name} does not support explain on findAndModify`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
super.executeCommand(server, session, cmd, (err, result) => {
|
||||
if (err) return callback(err);
|
||||
return callback(undefined, result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class FindOneAndDeleteOperation extends FindAndModifyOperation {
|
||||
constructor(collection: Collection, filter: Document, options: FindOneAndDeleteOptions) {
|
||||
// Basic validation
|
||||
if (filter == null || typeof filter !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Argument "filter" must be an object');
|
||||
}
|
||||
|
||||
super(collection, filter, options);
|
||||
this.cmdBase.remove = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class FindOneAndReplaceOperation extends FindAndModifyOperation {
|
||||
constructor(
|
||||
collection: Collection,
|
||||
filter: Document,
|
||||
replacement: Document,
|
||||
options: FindOneAndReplaceOptions
|
||||
) {
|
||||
if (filter == null || typeof filter !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Argument "filter" must be an object');
|
||||
}
|
||||
|
||||
if (replacement == null || typeof replacement !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Argument "replacement" must be an object');
|
||||
}
|
||||
|
||||
if (hasAtomicOperators(replacement)) {
|
||||
throw new MongoInvalidArgumentError('Replacement document must not contain atomic operators');
|
||||
}
|
||||
|
||||
super(collection, filter, options);
|
||||
this.cmdBase.update = replacement;
|
||||
configureFindAndModifyCmdBaseUpdateOpts(this.cmdBase, options);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class FindOneAndUpdateOperation extends FindAndModifyOperation {
|
||||
constructor(
|
||||
collection: Collection,
|
||||
filter: Document,
|
||||
update: Document,
|
||||
options: FindOneAndUpdateOptions
|
||||
) {
|
||||
if (filter == null || typeof filter !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Argument "filter" must be an object');
|
||||
}
|
||||
|
||||
if (update == null || typeof update !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Argument "update" must be an object');
|
||||
}
|
||||
|
||||
if (!hasAtomicOperators(update)) {
|
||||
throw new MongoInvalidArgumentError('Update document requires atomic operators');
|
||||
}
|
||||
|
||||
super(collection, filter, options);
|
||||
this.cmdBase.update = update;
|
||||
configureFindAndModifyCmdBaseUpdateOpts(this.cmdBase, options);
|
||||
|
||||
if (options.arrayFilters) {
|
||||
this.cmdBase.arrayFilters = options.arrayFilters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(FindAndModifyOperation, [
|
||||
Aspect.WRITE_OPERATION,
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.EXPLAINABLE
|
||||
]);
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import type { Document, Long } from '../bson';
|
||||
import { MongoRuntimeError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, maxWireVersion, MongoDBNamespace } from '../utils';
|
||||
import { AbstractOperation, Aspect, defineAspects, OperationOptions } from './operation';
|
||||
|
||||
/** @internal */
|
||||
export interface GetMoreOptions extends OperationOptions {
|
||||
/** Set the batchSize for the getMoreCommand when iterating over the query results. */
|
||||
batchSize?: number;
|
||||
/**
|
||||
* Comment to apply to the operation.
|
||||
*
|
||||
* getMore only supports 'comment' in server versions 4.4 and above.
|
||||
*/
|
||||
comment?: unknown;
|
||||
/** Number of milliseconds to wait before aborting the query. */
|
||||
maxTimeMS?: number;
|
||||
/** TODO(NODE-4413): Address bug with maxAwaitTimeMS not being passed in from the cursor correctly */
|
||||
maxAwaitTimeMS?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetMore command: https://www.mongodb.com/docs/manual/reference/command/getMore/
|
||||
* @internal
|
||||
*/
|
||||
export interface GetMoreCommand {
|
||||
getMore: Long;
|
||||
collection: string;
|
||||
batchSize?: number;
|
||||
maxTimeMS?: number;
|
||||
/** Only supported on wire versions 10 or greater */
|
||||
comment?: unknown;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class GetMoreOperation extends AbstractOperation {
|
||||
cursorId: Long;
|
||||
override options: GetMoreOptions;
|
||||
|
||||
constructor(ns: MongoDBNamespace, cursorId: Long, server: Server, options: GetMoreOptions) {
|
||||
super(options);
|
||||
|
||||
this.options = options;
|
||||
this.ns = ns;
|
||||
this.cursorId = cursorId;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Although there is a server already associated with the get more operation, the signature
|
||||
* for execute passes a server so we will just use that one.
|
||||
*/
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
if (server !== this.server) {
|
||||
return callback(
|
||||
new MongoRuntimeError('Getmore must run on the same server operation began on')
|
||||
);
|
||||
}
|
||||
|
||||
if (this.cursorId == null || this.cursorId.isZero()) {
|
||||
return callback(new MongoRuntimeError('Unable to iterate cursor with no id'));
|
||||
}
|
||||
|
||||
const collection = this.ns.collection;
|
||||
if (collection == null) {
|
||||
// Cursors should have adopted the namespace returned by MongoDB
|
||||
// which should always defined a collection name (even a pseudo one, ex. db.aggregate())
|
||||
return callback(new MongoRuntimeError('A collection name must be determined before getMore'));
|
||||
}
|
||||
|
||||
const getMoreCmd: GetMoreCommand = {
|
||||
getMore: this.cursorId,
|
||||
collection
|
||||
};
|
||||
|
||||
if (typeof this.options.batchSize === 'number') {
|
||||
getMoreCmd.batchSize = Math.abs(this.options.batchSize);
|
||||
}
|
||||
|
||||
if (typeof this.options.maxAwaitTimeMS === 'number') {
|
||||
getMoreCmd.maxTimeMS = this.options.maxAwaitTimeMS;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (this.options.comment !== undefined && maxWireVersion(server) >= 9) {
|
||||
getMoreCmd.comment = this.options.comment;
|
||||
}
|
||||
|
||||
const commandOptions = {
|
||||
returnFieldSelector: null,
|
||||
documentsReturnedIn: 'nextBatch',
|
||||
...this.options
|
||||
};
|
||||
|
||||
server.command(this.ns, getMoreCmd, commandOptions, callback);
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(GetMoreOperation, [Aspect.READ_OPERATION, Aspect.MUST_SELECT_SAME_SERVER]);
|
||||
+500
@@ -0,0 +1,500 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Db } from '../db';
|
||||
import { MongoCompatibilityError, MONGODB_ERROR_CODES, MongoServerError } from '../error';
|
||||
import type { OneOrMore } from '../mongo_types';
|
||||
import { ReadPreference } from '../read_preference';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, isObject, maxWireVersion, MongoDBNamespace } from '../utils';
|
||||
import {
|
||||
CollationOptions,
|
||||
CommandOperation,
|
||||
CommandOperationOptions,
|
||||
OperationParent
|
||||
} from './command';
|
||||
import { indexInformation, IndexInformationOptions } from './common_functions';
|
||||
import { AbstractOperation, Aspect, defineAspects } from './operation';
|
||||
|
||||
const VALID_INDEX_OPTIONS = new Set([
|
||||
'background',
|
||||
'unique',
|
||||
'name',
|
||||
'partialFilterExpression',
|
||||
'sparse',
|
||||
'hidden',
|
||||
'expireAfterSeconds',
|
||||
'storageEngine',
|
||||
'collation',
|
||||
'version',
|
||||
|
||||
// text indexes
|
||||
'weights',
|
||||
'default_language',
|
||||
'language_override',
|
||||
'textIndexVersion',
|
||||
|
||||
// 2d-sphere indexes
|
||||
'2dsphereIndexVersion',
|
||||
|
||||
// 2d indexes
|
||||
'bits',
|
||||
'min',
|
||||
'max',
|
||||
|
||||
// geoHaystack Indexes
|
||||
'bucketSize',
|
||||
|
||||
// wildcard indexes
|
||||
'wildcardProjection'
|
||||
]);
|
||||
|
||||
/** @public */
|
||||
export type IndexDirection = -1 | 1 | '2d' | '2dsphere' | 'text' | 'geoHaystack' | number;
|
||||
function isIndexDirection(x: unknown): x is IndexDirection {
|
||||
return (
|
||||
typeof x === 'number' || x === '2d' || x === '2dsphere' || x === 'text' || x === 'geoHaystack'
|
||||
);
|
||||
}
|
||||
/** @public */
|
||||
export type IndexSpecification = OneOrMore<
|
||||
| string
|
||||
| [string, IndexDirection]
|
||||
| { [key: string]: IndexDirection }
|
||||
| Map<string, IndexDirection>
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
export interface IndexDescription
|
||||
extends Pick<
|
||||
CreateIndexesOptions,
|
||||
| 'background'
|
||||
| 'unique'
|
||||
| 'partialFilterExpression'
|
||||
| 'sparse'
|
||||
| 'hidden'
|
||||
| 'expireAfterSeconds'
|
||||
| 'storageEngine'
|
||||
| 'version'
|
||||
| 'weights'
|
||||
| 'default_language'
|
||||
| 'language_override'
|
||||
| 'textIndexVersion'
|
||||
| '2dsphereIndexVersion'
|
||||
| 'bits'
|
||||
| 'min'
|
||||
| 'max'
|
||||
| 'bucketSize'
|
||||
| 'wildcardProjection'
|
||||
> {
|
||||
collation?: CollationOptions;
|
||||
name?: string;
|
||||
key: { [key: string]: IndexDirection } | Map<string, IndexDirection>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CreateIndexesOptions extends CommandOperationOptions {
|
||||
/** Creates the index in the background, yielding whenever possible. */
|
||||
background?: boolean;
|
||||
/** Creates an unique index. */
|
||||
unique?: boolean;
|
||||
/** Override the autogenerated index name (useful if the resulting name is larger than 128 bytes) */
|
||||
name?: string;
|
||||
/** Creates a partial index based on the given filter object (MongoDB 3.2 or higher) */
|
||||
partialFilterExpression?: Document;
|
||||
/** Creates a sparse index. */
|
||||
sparse?: boolean;
|
||||
/** Allows you to expire data on indexes applied to a data (MongoDB 2.2 or higher) */
|
||||
expireAfterSeconds?: number;
|
||||
/** Allows users to configure the storage engine on a per-index basis when creating an index. (MongoDB 3.0 or higher) */
|
||||
storageEngine?: Document;
|
||||
/** (MongoDB 4.4. or higher) Specifies how many data-bearing members of a replica set, including the primary, must complete the index builds successfully before the primary marks the indexes as ready. This option accepts the same values for the "w" field in a write concern plus "votingMembers", which indicates all voting data-bearing nodes. */
|
||||
commitQuorum?: number | string;
|
||||
/** Specifies the index version number, either 0 or 1. */
|
||||
version?: number;
|
||||
// text indexes
|
||||
weights?: Document;
|
||||
default_language?: string;
|
||||
language_override?: string;
|
||||
textIndexVersion?: number;
|
||||
// 2d-sphere indexes
|
||||
'2dsphereIndexVersion'?: number;
|
||||
// 2d indexes
|
||||
bits?: number;
|
||||
/** For geospatial indexes set the lower bound for the co-ordinates. */
|
||||
min?: number;
|
||||
/** For geospatial indexes set the high bound for the co-ordinates. */
|
||||
max?: number;
|
||||
// geoHaystack Indexes
|
||||
bucketSize?: number;
|
||||
// wildcard indexes
|
||||
wildcardProjection?: Document;
|
||||
/** Specifies that the index should exist on the target collection but should not be used by the query planner when executing operations. (MongoDB 4.4 or higher) */
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
function isSingleIndexTuple(t: unknown): t is [string, IndexDirection] {
|
||||
return Array.isArray(t) && t.length === 2 && isIndexDirection(t[1]);
|
||||
}
|
||||
|
||||
function makeIndexSpec(
|
||||
indexSpec: IndexSpecification,
|
||||
options?: CreateIndexesOptions
|
||||
): IndexDescription {
|
||||
const key: Map<string, IndexDirection> = new Map();
|
||||
|
||||
const indexSpecs =
|
||||
!Array.isArray(indexSpec) || isSingleIndexTuple(indexSpec) ? [indexSpec] : indexSpec;
|
||||
|
||||
// Iterate through array and handle different types
|
||||
for (const spec of indexSpecs) {
|
||||
if (typeof spec === 'string') {
|
||||
key.set(spec, 1);
|
||||
} else if (Array.isArray(spec)) {
|
||||
key.set(spec[0], spec[1] ?? 1);
|
||||
} else if (spec instanceof Map) {
|
||||
for (const [property, value] of spec) {
|
||||
key.set(property, value);
|
||||
}
|
||||
} else if (isObject(spec)) {
|
||||
for (const [property, value] of Object.entries(spec)) {
|
||||
key.set(property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ...options, key };
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class IndexesOperation extends AbstractOperation<Document[]> {
|
||||
override options: IndexInformationOptions;
|
||||
collection: Collection;
|
||||
|
||||
constructor(collection: Collection, options: IndexInformationOptions) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document[]>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
const options = this.options;
|
||||
|
||||
indexInformation(
|
||||
coll.s.db,
|
||||
coll.collectionName,
|
||||
{ full: true, ...options, readPreference: this.readPreference, session },
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class CreateIndexesOperation<
|
||||
T extends string | string[] = string[]
|
||||
> extends CommandOperation<T> {
|
||||
override options: CreateIndexesOptions;
|
||||
collectionName: string;
|
||||
indexes: ReadonlyArray<Omit<IndexDescription, 'key'> & { key: Map<string, IndexDirection> }>;
|
||||
|
||||
constructor(
|
||||
parent: OperationParent,
|
||||
collectionName: string,
|
||||
indexes: IndexDescription[],
|
||||
options?: CreateIndexesOptions
|
||||
) {
|
||||
super(parent, options);
|
||||
|
||||
this.options = options ?? {};
|
||||
this.collectionName = collectionName;
|
||||
this.indexes = indexes.map(userIndex => {
|
||||
// Ensure the key is a Map to preserve index key ordering
|
||||
const key =
|
||||
userIndex.key instanceof Map ? userIndex.key : new Map(Object.entries(userIndex.key));
|
||||
const name = userIndex.name != null ? userIndex.name : Array.from(key).flat().join('_');
|
||||
const validIndexOptions = Object.fromEntries(
|
||||
Object.entries({ ...userIndex }).filter(([optionName]) =>
|
||||
VALID_INDEX_OPTIONS.has(optionName)
|
||||
)
|
||||
);
|
||||
return {
|
||||
...validIndexOptions,
|
||||
name,
|
||||
key
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<T>
|
||||
): void {
|
||||
const options = this.options;
|
||||
const indexes = this.indexes;
|
||||
|
||||
const serverWireVersion = maxWireVersion(server);
|
||||
|
||||
const cmd: Document = { createIndexes: this.collectionName, indexes };
|
||||
|
||||
if (options.commitQuorum != null) {
|
||||
if (serverWireVersion < 9) {
|
||||
callback(
|
||||
new MongoCompatibilityError(
|
||||
'Option `commitQuorum` for `createIndexes` not supported on servers < 4.4'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
cmd.commitQuorum = options.commitQuorum;
|
||||
}
|
||||
|
||||
// collation is set on each index, it should not be defined at the root
|
||||
this.options.collation = undefined;
|
||||
|
||||
super.executeCommand(server, session, cmd, err => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const indexNames = indexes.map(index => index.name || '');
|
||||
callback(undefined, indexNames as T);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class CreateIndexOperation extends CreateIndexesOperation<string> {
|
||||
constructor(
|
||||
parent: OperationParent,
|
||||
collectionName: string,
|
||||
indexSpec: IndexSpecification,
|
||||
options?: CreateIndexesOptions
|
||||
) {
|
||||
super(parent, collectionName, [makeIndexSpec(indexSpec, options)], options);
|
||||
}
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
super.execute(server, session, (err, indexNames) => {
|
||||
if (err || !indexNames) return callback(err);
|
||||
return callback(undefined, indexNames[0]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class EnsureIndexOperation extends CreateIndexOperation {
|
||||
db: Db;
|
||||
|
||||
constructor(
|
||||
db: Db,
|
||||
collectionName: string,
|
||||
indexSpec: IndexSpecification,
|
||||
options?: CreateIndexesOptions
|
||||
) {
|
||||
super(db, collectionName, indexSpec, options);
|
||||
|
||||
this.readPreference = ReadPreference.primary;
|
||||
this.db = db;
|
||||
this.collectionName = collectionName;
|
||||
}
|
||||
|
||||
override execute(server: Server, session: ClientSession | undefined, callback: Callback): void {
|
||||
const indexName = this.indexes[0].name;
|
||||
const cursor = this.db.collection(this.collectionName).listIndexes({ session });
|
||||
cursor.toArray((err, indexes) => {
|
||||
/// ignore "NamespaceNotFound" errors
|
||||
if (err && (err as MongoServerError).code !== MONGODB_ERROR_CODES.NamespaceNotFound) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (indexes) {
|
||||
indexes = Array.isArray(indexes) ? indexes : [indexes];
|
||||
if (indexes.some(index => index.name === indexName)) {
|
||||
callback(undefined, indexName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.execute(server, session, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type DropIndexesOptions = CommandOperationOptions;
|
||||
|
||||
/** @internal */
|
||||
export class DropIndexOperation extends CommandOperation<Document> {
|
||||
override options: DropIndexesOptions;
|
||||
collection: Collection;
|
||||
indexName: string;
|
||||
|
||||
constructor(collection: Collection, indexName: string, options?: DropIndexesOptions) {
|
||||
super(collection, options);
|
||||
|
||||
this.options = options ?? {};
|
||||
this.collection = collection;
|
||||
this.indexName = indexName;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const cmd = { dropIndexes: this.collection.collectionName, index: this.indexName };
|
||||
super.executeCommand(server, session, cmd, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class DropIndexesOperation extends DropIndexOperation {
|
||||
constructor(collection: Collection, options: DropIndexesOptions) {
|
||||
super(collection, '*', options);
|
||||
}
|
||||
|
||||
override execute(server: Server, session: ClientSession | undefined, callback: Callback): void {
|
||||
super.execute(server, session, err => {
|
||||
if (err) return callback(err, false);
|
||||
callback(undefined, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ListIndexesOptions extends CommandOperationOptions {
|
||||
/** The batchSize for the returned command cursor or if pre 2.8 the systems batch collection */
|
||||
batchSize?: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ListIndexesOperation extends CommandOperation<Document> {
|
||||
override options: ListIndexesOptions;
|
||||
collectionNamespace: MongoDBNamespace;
|
||||
|
||||
constructor(collection: Collection, options?: ListIndexesOptions) {
|
||||
super(collection, options);
|
||||
|
||||
this.options = options ?? {};
|
||||
this.collectionNamespace = collection.s.namespace;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const serverWireVersion = maxWireVersion(server);
|
||||
|
||||
const cursor = this.options.batchSize ? { batchSize: this.options.batchSize } : {};
|
||||
|
||||
const command: Document = { listIndexes: this.collectionNamespace.collection, cursor };
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (serverWireVersion >= 9 && this.options.comment !== undefined) {
|
||||
command.comment = this.options.comment;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class IndexExistsOperation extends AbstractOperation<boolean> {
|
||||
override options: IndexInformationOptions;
|
||||
collection: Collection;
|
||||
indexes: string | string[];
|
||||
|
||||
constructor(
|
||||
collection: Collection,
|
||||
indexes: string | string[],
|
||||
options: IndexInformationOptions
|
||||
) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
this.indexes = indexes;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<boolean>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
const indexes = this.indexes;
|
||||
|
||||
indexInformation(
|
||||
coll.s.db,
|
||||
coll.collectionName,
|
||||
{ ...this.options, readPreference: this.readPreference, session },
|
||||
(err, indexInformation) => {
|
||||
// If we have an error return
|
||||
if (err != null) return callback(err);
|
||||
// Let's check for the index names
|
||||
if (!Array.isArray(indexes)) return callback(undefined, indexInformation[indexes] != null);
|
||||
// Check in list of indexes
|
||||
for (let i = 0; i < indexes.length; i++) {
|
||||
if (indexInformation[indexes[i]] == null) {
|
||||
return callback(undefined, false);
|
||||
}
|
||||
}
|
||||
|
||||
// All keys found return true
|
||||
return callback(undefined, true);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class IndexInformationOperation extends AbstractOperation<Document> {
|
||||
override options: IndexInformationOptions;
|
||||
db: Db;
|
||||
name: string;
|
||||
|
||||
constructor(db: Db, name: string, options?: IndexInformationOptions) {
|
||||
super(options);
|
||||
this.options = options ?? {};
|
||||
this.db = db;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const db = this.db;
|
||||
const name = this.name;
|
||||
|
||||
indexInformation(
|
||||
db,
|
||||
name,
|
||||
{ ...this.options, readPreference: this.readPreference, session },
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(ListIndexesOperation, [
|
||||
Aspect.READ_OPERATION,
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.CURSOR_CREATING
|
||||
]);
|
||||
defineAspects(CreateIndexesOperation, [Aspect.WRITE_OPERATION]);
|
||||
defineAspects(CreateIndexOperation, [Aspect.WRITE_OPERATION]);
|
||||
defineAspects(EnsureIndexOperation, [Aspect.WRITE_OPERATION]);
|
||||
defineAspects(DropIndexOperation, [Aspect.WRITE_OPERATION]);
|
||||
defineAspects(DropIndexesOperation, [Aspect.WRITE_OPERATION]);
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { BulkWriteOptions } from '../bulk/common';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoInvalidArgumentError, MongoServerError } from '../error';
|
||||
import type { InferIdType } from '../mongo_types';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback, MongoDBNamespace } from '../utils';
|
||||
import { WriteConcern } from '../write_concern';
|
||||
import { BulkWriteOperation } from './bulk_write';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { prepareDocs } from './common_functions';
|
||||
import { AbstractOperation, Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @internal */
|
||||
export class InsertOperation extends CommandOperation<Document> {
|
||||
override options: BulkWriteOptions;
|
||||
documents: Document[];
|
||||
|
||||
constructor(ns: MongoDBNamespace, documents: Document[], options: BulkWriteOptions) {
|
||||
super(undefined, options);
|
||||
this.options = { ...options, checkKeys: options.checkKeys ?? false };
|
||||
this.ns = ns;
|
||||
this.documents = documents;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const options = this.options ?? {};
|
||||
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
const command: Document = {
|
||||
insert: this.ns.collection,
|
||||
documents: this.documents,
|
||||
ordered
|
||||
};
|
||||
|
||||
if (typeof options.bypassDocumentValidation === 'boolean') {
|
||||
command.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (options.comment !== undefined) {
|
||||
command.comment = options.comment;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface InsertOneOptions extends CommandOperationOptions {
|
||||
/** Allow driver to bypass schema validation in MongoDB 3.2 or higher. */
|
||||
bypassDocumentValidation?: boolean;
|
||||
/** Force server to assign _id values instead of driver. */
|
||||
forceServerObjectId?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface InsertOneResult<TSchema = Document> {
|
||||
/** Indicates whether this write result was acknowledged. If not, then all other members of this result will be undefined */
|
||||
acknowledged: boolean;
|
||||
/** The identifier that was inserted. If the server generated the identifier, this value will be null as the driver does not have access to that data */
|
||||
insertedId: InferIdType<TSchema>;
|
||||
}
|
||||
|
||||
export class InsertOneOperation extends InsertOperation {
|
||||
constructor(collection: Collection, doc: Document, options: InsertOneOptions) {
|
||||
super(collection.s.namespace, prepareDocs(collection, [doc], options), options);
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<InsertOneResult>
|
||||
): void {
|
||||
super.execute(server, session, (err, res) => {
|
||||
if (err || res == null) return callback(err);
|
||||
if (res.code) return callback(new MongoServerError(res));
|
||||
if (res.writeErrors) {
|
||||
// This should be a WriteError but we can't change it now because of error hierarchy
|
||||
return callback(new MongoServerError(res.writeErrors[0]));
|
||||
}
|
||||
|
||||
callback(undefined, {
|
||||
acknowledged: this.writeConcern?.w !== 0 ?? true,
|
||||
insertedId: this.documents[0]._id
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface InsertManyResult<TSchema = Document> {
|
||||
/** Indicates whether this write result was acknowledged. If not, then all other members of this result will be undefined */
|
||||
acknowledged: boolean;
|
||||
/** The number of inserted documents for this operations */
|
||||
insertedCount: number;
|
||||
/** Map of the index of the inserted document to the id of the inserted document */
|
||||
insertedIds: { [key: number]: InferIdType<TSchema> };
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class InsertManyOperation extends AbstractOperation<InsertManyResult> {
|
||||
override options: BulkWriteOptions;
|
||||
collection: Collection;
|
||||
docs: Document[];
|
||||
|
||||
constructor(collection: Collection, docs: Document[], options: BulkWriteOptions) {
|
||||
super(options);
|
||||
|
||||
if (!Array.isArray(docs)) {
|
||||
throw new MongoInvalidArgumentError('Argument "docs" must be an array of documents');
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
this.docs = docs;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<InsertManyResult>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
const options = { ...this.options, ...this.bsonOptions, readPreference: this.readPreference };
|
||||
const writeConcern = WriteConcern.fromOptions(options);
|
||||
const bulkWriteOperation = new BulkWriteOperation(
|
||||
coll,
|
||||
prepareDocs(coll, this.docs, options).map(document => ({ insertOne: { document } })),
|
||||
options
|
||||
);
|
||||
|
||||
bulkWriteOperation.execute(server, session, (err, res) => {
|
||||
if (err || res == null) {
|
||||
if (err && err.message === 'Operation must be an object with an operation key') {
|
||||
err = new MongoInvalidArgumentError(
|
||||
'Collection.insertMany() cannot be called with an array that has null/undefined values'
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, {
|
||||
acknowledged: writeConcern?.w !== 0 ?? true,
|
||||
insertedCount: res.insertedCount,
|
||||
insertedIds: res.insertedIds
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(InsertOperation, [Aspect.RETRYABLE, Aspect.WRITE_OPERATION]);
|
||||
defineAspects(InsertOneOperation, [Aspect.RETRYABLE, Aspect.WRITE_OPERATION]);
|
||||
defineAspects(InsertManyOperation, [Aspect.WRITE_OPERATION]);
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoAPIError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { AbstractOperation, OperationOptions } from './operation';
|
||||
|
||||
/** @internal */
|
||||
export class IsCappedOperation extends AbstractOperation<boolean> {
|
||||
override options: OperationOptions;
|
||||
collection: Collection;
|
||||
|
||||
constructor(collection: Collection, options: OperationOptions) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<boolean>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
|
||||
coll.s.db
|
||||
.listCollections(
|
||||
{ name: coll.collectionName },
|
||||
{ ...this.options, nameOnly: false, readPreference: this.readPreference, session }
|
||||
)
|
||||
.toArray((err, collections) => {
|
||||
if (err || !collections) return callback(err);
|
||||
if (collections.length === 0) {
|
||||
// TODO(NODE-3485)
|
||||
return callback(new MongoAPIError(`collection ${coll.namespace} not found`));
|
||||
}
|
||||
|
||||
const collOptions = collections[0].options;
|
||||
callback(undefined, !!(collOptions && collOptions.capped));
|
||||
});
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import type { Long } from '../bson';
|
||||
import { MongoRuntimeError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback, MongoDBNamespace } from '../utils';
|
||||
import { AbstractOperation, Aspect, defineAspects, OperationOptions } from './operation';
|
||||
|
||||
/**
|
||||
* https://www.mongodb.com/docs/manual/reference/command/killCursors/
|
||||
* @internal
|
||||
*/
|
||||
interface KillCursorsCommand {
|
||||
killCursors: string;
|
||||
cursors: Long[];
|
||||
comment?: unknown;
|
||||
}
|
||||
|
||||
export class KillCursorsOperation extends AbstractOperation {
|
||||
cursorId: Long;
|
||||
|
||||
constructor(cursorId: Long, ns: MongoDBNamespace, server: Server, options: OperationOptions) {
|
||||
super(options);
|
||||
this.ns = ns;
|
||||
this.cursorId = cursorId;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
execute(server: Server, session: ClientSession | undefined, callback: Callback<void>): void {
|
||||
if (server !== this.server) {
|
||||
return callback(
|
||||
new MongoRuntimeError('Killcursor must run on the same server operation began on')
|
||||
);
|
||||
}
|
||||
|
||||
const killCursors = this.ns.collection;
|
||||
if (killCursors == null) {
|
||||
// Cursors should have adopted the namespace returned by MongoDB
|
||||
// which should always defined a collection name (even a pseudo one, ex. db.aggregate())
|
||||
return callback(
|
||||
new MongoRuntimeError('A collection name must be determined before killCursors')
|
||||
);
|
||||
}
|
||||
|
||||
const killCursorsCommand: KillCursorsCommand = {
|
||||
killCursors,
|
||||
cursors: [this.cursorId]
|
||||
};
|
||||
|
||||
server.command(this.ns, killCursorsCommand, { session }, () => callback());
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(KillCursorsOperation, [Aspect.MUST_SELECT_SAME_SERVER]);
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import type { Binary, Document } from '../bson';
|
||||
import type { Db } from '../db';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, maxWireVersion } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface ListCollectionsOptions extends CommandOperationOptions {
|
||||
/** Since 4.0: If true, will only return the collection name in the response, and will omit additional info */
|
||||
nameOnly?: boolean;
|
||||
/** Since 4.0: If true and nameOnly is true, allows a user without the required privilege (i.e. listCollections action on the database) to run the command when access control is enforced. */
|
||||
authorizedCollections?: boolean;
|
||||
/** The batchSize for the returned command cursor or if pre 2.8 the systems batch collection */
|
||||
batchSize?: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ListCollectionsOperation extends CommandOperation<string[]> {
|
||||
override options: ListCollectionsOptions;
|
||||
db: Db;
|
||||
filter: Document;
|
||||
nameOnly: boolean;
|
||||
authorizedCollections: boolean;
|
||||
batchSize?: number;
|
||||
|
||||
constructor(db: Db, filter: Document, options?: ListCollectionsOptions) {
|
||||
super(db, options);
|
||||
|
||||
this.options = options ?? {};
|
||||
this.db = db;
|
||||
this.filter = filter;
|
||||
this.nameOnly = !!this.options.nameOnly;
|
||||
this.authorizedCollections = !!this.options.authorizedCollections;
|
||||
|
||||
if (typeof this.options.batchSize === 'number') {
|
||||
this.batchSize = this.options.batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<string[]>
|
||||
): void {
|
||||
return super.executeCommand(
|
||||
server,
|
||||
session,
|
||||
this.generateCommand(maxWireVersion(server)),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/* This is here for the purpose of unit testing the final command that gets sent. */
|
||||
generateCommand(wireVersion: number): Document {
|
||||
const command: Document = {
|
||||
listCollections: 1,
|
||||
filter: this.filter,
|
||||
cursor: this.batchSize ? { batchSize: this.batchSize } : {},
|
||||
nameOnly: this.nameOnly,
|
||||
authorizedCollections: this.authorizedCollections
|
||||
};
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (wireVersion >= 9 && this.options.comment !== undefined) {
|
||||
command.comment = this.options.comment;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CollectionInfo extends Document {
|
||||
name: string;
|
||||
type?: string;
|
||||
options?: Document;
|
||||
info?: {
|
||||
readOnly?: false;
|
||||
uuid?: Binary;
|
||||
};
|
||||
idIndex?: Document;
|
||||
}
|
||||
|
||||
defineAspects(ListCollectionsOperation, [
|
||||
Aspect.READ_OPERATION,
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.CURSOR_CREATING
|
||||
]);
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Db } from '../db';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, maxWireVersion, MongoDBNamespace } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface ListDatabasesResult {
|
||||
databases: ({ name: string; sizeOnDisk?: number; empty?: boolean } & Document)[];
|
||||
totalSize?: number;
|
||||
totalSizeMb?: number;
|
||||
ok: 1 | 0;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ListDatabasesOptions extends CommandOperationOptions {
|
||||
/** A query predicate that determines which databases are listed */
|
||||
filter?: Document;
|
||||
/** A flag to indicate whether the command should return just the database names, or return both database names and size information */
|
||||
nameOnly?: boolean;
|
||||
/** A flag that determines which databases are returned based on the user privileges when access control is enabled */
|
||||
authorizedDatabases?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ListDatabasesOperation extends CommandOperation<ListDatabasesResult> {
|
||||
override options: ListDatabasesOptions;
|
||||
|
||||
constructor(db: Db, options?: ListDatabasesOptions) {
|
||||
super(db, options);
|
||||
this.options = options ?? {};
|
||||
this.ns = new MongoDBNamespace('admin', '$cmd');
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<ListDatabasesResult>
|
||||
): void {
|
||||
const cmd: Document = { listDatabases: 1 };
|
||||
if (this.options.nameOnly) {
|
||||
cmd.nameOnly = Number(cmd.nameOnly);
|
||||
}
|
||||
|
||||
if (this.options.filter) {
|
||||
cmd.filter = this.options.filter;
|
||||
}
|
||||
|
||||
if (typeof this.options.authorizedDatabases === 'boolean') {
|
||||
cmd.authorizedDatabases = this.options.authorizedDatabases;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (maxWireVersion(server) >= 9 && this.options.comment !== undefined) {
|
||||
cmd.comment = this.options.comment;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, cmd, callback);
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(ListDatabasesOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]);
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import type { ObjectId } from '../bson';
|
||||
import { Code, Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoCompatibilityError, MongoServerError } from '../error';
|
||||
import { ReadPreference, ReadPreferenceMode } from '../read_preference';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Sort } from '../sort';
|
||||
import {
|
||||
applyWriteConcern,
|
||||
Callback,
|
||||
decorateWithCollation,
|
||||
decorateWithReadConcern,
|
||||
isObject,
|
||||
maxWireVersion
|
||||
} from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
const exclusionList = [
|
||||
'explain',
|
||||
'readPreference',
|
||||
'readConcern',
|
||||
'session',
|
||||
'bypassDocumentValidation',
|
||||
'writeConcern',
|
||||
'raw',
|
||||
'fieldsAsRaw',
|
||||
'promoteLongs',
|
||||
'promoteValues',
|
||||
'promoteBuffers',
|
||||
'bsonRegExp',
|
||||
'serializeFunctions',
|
||||
'ignoreUndefined',
|
||||
'enableUtf8Validation',
|
||||
'scope' // this option is reformatted thus exclude the original
|
||||
];
|
||||
|
||||
/** @public */
|
||||
export type MapFunction<TSchema = Document> = (this: TSchema) => void;
|
||||
/** @public */
|
||||
export type ReduceFunction<TKey = ObjectId, TValue = any> = (key: TKey, values: TValue[]) => TValue;
|
||||
/** @public */
|
||||
export type FinalizeFunction<TKey = ObjectId, TValue = Document> = (
|
||||
key: TKey,
|
||||
reducedValue: TValue
|
||||
) => TValue;
|
||||
|
||||
/** @public */
|
||||
export interface MapReduceOptions<TKey = ObjectId, TValue = Document>
|
||||
extends CommandOperationOptions {
|
||||
/** Sets the output target for the map reduce job. */
|
||||
out?: 'inline' | { inline: 1 } | { replace: string } | { merge: string } | { reduce: string };
|
||||
/** Query filter object. */
|
||||
query?: Document;
|
||||
/** Sorts the input objects using this key. Useful for optimization, like sorting by the emit key for fewer reduces. */
|
||||
sort?: Sort;
|
||||
/** Number of objects to return from collection. */
|
||||
limit?: number;
|
||||
/** Keep temporary data. */
|
||||
keeptemp?: boolean;
|
||||
/** Finalize function. */
|
||||
finalize?: FinalizeFunction<TKey, TValue> | string;
|
||||
/** Can pass in variables that can be access from map/reduce/finalize. */
|
||||
scope?: Document;
|
||||
/** It is possible to make the execution stay in JS. Provided in MongoDB \> 2.0.X. */
|
||||
jsMode?: boolean;
|
||||
/** Provide statistics on job execution time. */
|
||||
verbose?: boolean;
|
||||
/** Allow driver to bypass schema validation in MongoDB 3.2 or higher. */
|
||||
bypassDocumentValidation?: boolean;
|
||||
}
|
||||
|
||||
interface MapReduceStats {
|
||||
processtime?: number;
|
||||
counts?: number;
|
||||
timing?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Map Reduce across a collection. Be aware that the inline option for out will return an array of results not a collection.
|
||||
* @internal
|
||||
*/
|
||||
export class MapReduceOperation extends CommandOperation<Document | Document[]> {
|
||||
override options: MapReduceOptions;
|
||||
collection: Collection;
|
||||
/** The mapping function. */
|
||||
map: MapFunction | string;
|
||||
/** The reduce function. */
|
||||
reduce: ReduceFunction | string;
|
||||
|
||||
/**
|
||||
* Constructs a MapReduce operation.
|
||||
*
|
||||
* @param collection - Collection instance.
|
||||
* @param map - The mapping function.
|
||||
* @param reduce - The reduce function.
|
||||
* @param options - Optional settings. See Collection.prototype.mapReduce for a list of options.
|
||||
*/
|
||||
constructor(
|
||||
collection: Collection,
|
||||
map: MapFunction | string,
|
||||
reduce: ReduceFunction | string,
|
||||
options?: MapReduceOptions
|
||||
) {
|
||||
super(collection, options);
|
||||
|
||||
this.options = options ?? {};
|
||||
this.collection = collection;
|
||||
this.map = map;
|
||||
this.reduce = reduce;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document | Document[]>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
const map = this.map;
|
||||
const reduce = this.reduce;
|
||||
let options = this.options;
|
||||
|
||||
const mapCommandHash: Document = {
|
||||
mapReduce: coll.collectionName,
|
||||
map: map,
|
||||
reduce: reduce
|
||||
};
|
||||
|
||||
if (options.scope) {
|
||||
mapCommandHash.scope = processScope(options.scope);
|
||||
}
|
||||
|
||||
// Add any other options passed in
|
||||
for (const n in options) {
|
||||
// Only include if not in exclusion list
|
||||
if (exclusionList.indexOf(n) === -1) {
|
||||
mapCommandHash[n] = (options as any)[n];
|
||||
}
|
||||
}
|
||||
|
||||
options = Object.assign({}, options);
|
||||
|
||||
// If we have a read preference and inline is not set as output fail hard
|
||||
if (
|
||||
this.readPreference.mode === ReadPreferenceMode.primary &&
|
||||
options.out &&
|
||||
(options.out as any).inline !== 1 &&
|
||||
options.out !== 'inline'
|
||||
) {
|
||||
// Force readPreference to primary
|
||||
options.readPreference = ReadPreference.primary;
|
||||
// Decorate command with writeConcern if supported
|
||||
applyWriteConcern(mapCommandHash, { db: coll.s.db, collection: coll }, options);
|
||||
} else {
|
||||
decorateWithReadConcern(mapCommandHash, coll, options);
|
||||
}
|
||||
|
||||
// Is bypassDocumentValidation specified
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
mapCommandHash.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
// Have we specified collation
|
||||
try {
|
||||
decorateWithCollation(mapCommandHash, coll, options);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (this.explain && maxWireVersion(server) < 9) {
|
||||
callback(
|
||||
new MongoCompatibilityError(`Server ${server.name} does not support explain on mapReduce`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute command
|
||||
super.executeCommand(server, session, mapCommandHash, (err, result) => {
|
||||
if (err) return callback(err);
|
||||
// Check if we have an error
|
||||
if (1 !== result.ok || result.err || result.errmsg) {
|
||||
return callback(new MongoServerError(result));
|
||||
}
|
||||
|
||||
// If an explain option was executed, don't process the server results
|
||||
if (this.explain) return callback(undefined, result);
|
||||
|
||||
// Create statistics value
|
||||
const stats: MapReduceStats = {};
|
||||
if (result.timeMillis) stats['processtime'] = result.timeMillis;
|
||||
if (result.counts) stats['counts'] = result.counts;
|
||||
if (result.timing) stats['timing'] = result.timing;
|
||||
|
||||
// invoked with inline?
|
||||
if (result.results) {
|
||||
// If we wish for no verbosity
|
||||
if (options['verbose'] == null || !options['verbose']) {
|
||||
return callback(undefined, result.results);
|
||||
}
|
||||
|
||||
return callback(undefined, { results: result.results, stats: stats });
|
||||
}
|
||||
|
||||
// The returned collection
|
||||
let collection = null;
|
||||
|
||||
// If we have an object it's a different db
|
||||
if (result.result != null && typeof result.result === 'object') {
|
||||
const doc = result.result;
|
||||
// Return a collection from another db
|
||||
collection = coll.s.db.s.client.db(doc.db, coll.s.db.s.options).collection(doc.collection);
|
||||
} else {
|
||||
// Create a collection object that wraps the result collection
|
||||
collection = coll.s.db.collection(result.result);
|
||||
}
|
||||
|
||||
// If we wish for no verbosity
|
||||
if (options['verbose'] == null || !options['verbose']) {
|
||||
return callback(err, collection);
|
||||
}
|
||||
|
||||
// Return stats as third set of values
|
||||
callback(err, { collection, stats });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Functions that are passed as scope args must be converted to Code instances. */
|
||||
function processScope(scope: Document | ObjectId) {
|
||||
if (!isObject(scope) || (scope as any)._bsontype === 'ObjectID') {
|
||||
return scope;
|
||||
}
|
||||
|
||||
const newScope: Document = {};
|
||||
|
||||
for (const key of Object.keys(scope)) {
|
||||
if ('function' === typeof (scope as Document)[key]) {
|
||||
newScope[key] = new Code(String((scope as Document)[key]));
|
||||
} else if ((scope as Document)[key]._bsontype === 'Code') {
|
||||
newScope[key] = (scope as Document)[key];
|
||||
} else {
|
||||
newScope[key] = processScope((scope as Document)[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return newScope;
|
||||
}
|
||||
|
||||
defineAspects(MapReduceOperation, [Aspect.EXPLAINABLE]);
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { BSONSerializeOptions, Document, resolveBSONOptions } from '../bson';
|
||||
import { ReadPreference, ReadPreferenceLike } from '../read_preference';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback, MongoDBNamespace } from '../utils';
|
||||
|
||||
export const Aspect = {
|
||||
READ_OPERATION: Symbol('READ_OPERATION'),
|
||||
WRITE_OPERATION: Symbol('WRITE_OPERATION'),
|
||||
RETRYABLE: Symbol('RETRYABLE'),
|
||||
EXPLAINABLE: Symbol('EXPLAINABLE'),
|
||||
SKIP_COLLATION: Symbol('SKIP_COLLATION'),
|
||||
CURSOR_CREATING: Symbol('CURSOR_CREATING'),
|
||||
MUST_SELECT_SAME_SERVER: Symbol('MUST_SELECT_SAME_SERVER')
|
||||
} as const;
|
||||
|
||||
/** @public */
|
||||
export type Hint = string | Document;
|
||||
|
||||
export interface OperationConstructor extends Function {
|
||||
aspects?: Set<symbol>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface OperationOptions extends BSONSerializeOptions {
|
||||
/** Specify ClientSession for this command */
|
||||
session?: ClientSession;
|
||||
willRetryWrite?: boolean;
|
||||
|
||||
/** The preferred read preference (ReadPreference.primary, ReadPreference.primary_preferred, ReadPreference.secondary, ReadPreference.secondary_preferred, ReadPreference.nearest). */
|
||||
readPreference?: ReadPreferenceLike;
|
||||
|
||||
/** @internal Hints to `executeOperation` that this operation should not unpin on an ended transaction */
|
||||
bypassPinningCheck?: boolean;
|
||||
omitReadPreference?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
const kSession = Symbol('session');
|
||||
|
||||
/**
|
||||
* This class acts as a parent class for any operation and is responsible for setting this.options,
|
||||
* as well as setting and getting a session.
|
||||
* Additionally, this class implements `hasAspect`, which determines whether an operation has
|
||||
* a specific aspect.
|
||||
* @internal
|
||||
*/
|
||||
export abstract class AbstractOperation<TResult = any> {
|
||||
ns!: MongoDBNamespace;
|
||||
cmd!: Document;
|
||||
readPreference: ReadPreference;
|
||||
server!: Server;
|
||||
bypassPinningCheck: boolean;
|
||||
trySecondaryWrite: boolean;
|
||||
|
||||
// BSON serialization options
|
||||
bsonOptions?: BSONSerializeOptions;
|
||||
|
||||
options: OperationOptions;
|
||||
|
||||
[kSession]: ClientSession | undefined;
|
||||
|
||||
constructor(options: OperationOptions = {}) {
|
||||
this.readPreference = this.hasAspect(Aspect.WRITE_OPERATION)
|
||||
? ReadPreference.primary
|
||||
: ReadPreference.fromOptions(options) ?? ReadPreference.primary;
|
||||
|
||||
// Pull the BSON serialize options from the already-resolved options
|
||||
this.bsonOptions = resolveBSONOptions(options);
|
||||
|
||||
this[kSession] = options.session != null ? options.session : undefined;
|
||||
|
||||
this.options = options;
|
||||
this.bypassPinningCheck = !!options.bypassPinningCheck;
|
||||
this.trySecondaryWrite = false;
|
||||
}
|
||||
|
||||
abstract execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<TResult>
|
||||
): void;
|
||||
|
||||
hasAspect(aspect: symbol): boolean {
|
||||
const ctor = this.constructor as OperationConstructor;
|
||||
if (ctor.aspects == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ctor.aspects.has(aspect);
|
||||
}
|
||||
|
||||
get session(): ClientSession | undefined {
|
||||
return this[kSession];
|
||||
}
|
||||
|
||||
clearSession() {
|
||||
this[kSession] = undefined;
|
||||
}
|
||||
|
||||
get canRetryRead(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
get canRetryWrite(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function defineAspects(
|
||||
operation: OperationConstructor,
|
||||
aspects: symbol | symbol[] | Set<symbol>
|
||||
): Set<symbol> {
|
||||
if (!Array.isArray(aspects) && !(aspects instanceof Set)) {
|
||||
aspects = [aspects];
|
||||
}
|
||||
|
||||
aspects = new Set(aspects);
|
||||
Object.defineProperty(operation, 'aspects', {
|
||||
value: aspects,
|
||||
writable: false
|
||||
});
|
||||
|
||||
return aspects;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoAPIError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { AbstractOperation, OperationOptions } from './operation';
|
||||
|
||||
/** @internal */
|
||||
export class OptionsOperation extends AbstractOperation<Document> {
|
||||
override options: OperationOptions;
|
||||
collection: Collection;
|
||||
|
||||
constructor(collection: Collection, options: OperationOptions) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
|
||||
coll.s.db
|
||||
.listCollections(
|
||||
{ name: coll.collectionName },
|
||||
{ ...this.options, nameOnly: false, readPreference: this.readPreference, session }
|
||||
)
|
||||
.toArray((err, collections) => {
|
||||
if (err || !collections) return callback(err);
|
||||
if (collections.length === 0) {
|
||||
// TODO(NODE-3485)
|
||||
return callback(new MongoAPIError(`collection ${coll.namespace} not found`));
|
||||
}
|
||||
|
||||
callback(err, collections[0].options);
|
||||
});
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import type { Db } from '../db';
|
||||
import { MongoRuntimeError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
|
||||
/** @public */
|
||||
export type ProfilingLevelOptions = CommandOperationOptions;
|
||||
|
||||
/** @internal */
|
||||
export class ProfilingLevelOperation extends CommandOperation<string> {
|
||||
override options: ProfilingLevelOptions;
|
||||
|
||||
constructor(db: Db, options: ProfilingLevelOptions) {
|
||||
super(db, options);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<string>
|
||||
): void {
|
||||
super.executeCommand(server, session, { profile: -1 }, (err, doc) => {
|
||||
if (err == null && doc.ok === 1) {
|
||||
const was = doc.was;
|
||||
if (was === 0) return callback(undefined, 'off');
|
||||
if (was === 1) return callback(undefined, 'slow_only');
|
||||
if (was === 2) return callback(undefined, 'all');
|
||||
// TODO(NODE-3483)
|
||||
return callback(new MongoRuntimeError(`Illegal profiling level value ${was}`));
|
||||
} else {
|
||||
// TODO(NODE-3483): Consider MongoUnexpectedServerResponseError
|
||||
err != null ? callback(err) : callback(new MongoRuntimeError('Error with profile command'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import type { Db } from '../db';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export type RemoveUserOptions = CommandOperationOptions;
|
||||
|
||||
/** @internal */
|
||||
export class RemoveUserOperation extends CommandOperation<boolean> {
|
||||
override options: RemoveUserOptions;
|
||||
username: string;
|
||||
|
||||
constructor(db: Db, username: string, options: RemoveUserOptions) {
|
||||
super(db, options);
|
||||
this.options = options;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<boolean>
|
||||
): void {
|
||||
super.executeCommand(server, session, { dropUser: this.username }, err => {
|
||||
callback(err, err ? false : true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(RemoveUserOperation, [Aspect.WRITE_OPERATION]);
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import type { Document } from '../bson';
|
||||
import { Collection } from '../collection';
|
||||
import { MongoServerError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, checkCollectionName } from '../utils';
|
||||
import type { CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
import { RunAdminCommandOperation } from './run_command';
|
||||
|
||||
/** @public */
|
||||
export interface RenameOptions extends CommandOperationOptions {
|
||||
/** Drop the target name collection if it previously exists. */
|
||||
dropTarget?: boolean;
|
||||
/** Unclear */
|
||||
new_collection?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class RenameOperation extends RunAdminCommandOperation {
|
||||
override options: RenameOptions;
|
||||
collection: Collection;
|
||||
newName: string;
|
||||
|
||||
constructor(collection: Collection, newName: string, options: RenameOptions) {
|
||||
// Check the collection name
|
||||
checkCollectionName(newName);
|
||||
|
||||
// Build the command
|
||||
const renameCollection = collection.namespace;
|
||||
const toCollection = collection.s.namespace.withCollection(newName).toString();
|
||||
const dropTarget = typeof options.dropTarget === 'boolean' ? options.dropTarget : false;
|
||||
const cmd = { renameCollection: renameCollection, to: toCollection, dropTarget: dropTarget };
|
||||
|
||||
super(collection, cmd, options);
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
this.newName = newName;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Collection>
|
||||
): void {
|
||||
const coll = this.collection;
|
||||
|
||||
super.execute(server, session, (err, doc) => {
|
||||
if (err) return callback(err);
|
||||
// We have an error
|
||||
if (doc?.errmsg) {
|
||||
return callback(new MongoServerError(doc));
|
||||
}
|
||||
|
||||
let newColl: Collection<Document>;
|
||||
try {
|
||||
newColl = new Collection(coll.s.db, this.newName, coll.s.options);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(undefined, newColl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineAspects(RenameOperation, [Aspect.WRITE_OPERATION]);
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { Callback, MongoDBNamespace } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions, OperationParent } from './command';
|
||||
|
||||
/** @public */
|
||||
export type RunCommandOptions = CommandOperationOptions;
|
||||
|
||||
/** @internal */
|
||||
export class RunCommandOperation<T = Document> extends CommandOperation<T> {
|
||||
override options: RunCommandOptions;
|
||||
command: Document;
|
||||
|
||||
constructor(parent: OperationParent | undefined, command: Document, options?: RunCommandOptions) {
|
||||
super(parent, options);
|
||||
this.options = options ?? {};
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<T>
|
||||
): void {
|
||||
const command = this.command;
|
||||
this.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export class RunAdminCommandOperation<T = Document> extends RunCommandOperation<T> {
|
||||
constructor(parent: OperationParent | undefined, command: Document, options?: RunCommandOptions) {
|
||||
super(parent, command, options);
|
||||
this.ns = new MongoDBNamespace('admin');
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import type { Db } from '../db';
|
||||
import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { enumToString } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
|
||||
const levelValues = new Set(['off', 'slow_only', 'all']);
|
||||
|
||||
/** @public */
|
||||
export const ProfilingLevel = Object.freeze({
|
||||
off: 'off',
|
||||
slowOnly: 'slow_only',
|
||||
all: 'all'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type ProfilingLevel = typeof ProfilingLevel[keyof typeof ProfilingLevel];
|
||||
|
||||
/** @public */
|
||||
export type SetProfilingLevelOptions = CommandOperationOptions;
|
||||
|
||||
/** @internal */
|
||||
export class SetProfilingLevelOperation extends CommandOperation<ProfilingLevel> {
|
||||
override options: SetProfilingLevelOptions;
|
||||
level: ProfilingLevel;
|
||||
profile: 0 | 1 | 2;
|
||||
|
||||
constructor(db: Db, level: ProfilingLevel, options: SetProfilingLevelOptions) {
|
||||
super(db, options);
|
||||
this.options = options;
|
||||
switch (level) {
|
||||
case ProfilingLevel.off:
|
||||
this.profile = 0;
|
||||
break;
|
||||
case ProfilingLevel.slowOnly:
|
||||
this.profile = 1;
|
||||
break;
|
||||
case ProfilingLevel.all:
|
||||
this.profile = 2;
|
||||
break;
|
||||
default:
|
||||
this.profile = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<ProfilingLevel>
|
||||
): void {
|
||||
const level = this.level;
|
||||
|
||||
if (!levelValues.has(level)) {
|
||||
return callback(
|
||||
new MongoInvalidArgumentError(
|
||||
`Profiling level must be one of "${enumToString(ProfilingLevel)}"`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(NODE-3483): Determine error to put here
|
||||
super.executeCommand(server, session, { profile: this.profile }, (err, doc) => {
|
||||
if (err == null && doc.ok === 1) return callback(undefined, level);
|
||||
return err != null
|
||||
? callback(err)
|
||||
: callback(new MongoRuntimeError('Error with profile command'));
|
||||
});
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import type { Db } from '../db';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface CollStatsOptions extends CommandOperationOptions {
|
||||
/** Divide the returned sizes by scale value. */
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the collection statistics.
|
||||
* @internal
|
||||
*/
|
||||
export class CollStatsOperation extends CommandOperation<Document> {
|
||||
override options: CollStatsOptions;
|
||||
collectionName: string;
|
||||
|
||||
/**
|
||||
* Construct a Stats operation.
|
||||
*
|
||||
* @param collection - Collection instance
|
||||
* @param options - Optional settings. See Collection.prototype.stats for a list of options.
|
||||
*/
|
||||
constructor(collection: Collection, options?: CollStatsOptions) {
|
||||
super(collection, options);
|
||||
this.options = options ?? {};
|
||||
this.collectionName = collection.collectionName;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<CollStats>
|
||||
): void {
|
||||
const command: Document = { collStats: this.collectionName };
|
||||
if (this.options.scale != null) {
|
||||
command.scale = this.options.scale;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface DbStatsOptions extends CommandOperationOptions {
|
||||
/** Divide the returned sizes by scale value. */
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class DbStatsOperation extends CommandOperation<Document> {
|
||||
override options: DbStatsOptions;
|
||||
|
||||
constructor(db: Db, options: DbStatsOptions) {
|
||||
super(db, options);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const command: Document = { dbStats: true };
|
||||
if (this.options.scale != null) {
|
||||
command.scale = this.options.scale;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @see https://docs.mongodb.org/manual/reference/command/collStats/
|
||||
*/
|
||||
export interface CollStats extends Document {
|
||||
/** Namespace */
|
||||
ns: string;
|
||||
/** Number of documents */
|
||||
count: number;
|
||||
/** Collection size in bytes */
|
||||
size: number;
|
||||
/** Average object size in bytes */
|
||||
avgObjSize: number;
|
||||
/** (Pre)allocated space for the collection in bytes */
|
||||
storageSize: number;
|
||||
/** Number of extents (contiguously allocated chunks of datafile space) */
|
||||
numExtents: number;
|
||||
/** Number of indexes */
|
||||
nindexes: number;
|
||||
/** Size of the most recently created extent in bytes */
|
||||
lastExtentSize: number;
|
||||
/** Padding can speed up updates if documents grow */
|
||||
paddingFactor: number;
|
||||
/** A number that indicates the user-set flags on the collection. userFlags only appears when using the mmapv1 storage engine */
|
||||
userFlags?: number;
|
||||
/** Total index size in bytes */
|
||||
totalIndexSize: number;
|
||||
/** Size of specific indexes in bytes */
|
||||
indexSizes: {
|
||||
_id_: number;
|
||||
[index: string]: number;
|
||||
};
|
||||
/** `true` if the collection is capped */
|
||||
capped: boolean;
|
||||
/** The maximum number of documents that may be present in a capped collection */
|
||||
max: number;
|
||||
/** The maximum size of a capped collection */
|
||||
maxSize: number;
|
||||
/** This document contains data reported directly by the WiredTiger engine and other data for internal diagnostic use */
|
||||
wiredTiger?: WiredTigerData;
|
||||
/** The fields in this document are the names of the indexes, while the values themselves are documents that contain statistics for the index provided by the storage engine */
|
||||
indexDetails?: any;
|
||||
ok: number;
|
||||
|
||||
/** The amount of storage available for reuse. The scale argument affects this value. */
|
||||
freeStorageSize?: number;
|
||||
/** An array that contains the names of the indexes that are currently being built on the collection */
|
||||
indexBuilds?: number;
|
||||
/** The sum of the storageSize and totalIndexSize. The scale argument affects this value */
|
||||
totalSize: number;
|
||||
/** The scale value used by the command. */
|
||||
scaleFactor: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface WiredTigerData extends Document {
|
||||
LSM: {
|
||||
'bloom filter false positives': number;
|
||||
'bloom filter hits': number;
|
||||
'bloom filter misses': number;
|
||||
'bloom filter pages evicted from cache': number;
|
||||
'bloom filter pages read into cache': number;
|
||||
'bloom filters in the LSM tree': number;
|
||||
'chunks in the LSM tree': number;
|
||||
'highest merge generation in the LSM tree': number;
|
||||
'queries that could have benefited from a Bloom filter that did not exist': number;
|
||||
'sleep for LSM checkpoint throttle': number;
|
||||
'sleep for LSM merge throttle': number;
|
||||
'total size of bloom filters': number;
|
||||
} & Document;
|
||||
'block-manager': {
|
||||
'allocations requiring file extension': number;
|
||||
'blocks allocated': number;
|
||||
'blocks freed': number;
|
||||
'checkpoint size': number;
|
||||
'file allocation unit size': number;
|
||||
'file bytes available for reuse': number;
|
||||
'file magic number': number;
|
||||
'file major version number': number;
|
||||
'file size in bytes': number;
|
||||
'minor version number': number;
|
||||
};
|
||||
btree: {
|
||||
'btree checkpoint generation': number;
|
||||
'column-store fixed-size leaf pages': number;
|
||||
'column-store internal pages': number;
|
||||
'column-store variable-size RLE encoded values': number;
|
||||
'column-store variable-size deleted values': number;
|
||||
'column-store variable-size leaf pages': number;
|
||||
'fixed-record size': number;
|
||||
'maximum internal page key size': number;
|
||||
'maximum internal page size': number;
|
||||
'maximum leaf page key size': number;
|
||||
'maximum leaf page size': number;
|
||||
'maximum leaf page value size': number;
|
||||
'maximum tree depth': number;
|
||||
'number of key/value pairs': number;
|
||||
'overflow pages': number;
|
||||
'pages rewritten by compaction': number;
|
||||
'row-store internal pages': number;
|
||||
'row-store leaf pages': number;
|
||||
} & Document;
|
||||
cache: {
|
||||
'bytes currently in the cache': number;
|
||||
'bytes read into cache': number;
|
||||
'bytes written from cache': number;
|
||||
'checkpoint blocked page eviction': number;
|
||||
'data source pages selected for eviction unable to be evicted': number;
|
||||
'hazard pointer blocked page eviction': number;
|
||||
'in-memory page passed criteria to be split': number;
|
||||
'in-memory page splits': number;
|
||||
'internal pages evicted': number;
|
||||
'internal pages split during eviction': number;
|
||||
'leaf pages split during eviction': number;
|
||||
'modified pages evicted': number;
|
||||
'overflow pages read into cache': number;
|
||||
'overflow values cached in memory': number;
|
||||
'page split during eviction deepened the tree': number;
|
||||
'page written requiring lookaside records': number;
|
||||
'pages read into cache': number;
|
||||
'pages read into cache requiring lookaside entries': number;
|
||||
'pages requested from the cache': number;
|
||||
'pages written from cache': number;
|
||||
'pages written requiring in-memory restoration': number;
|
||||
'tracked dirty bytes in the cache': number;
|
||||
'unmodified pages evicted': number;
|
||||
} & Document;
|
||||
cache_walk: {
|
||||
'Average difference between current eviction generation when the page was last considered': number;
|
||||
'Average on-disk page image size seen': number;
|
||||
'Clean pages currently in cache': number;
|
||||
'Current eviction generation': number;
|
||||
'Dirty pages currently in cache': number;
|
||||
'Entries in the root page': number;
|
||||
'Internal pages currently in cache': number;
|
||||
'Leaf pages currently in cache': number;
|
||||
'Maximum difference between current eviction generation when the page was last considered': number;
|
||||
'Maximum page size seen': number;
|
||||
'Minimum on-disk page image size seen': number;
|
||||
'On-disk page image sizes smaller than a single allocation unit': number;
|
||||
'Pages created in memory and never written': number;
|
||||
'Pages currently queued for eviction': number;
|
||||
'Pages that could not be queued for eviction': number;
|
||||
'Refs skipped during cache traversal': number;
|
||||
'Size of the root page': number;
|
||||
'Total number of pages currently in cache': number;
|
||||
} & Document;
|
||||
compression: {
|
||||
'compressed pages read': number;
|
||||
'compressed pages written': number;
|
||||
'page written failed to compress': number;
|
||||
'page written was too small to compress': number;
|
||||
'raw compression call failed, additional data available': number;
|
||||
'raw compression call failed, no additional data available': number;
|
||||
'raw compression call succeeded': number;
|
||||
} & Document;
|
||||
cursor: {
|
||||
'bulk-loaded cursor-insert calls': number;
|
||||
'create calls': number;
|
||||
'cursor-insert key and value bytes inserted': number;
|
||||
'cursor-remove key bytes removed': number;
|
||||
'cursor-update value bytes updated': number;
|
||||
'insert calls': number;
|
||||
'next calls': number;
|
||||
'prev calls': number;
|
||||
'remove calls': number;
|
||||
'reset calls': number;
|
||||
'restarted searches': number;
|
||||
'search calls': number;
|
||||
'search near calls': number;
|
||||
'truncate calls': number;
|
||||
'update calls': number;
|
||||
};
|
||||
reconciliation: {
|
||||
'dictionary matches': number;
|
||||
'fast-path pages deleted': number;
|
||||
'internal page key bytes discarded using suffix compression': number;
|
||||
'internal page multi-block writes': number;
|
||||
'internal-page overflow keys': number;
|
||||
'leaf page key bytes discarded using prefix compression': number;
|
||||
'leaf page multi-block writes': number;
|
||||
'leaf-page overflow keys': number;
|
||||
'maximum blocks required for a page': number;
|
||||
'overflow values written': number;
|
||||
'page checksum matches': number;
|
||||
'page reconciliation calls': number;
|
||||
'page reconciliation calls for eviction': number;
|
||||
'pages deleted': number;
|
||||
} & Document;
|
||||
}
|
||||
|
||||
defineAspects(CollStatsOperation, [Aspect.READ_OPERATION]);
|
||||
defineAspects(DbStatsOperation, [Aspect.READ_OPERATION]);
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
import type { Document, ObjectId } from '../bson';
|
||||
import type { Collection } from '../collection';
|
||||
import { MongoCompatibilityError, MongoInvalidArgumentError, MongoServerError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import {
|
||||
Callback,
|
||||
collationNotSupported,
|
||||
hasAtomicOperators,
|
||||
maxWireVersion,
|
||||
MongoDBNamespace
|
||||
} from '../utils';
|
||||
import { CollationOptions, CommandOperation, CommandOperationOptions } from './command';
|
||||
import { Aspect, defineAspects, Hint } from './operation';
|
||||
|
||||
/** @public */
|
||||
export interface UpdateOptions extends CommandOperationOptions {
|
||||
/** A set of filters specifying to which array elements an update should apply */
|
||||
arrayFilters?: Document[];
|
||||
/** If true, allows the write to opt-out of document level validation */
|
||||
bypassDocumentValidation?: boolean;
|
||||
/** Specifies a collation */
|
||||
collation?: CollationOptions;
|
||||
/** Specify that the update query should only consider plans using the hinted index */
|
||||
hint?: string | Document;
|
||||
/** When true, creates a new document if no document matches the query */
|
||||
upsert?: boolean;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface UpdateResult {
|
||||
/** Indicates whether this write result was acknowledged. If not, then all other members of this result will be undefined */
|
||||
acknowledged: boolean;
|
||||
/** The number of documents that matched the filter */
|
||||
matchedCount: number;
|
||||
/** The number of documents that were modified */
|
||||
modifiedCount: number;
|
||||
/** The number of documents that were upserted */
|
||||
upsertedCount: number;
|
||||
/** The identifier of the inserted document if an upsert took place */
|
||||
upsertedId: ObjectId;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface UpdateStatement {
|
||||
/** The query that matches documents to update. */
|
||||
q: Document;
|
||||
/** The modifications to apply. */
|
||||
u: Document | Document[];
|
||||
/** If true, perform an insert if no documents match the query. */
|
||||
upsert?: boolean;
|
||||
/** If true, updates all documents that meet the query criteria. */
|
||||
multi?: boolean;
|
||||
/** Specifies the collation to use for the operation. */
|
||||
collation?: CollationOptions;
|
||||
/** An array of filter documents that determines which array elements to modify for an update operation on an array field. */
|
||||
arrayFilters?: Document[];
|
||||
/** A document or string that specifies the index to use to support the query predicate. */
|
||||
hint?: Hint;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class UpdateOperation extends CommandOperation<Document> {
|
||||
override options: UpdateOptions & { ordered?: boolean };
|
||||
statements: UpdateStatement[];
|
||||
|
||||
constructor(
|
||||
ns: MongoDBNamespace,
|
||||
statements: UpdateStatement[],
|
||||
options: UpdateOptions & { ordered?: boolean }
|
||||
) {
|
||||
super(undefined, options);
|
||||
this.options = options;
|
||||
this.ns = ns;
|
||||
|
||||
this.statements = statements;
|
||||
}
|
||||
|
||||
override get canRetryWrite(): boolean {
|
||||
if (super.canRetryWrite === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.statements.every(op => op.multi == null || op.multi === false);
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const options = this.options ?? {};
|
||||
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
const command: Document = {
|
||||
update: this.ns.collection,
|
||||
updates: this.statements,
|
||||
ordered
|
||||
};
|
||||
|
||||
if (typeof options.bypassDocumentValidation === 'boolean') {
|
||||
command.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
if (options.let) {
|
||||
command.let = options.let;
|
||||
}
|
||||
|
||||
// we check for undefined specifically here to allow falsy values
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (options.comment !== undefined) {
|
||||
command.comment = options.comment;
|
||||
}
|
||||
|
||||
const statementWithCollation = this.statements.find(statement => !!statement.collation);
|
||||
if (
|
||||
collationNotSupported(server, options) ||
|
||||
(statementWithCollation && collationNotSupported(server, statementWithCollation))
|
||||
) {
|
||||
callback(new MongoCompatibilityError(`Server ${server.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
const unacknowledgedWrite = this.writeConcern && this.writeConcern.w === 0;
|
||||
if (unacknowledgedWrite || maxWireVersion(server) < 5) {
|
||||
if (this.statements.find((o: Document) => o.hint)) {
|
||||
callback(new MongoCompatibilityError(`Servers < 3.4 do not support hint on update`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.explain && maxWireVersion(server) < 3) {
|
||||
callback(
|
||||
new MongoCompatibilityError(`Server ${server.name} does not support explain on update`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.statements.some(statement => !!statement.arrayFilters) && maxWireVersion(server) < 6) {
|
||||
callback(
|
||||
new MongoCompatibilityError('Option "arrayFilters" is only supported on MongoDB 3.6+')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
super.executeCommand(server, session, command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class UpdateOneOperation extends UpdateOperation {
|
||||
constructor(collection: Collection, filter: Document, update: Document, options: UpdateOptions) {
|
||||
super(
|
||||
collection.s.namespace,
|
||||
[makeUpdateStatement(filter, update, { ...options, multi: false })],
|
||||
options
|
||||
);
|
||||
|
||||
if (!hasAtomicOperators(update)) {
|
||||
throw new MongoInvalidArgumentError('Update document requires atomic operators');
|
||||
}
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<UpdateResult | Document>
|
||||
): void {
|
||||
super.execute(server, session, (err, res) => {
|
||||
if (err || !res) return callback(err);
|
||||
if (this.explain != null) return callback(undefined, res);
|
||||
if (res.code) return callback(new MongoServerError(res));
|
||||
if (res.writeErrors) return callback(new MongoServerError(res.writeErrors[0]));
|
||||
|
||||
callback(undefined, {
|
||||
acknowledged: this.writeConcern?.w !== 0 ?? true,
|
||||
modifiedCount: res.nModified != null ? res.nModified : res.n,
|
||||
upsertedId:
|
||||
Array.isArray(res.upserted) && res.upserted.length > 0 ? res.upserted[0]._id : null,
|
||||
upsertedCount: Array.isArray(res.upserted) && res.upserted.length ? res.upserted.length : 0,
|
||||
matchedCount: Array.isArray(res.upserted) && res.upserted.length > 0 ? 0 : res.n
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class UpdateManyOperation extends UpdateOperation {
|
||||
constructor(collection: Collection, filter: Document, update: Document, options: UpdateOptions) {
|
||||
super(
|
||||
collection.s.namespace,
|
||||
[makeUpdateStatement(filter, update, { ...options, multi: true })],
|
||||
options
|
||||
);
|
||||
|
||||
if (!hasAtomicOperators(update)) {
|
||||
throw new MongoInvalidArgumentError('Update document requires atomic operators');
|
||||
}
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<UpdateResult | Document>
|
||||
): void {
|
||||
super.execute(server, session, (err, res) => {
|
||||
if (err || !res) return callback(err);
|
||||
if (this.explain != null) return callback(undefined, res);
|
||||
if (res.code) return callback(new MongoServerError(res));
|
||||
if (res.writeErrors) return callback(new MongoServerError(res.writeErrors[0]));
|
||||
|
||||
callback(undefined, {
|
||||
acknowledged: this.writeConcern?.w !== 0 ?? true,
|
||||
modifiedCount: res.nModified != null ? res.nModified : res.n,
|
||||
upsertedId:
|
||||
Array.isArray(res.upserted) && res.upserted.length > 0 ? res.upserted[0]._id : null,
|
||||
upsertedCount: Array.isArray(res.upserted) && res.upserted.length ? res.upserted.length : 0,
|
||||
matchedCount: Array.isArray(res.upserted) && res.upserted.length > 0 ? 0 : res.n
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ReplaceOptions extends CommandOperationOptions {
|
||||
/** If true, allows the write to opt-out of document level validation */
|
||||
bypassDocumentValidation?: boolean;
|
||||
/** Specifies a collation */
|
||||
collation?: CollationOptions;
|
||||
/** Specify that the update query should only consider plans using the hinted index */
|
||||
hint?: string | Document;
|
||||
/** When true, creates a new document if no document matches the query */
|
||||
upsert?: boolean;
|
||||
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
|
||||
let?: Document;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ReplaceOneOperation extends UpdateOperation {
|
||||
constructor(
|
||||
collection: Collection,
|
||||
filter: Document,
|
||||
replacement: Document,
|
||||
options: ReplaceOptions
|
||||
) {
|
||||
super(
|
||||
collection.s.namespace,
|
||||
[makeUpdateStatement(filter, replacement, { ...options, multi: false })],
|
||||
options
|
||||
);
|
||||
|
||||
if (hasAtomicOperators(replacement)) {
|
||||
throw new MongoInvalidArgumentError('Replacement document must not contain atomic operators');
|
||||
}
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<UpdateResult | Document>
|
||||
): void {
|
||||
super.execute(server, session, (err, res) => {
|
||||
if (err || !res) return callback(err);
|
||||
if (this.explain != null) return callback(undefined, res);
|
||||
if (res.code) return callback(new MongoServerError(res));
|
||||
if (res.writeErrors) return callback(new MongoServerError(res.writeErrors[0]));
|
||||
|
||||
callback(undefined, {
|
||||
acknowledged: this.writeConcern?.w !== 0 ?? true,
|
||||
modifiedCount: res.nModified != null ? res.nModified : res.n,
|
||||
upsertedId:
|
||||
Array.isArray(res.upserted) && res.upserted.length > 0 ? res.upserted[0]._id : null,
|
||||
upsertedCount: Array.isArray(res.upserted) && res.upserted.length ? res.upserted.length : 0,
|
||||
matchedCount: Array.isArray(res.upserted) && res.upserted.length > 0 ? 0 : res.n
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function makeUpdateStatement(
|
||||
filter: Document,
|
||||
update: Document,
|
||||
options: UpdateOptions & { multi?: boolean }
|
||||
): UpdateStatement {
|
||||
if (filter == null || typeof filter !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Selector must be a valid JavaScript object');
|
||||
}
|
||||
|
||||
if (update == null || typeof update !== 'object') {
|
||||
throw new MongoInvalidArgumentError('Document must be a valid JavaScript object');
|
||||
}
|
||||
|
||||
const op: UpdateStatement = { q: filter, u: update };
|
||||
if (typeof options.upsert === 'boolean') {
|
||||
op.upsert = options.upsert;
|
||||
}
|
||||
|
||||
if (options.multi) {
|
||||
op.multi = options.multi;
|
||||
}
|
||||
|
||||
if (options.hint) {
|
||||
op.hint = options.hint;
|
||||
}
|
||||
|
||||
if (options.arrayFilters) {
|
||||
op.arrayFilters = options.arrayFilters;
|
||||
}
|
||||
|
||||
if (options.collation) {
|
||||
op.collation = options.collation;
|
||||
}
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
defineAspects(UpdateOperation, [Aspect.RETRYABLE, Aspect.WRITE_OPERATION, Aspect.SKIP_COLLATION]);
|
||||
defineAspects(UpdateOneOperation, [
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.WRITE_OPERATION,
|
||||
Aspect.EXPLAINABLE,
|
||||
Aspect.SKIP_COLLATION
|
||||
]);
|
||||
defineAspects(UpdateManyOperation, [
|
||||
Aspect.WRITE_OPERATION,
|
||||
Aspect.EXPLAINABLE,
|
||||
Aspect.SKIP_COLLATION
|
||||
]);
|
||||
defineAspects(ReplaceOneOperation, [
|
||||
Aspect.RETRYABLE,
|
||||
Aspect.WRITE_OPERATION,
|
||||
Aspect.SKIP_COLLATION
|
||||
]);
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import type { Admin } from '../admin';
|
||||
import type { Document } from '../bson';
|
||||
import { MongoRuntimeError } from '../error';
|
||||
import type { Server } from '../sdam/server';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Callback } from '../utils';
|
||||
import { CommandOperation, CommandOperationOptions } from './command';
|
||||
|
||||
/** @public */
|
||||
export interface ValidateCollectionOptions extends CommandOperationOptions {
|
||||
/** Validates a collection in the background, without interrupting read or write traffic (only in MongoDB 4.4+) */
|
||||
background?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ValidateCollectionOperation extends CommandOperation<Document> {
|
||||
override options: ValidateCollectionOptions;
|
||||
collectionName: string;
|
||||
command: Document;
|
||||
|
||||
constructor(admin: Admin, collectionName: string, options: ValidateCollectionOptions) {
|
||||
// Decorate command with extra options
|
||||
const command: Document = { validate: collectionName };
|
||||
const keys = Object.keys(options);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (Object.prototype.hasOwnProperty.call(options, keys[i]) && keys[i] !== 'session') {
|
||||
command[keys[i]] = (options as Document)[keys[i]];
|
||||
}
|
||||
}
|
||||
|
||||
super(admin.s.db, options);
|
||||
this.options = options;
|
||||
this.command = command;
|
||||
this.collectionName = collectionName;
|
||||
}
|
||||
|
||||
override execute(
|
||||
server: Server,
|
||||
session: ClientSession | undefined,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
const collectionName = this.collectionName;
|
||||
|
||||
super.executeCommand(server, session, this.command, (err, doc) => {
|
||||
if (err != null) return callback(err);
|
||||
|
||||
// TODO(NODE-3483): Replace these with MongoUnexpectedServerResponseError
|
||||
if (doc.ok === 0) return callback(new MongoRuntimeError('Error with validate command'));
|
||||
if (doc.result != null && typeof doc.result !== 'string')
|
||||
return callback(new MongoRuntimeError('Error with validation data'));
|
||||
if (doc.result != null && doc.result.match(/exception|corrupt/) != null)
|
||||
return callback(new MongoRuntimeError(`Invalid collection ${collectionName}`));
|
||||
if (doc.valid != null && !doc.valid)
|
||||
return callback(new MongoRuntimeError(`Invalid collection ${collectionName}`));
|
||||
|
||||
return callback(undefined, doc);
|
||||
});
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { MongoInvalidArgumentError } from './error';
|
||||
|
||||
/** @internal */
|
||||
const kPromise = Symbol('promise');
|
||||
|
||||
interface PromiseStore {
|
||||
[kPromise]?: PromiseConstructor;
|
||||
}
|
||||
|
||||
const store: PromiseStore = {
|
||||
[kPromise]: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* Global promise store allowing user-provided promises
|
||||
* @public
|
||||
*/
|
||||
export class PromiseProvider {
|
||||
/** Validates the passed in promise library */
|
||||
static validate(lib: unknown): lib is PromiseConstructor {
|
||||
if (typeof lib !== 'function')
|
||||
throw new MongoInvalidArgumentError(`Promise must be a function, got ${lib}`);
|
||||
return !!lib;
|
||||
}
|
||||
|
||||
/** Sets the promise library */
|
||||
static set(lib: PromiseConstructor): void {
|
||||
if (!PromiseProvider.validate(lib)) {
|
||||
// validate
|
||||
return;
|
||||
}
|
||||
store[kPromise] = lib;
|
||||
}
|
||||
|
||||
/** Get the stored promise library, or resolves passed in */
|
||||
static get(): PromiseConstructor {
|
||||
return store[kPromise] as PromiseConstructor;
|
||||
}
|
||||
}
|
||||
|
||||
PromiseProvider.set(global.Promise);
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import type { Document } from './bson';
|
||||
|
||||
/** @public */
|
||||
export const ReadConcernLevel = Object.freeze({
|
||||
local: 'local',
|
||||
majority: 'majority',
|
||||
linearizable: 'linearizable',
|
||||
available: 'available',
|
||||
snapshot: 'snapshot'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type ReadConcernLevel = typeof ReadConcernLevel[keyof typeof ReadConcernLevel];
|
||||
|
||||
/** @public */
|
||||
export type ReadConcernLike = ReadConcern | { level: ReadConcernLevel } | ReadConcernLevel;
|
||||
|
||||
/**
|
||||
* The MongoDB ReadConcern, which allows for control of the consistency and isolation properties
|
||||
* of the data read from replica sets and replica set shards.
|
||||
* @public
|
||||
*
|
||||
* @see https://docs.mongodb.com/manual/reference/read-concern/index.html
|
||||
*/
|
||||
export class ReadConcern {
|
||||
level: ReadConcernLevel | string;
|
||||
|
||||
/** Constructs a ReadConcern from the read concern level.*/
|
||||
constructor(level: ReadConcernLevel) {
|
||||
/**
|
||||
* A spec test exists that allows level to be any string.
|
||||
* "invalid readConcern with out stage"
|
||||
* @see ./test/spec/crud/v2/aggregate-out-readConcern.json
|
||||
* @see https://github.com/mongodb/specifications/blob/master/source/read-write-concern/read-write-concern.rst#unknown-levels-and-additional-options-for-string-based-readconcerns
|
||||
*/
|
||||
this.level = ReadConcernLevel[level] ?? level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a ReadConcern given an options object.
|
||||
*
|
||||
* @param options - The options object from which to extract the write concern.
|
||||
*/
|
||||
static fromOptions(options?: {
|
||||
readConcern?: ReadConcernLike;
|
||||
level?: ReadConcernLevel;
|
||||
}): ReadConcern | undefined {
|
||||
if (options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.readConcern) {
|
||||
const { readConcern } = options;
|
||||
if (readConcern instanceof ReadConcern) {
|
||||
return readConcern;
|
||||
} else if (typeof readConcern === 'string') {
|
||||
return new ReadConcern(readConcern);
|
||||
} else if ('level' in readConcern && readConcern.level) {
|
||||
return new ReadConcern(readConcern.level);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.level) {
|
||||
return new ReadConcern(options.level);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
static get MAJORITY(): 'majority' {
|
||||
return ReadConcernLevel.majority;
|
||||
}
|
||||
|
||||
static get AVAILABLE(): 'available' {
|
||||
return ReadConcernLevel.available;
|
||||
}
|
||||
|
||||
static get LINEARIZABLE(): 'linearizable' {
|
||||
return ReadConcernLevel.linearizable;
|
||||
}
|
||||
|
||||
static get SNAPSHOT(): 'snapshot' {
|
||||
return ReadConcernLevel.snapshot;
|
||||
}
|
||||
|
||||
toJSON(): Document {
|
||||
return { level: this.level };
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
import type { Document } from './bson';
|
||||
import { MongoInvalidArgumentError } from './error';
|
||||
import type { TagSet } from './sdam/server_description';
|
||||
import type { ClientSession } from './sessions';
|
||||
|
||||
/** @public */
|
||||
export type ReadPreferenceLike = ReadPreference | ReadPreferenceMode;
|
||||
|
||||
/** @public */
|
||||
export const ReadPreferenceMode = Object.freeze({
|
||||
primary: 'primary',
|
||||
primaryPreferred: 'primaryPreferred',
|
||||
secondary: 'secondary',
|
||||
secondaryPreferred: 'secondaryPreferred',
|
||||
nearest: 'nearest'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type ReadPreferenceMode = typeof ReadPreferenceMode[keyof typeof ReadPreferenceMode];
|
||||
|
||||
/** @public */
|
||||
export interface HedgeOptions {
|
||||
/** Explicitly enable or disable hedged reads. */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ReadPreferenceOptions {
|
||||
/** Max secondary read staleness in seconds, Minimum value is 90 seconds.*/
|
||||
maxStalenessSeconds?: number;
|
||||
/** Server mode in which the same query is dispatched in parallel to multiple replica set members. */
|
||||
hedge?: HedgeOptions;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ReadPreferenceLikeOptions extends ReadPreferenceOptions {
|
||||
readPreference?:
|
||||
| ReadPreferenceLike
|
||||
| {
|
||||
mode?: ReadPreferenceMode;
|
||||
preference?: ReadPreferenceMode;
|
||||
tags?: TagSet[];
|
||||
maxStalenessSeconds?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ReadPreferenceFromOptions extends ReadPreferenceLikeOptions {
|
||||
session?: ClientSession;
|
||||
readPreferenceTags?: TagSet[];
|
||||
hedge?: HedgeOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* The **ReadPreference** class is a class that represents a MongoDB ReadPreference and is
|
||||
* used to construct connections.
|
||||
* @public
|
||||
*
|
||||
* @see https://docs.mongodb.com/manual/core/read-preference/
|
||||
*/
|
||||
export class ReadPreference {
|
||||
mode: ReadPreferenceMode;
|
||||
tags?: TagSet[];
|
||||
hedge?: HedgeOptions;
|
||||
maxStalenessSeconds?: number;
|
||||
minWireVersion?: number;
|
||||
|
||||
public static PRIMARY = ReadPreferenceMode.primary;
|
||||
public static PRIMARY_PREFERRED = ReadPreferenceMode.primaryPreferred;
|
||||
public static SECONDARY = ReadPreferenceMode.secondary;
|
||||
public static SECONDARY_PREFERRED = ReadPreferenceMode.secondaryPreferred;
|
||||
public static NEAREST = ReadPreferenceMode.nearest;
|
||||
|
||||
public static primary = new ReadPreference(ReadPreferenceMode.primary);
|
||||
public static primaryPreferred = new ReadPreference(ReadPreferenceMode.primaryPreferred);
|
||||
public static secondary = new ReadPreference(ReadPreferenceMode.secondary);
|
||||
public static secondaryPreferred = new ReadPreference(ReadPreferenceMode.secondaryPreferred);
|
||||
public static nearest = new ReadPreference(ReadPreferenceMode.nearest);
|
||||
|
||||
/**
|
||||
* @param mode - A string describing the read preference mode (primary|primaryPreferred|secondary|secondaryPreferred|nearest)
|
||||
* @param tags - A tag set used to target reads to members with the specified tag(s). tagSet is not available if using read preference mode primary.
|
||||
* @param options - Additional read preference options
|
||||
*/
|
||||
constructor(mode: ReadPreferenceMode, tags?: TagSet[], options?: ReadPreferenceOptions) {
|
||||
if (!ReadPreference.isValid(mode)) {
|
||||
throw new MongoInvalidArgumentError(`Invalid read preference mode ${JSON.stringify(mode)}`);
|
||||
}
|
||||
if (options == null && typeof tags === 'object' && !Array.isArray(tags)) {
|
||||
options = tags;
|
||||
tags = undefined;
|
||||
} else if (tags && !Array.isArray(tags)) {
|
||||
throw new MongoInvalidArgumentError('ReadPreference tags must be an array');
|
||||
}
|
||||
|
||||
this.mode = mode;
|
||||
this.tags = tags;
|
||||
this.hedge = options?.hedge;
|
||||
this.maxStalenessSeconds = undefined;
|
||||
this.minWireVersion = undefined;
|
||||
|
||||
options = options ?? {};
|
||||
if (options.maxStalenessSeconds != null) {
|
||||
if (options.maxStalenessSeconds <= 0) {
|
||||
throw new MongoInvalidArgumentError('maxStalenessSeconds must be a positive integer');
|
||||
}
|
||||
|
||||
this.maxStalenessSeconds = options.maxStalenessSeconds;
|
||||
|
||||
// NOTE: The minimum required wire version is 5 for this read preference. If the existing
|
||||
// topology has a lower value then a MongoError will be thrown during server selection.
|
||||
this.minWireVersion = 5;
|
||||
}
|
||||
|
||||
if (this.mode === ReadPreference.PRIMARY) {
|
||||
if (this.tags && Array.isArray(this.tags) && this.tags.length > 0) {
|
||||
throw new MongoInvalidArgumentError('Primary read preference cannot be combined with tags');
|
||||
}
|
||||
|
||||
if (this.maxStalenessSeconds) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
'Primary read preference cannot be combined with maxStalenessSeconds'
|
||||
);
|
||||
}
|
||||
|
||||
if (this.hedge) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
'Primary read preference cannot be combined with hedge'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Support the deprecated `preference` property introduced in the porcelain layer
|
||||
get preference(): ReadPreferenceMode {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
static fromString(mode: string): ReadPreference {
|
||||
return new ReadPreference(mode as ReadPreferenceMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a ReadPreference given an options object.
|
||||
*
|
||||
* @param options - The options object from which to extract the read preference.
|
||||
*/
|
||||
static fromOptions(options?: ReadPreferenceFromOptions): ReadPreference | undefined {
|
||||
if (!options) return;
|
||||
const readPreference =
|
||||
options.readPreference ?? options.session?.transaction.options.readPreference;
|
||||
const readPreferenceTags = options.readPreferenceTags;
|
||||
|
||||
if (readPreference == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof readPreference === 'string') {
|
||||
return new ReadPreference(readPreference, readPreferenceTags, {
|
||||
maxStalenessSeconds: options.maxStalenessSeconds,
|
||||
hedge: options.hedge
|
||||
});
|
||||
} else if (!(readPreference instanceof ReadPreference) && typeof readPreference === 'object') {
|
||||
const mode = readPreference.mode || readPreference.preference;
|
||||
if (mode && typeof mode === 'string') {
|
||||
return new ReadPreference(mode, readPreference.tags ?? readPreferenceTags, {
|
||||
maxStalenessSeconds: readPreference.maxStalenessSeconds,
|
||||
hedge: options.hedge
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (readPreferenceTags) {
|
||||
readPreference.tags = readPreferenceTags;
|
||||
}
|
||||
|
||||
return readPreference as ReadPreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces options.readPreference with a ReadPreference instance
|
||||
*/
|
||||
static translate(options: ReadPreferenceLikeOptions): ReadPreferenceLikeOptions {
|
||||
if (options.readPreference == null) return options;
|
||||
const r = options.readPreference;
|
||||
|
||||
if (typeof r === 'string') {
|
||||
options.readPreference = new ReadPreference(r);
|
||||
} else if (r && !(r instanceof ReadPreference) && typeof r === 'object') {
|
||||
const mode = r.mode || r.preference;
|
||||
if (mode && typeof mode === 'string') {
|
||||
options.readPreference = new ReadPreference(mode, r.tags, {
|
||||
maxStalenessSeconds: r.maxStalenessSeconds
|
||||
});
|
||||
}
|
||||
} else if (!(r instanceof ReadPreference)) {
|
||||
throw new MongoInvalidArgumentError(`Invalid read preference: ${r}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a mode is legal
|
||||
*
|
||||
* @param mode - The string representing the read preference mode.
|
||||
*/
|
||||
static isValid(mode: string): boolean {
|
||||
const VALID_MODES = new Set([
|
||||
ReadPreference.PRIMARY,
|
||||
ReadPreference.PRIMARY_PREFERRED,
|
||||
ReadPreference.SECONDARY,
|
||||
ReadPreference.SECONDARY_PREFERRED,
|
||||
ReadPreference.NEAREST,
|
||||
null
|
||||
]);
|
||||
|
||||
return VALID_MODES.has(mode as ReadPreferenceMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a mode is legal
|
||||
*
|
||||
* @param mode - The string representing the read preference mode.
|
||||
*/
|
||||
isValid(mode?: string): boolean {
|
||||
return ReadPreference.isValid(typeof mode === 'string' ? mode : this.mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that this readPreference needs the "secondaryOk" bit when sent over the wire
|
||||
* @deprecated Use secondaryOk instead
|
||||
* @see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#op-query
|
||||
*/
|
||||
slaveOk(): boolean {
|
||||
return this.secondaryOk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that this readPreference needs the "SecondaryOk" bit when sent over the wire
|
||||
* @see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#op-query
|
||||
*/
|
||||
secondaryOk(): boolean {
|
||||
const NEEDS_SECONDARYOK = new Set<string>([
|
||||
ReadPreference.PRIMARY_PREFERRED,
|
||||
ReadPreference.SECONDARY,
|
||||
ReadPreference.SECONDARY_PREFERRED,
|
||||
ReadPreference.NEAREST
|
||||
]);
|
||||
|
||||
return NEEDS_SECONDARYOK.has(this.mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the two ReadPreferences are equivalent
|
||||
*
|
||||
* @param readPreference - The read preference with which to check equality
|
||||
*/
|
||||
equals(readPreference: ReadPreference): boolean {
|
||||
return readPreference.mode === this.mode;
|
||||
}
|
||||
|
||||
/** Return JSON representation */
|
||||
toJSON(): Document {
|
||||
const readPreference = { mode: this.mode } as Document;
|
||||
if (Array.isArray(this.tags)) readPreference.tags = this.tags;
|
||||
if (this.maxStalenessSeconds) readPreference.maxStalenessSeconds = this.maxStalenessSeconds;
|
||||
if (this.hedge) readPreference.hedge = this.hedge;
|
||||
return readPreference;
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { clearTimeout } from 'timers';
|
||||
|
||||
import type { Binary, Long, Timestamp } from '../bson';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import type { Topology } from './topology';
|
||||
|
||||
// shared state names
|
||||
export const STATE_CLOSING = 'closing';
|
||||
export const STATE_CLOSED = 'closed';
|
||||
export const STATE_CONNECTING = 'connecting';
|
||||
export const STATE_CONNECTED = 'connected';
|
||||
|
||||
/**
|
||||
* An enumeration of topology types we know about
|
||||
* @public
|
||||
*/
|
||||
export const TopologyType = Object.freeze({
|
||||
Single: 'Single',
|
||||
ReplicaSetNoPrimary: 'ReplicaSetNoPrimary',
|
||||
ReplicaSetWithPrimary: 'ReplicaSetWithPrimary',
|
||||
Sharded: 'Sharded',
|
||||
Unknown: 'Unknown',
|
||||
LoadBalanced: 'LoadBalanced'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type TopologyType = typeof TopologyType[keyof typeof TopologyType];
|
||||
|
||||
/**
|
||||
* An enumeration of server types we know about
|
||||
* @public
|
||||
*/
|
||||
export const ServerType = Object.freeze({
|
||||
Standalone: 'Standalone',
|
||||
Mongos: 'Mongos',
|
||||
PossiblePrimary: 'PossiblePrimary',
|
||||
RSPrimary: 'RSPrimary',
|
||||
RSSecondary: 'RSSecondary',
|
||||
RSArbiter: 'RSArbiter',
|
||||
RSOther: 'RSOther',
|
||||
RSGhost: 'RSGhost',
|
||||
Unknown: 'Unknown',
|
||||
LoadBalancer: 'LoadBalancer'
|
||||
} as const);
|
||||
|
||||
/** @public */
|
||||
export type ServerType = typeof ServerType[keyof typeof ServerType];
|
||||
|
||||
/** @internal */
|
||||
export type TimerQueue = Set<NodeJS.Timeout>;
|
||||
|
||||
/** @internal */
|
||||
export function drainTimerQueue(queue: TimerQueue): void {
|
||||
queue.forEach(clearTimeout);
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ClusterTime {
|
||||
clusterTime: Timestamp;
|
||||
signature: {
|
||||
hash: Binary;
|
||||
keyId: Long;
|
||||
};
|
||||
}
|
||||
|
||||
/** Shared function to determine clusterTime for a given topology or session */
|
||||
export function _advanceClusterTime(
|
||||
entity: Topology | ClientSession,
|
||||
$clusterTime: ClusterTime
|
||||
): void {
|
||||
if (entity.clusterTime == null) {
|
||||
entity.clusterTime = $clusterTime;
|
||||
} else {
|
||||
if ($clusterTime.clusterTime.greaterThan(entity.clusterTime.clusterTime)) {
|
||||
entity.clusterTime = $clusterTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import type { Document } from '../bson';
|
||||
import type { ServerDescription } from './server_description';
|
||||
import type { TopologyDescription } from './topology_description';
|
||||
|
||||
/**
|
||||
* Emitted when server description changes, but does NOT include changes to the RTT.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ServerDescriptionChangedEvent {
|
||||
/** A unique identifier for the topology */
|
||||
topologyId: number;
|
||||
/** The address (host/port pair) of the server */
|
||||
address: string;
|
||||
/** The previous server description */
|
||||
previousDescription: ServerDescription;
|
||||
/** The new server description */
|
||||
newDescription: ServerDescription;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
topologyId: number,
|
||||
address: string,
|
||||
previousDescription: ServerDescription,
|
||||
newDescription: ServerDescription
|
||||
) {
|
||||
this.topologyId = topologyId;
|
||||
this.address = address;
|
||||
this.previousDescription = previousDescription;
|
||||
this.newDescription = newDescription;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when server is initialized.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ServerOpeningEvent {
|
||||
/** A unique identifier for the topology */
|
||||
topologyId: number;
|
||||
/** The address (host/port pair) of the server */
|
||||
address: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(topologyId: number, address: string) {
|
||||
this.topologyId = topologyId;
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when server is closed.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ServerClosedEvent {
|
||||
/** A unique identifier for the topology */
|
||||
topologyId: number;
|
||||
/** The address (host/port pair) of the server */
|
||||
address: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(topologyId: number, address: string) {
|
||||
this.topologyId = topologyId;
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when topology description changes.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class TopologyDescriptionChangedEvent {
|
||||
/** A unique identifier for the topology */
|
||||
topologyId: number;
|
||||
/** The old topology description */
|
||||
previousDescription: TopologyDescription;
|
||||
/** The new topology description */
|
||||
newDescription: TopologyDescription;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
topologyId: number,
|
||||
previousDescription: TopologyDescription,
|
||||
newDescription: TopologyDescription
|
||||
) {
|
||||
this.topologyId = topologyId;
|
||||
this.previousDescription = previousDescription;
|
||||
this.newDescription = newDescription;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when topology is initialized.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class TopologyOpeningEvent {
|
||||
/** A unique identifier for the topology */
|
||||
topologyId: number;
|
||||
|
||||
/** @internal */
|
||||
constructor(topologyId: number) {
|
||||
this.topologyId = topologyId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when topology is closed.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class TopologyClosedEvent {
|
||||
/** A unique identifier for the topology */
|
||||
topologyId: number;
|
||||
|
||||
/** @internal */
|
||||
constructor(topologyId: number) {
|
||||
this.topologyId = topologyId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the server monitor’s hello command is started - immediately before
|
||||
* the hello command is serialized into raw BSON and written to the socket.
|
||||
*
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ServerHeartbeatStartedEvent {
|
||||
/** The connection id for the command */
|
||||
connectionId: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(connectionId: string) {
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the server monitor’s hello succeeds.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ServerHeartbeatSucceededEvent {
|
||||
/** The connection id for the command */
|
||||
connectionId: string;
|
||||
/** The execution time of the event in ms */
|
||||
duration: number;
|
||||
/** The command reply */
|
||||
reply: Document;
|
||||
|
||||
/** @internal */
|
||||
constructor(connectionId: string, duration: number, reply: Document | null) {
|
||||
this.connectionId = connectionId;
|
||||
this.duration = duration;
|
||||
this.reply = reply ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the server monitor’s hello fails, either with an “ok: 0” or a socket exception.
|
||||
* @public
|
||||
* @category Event
|
||||
*/
|
||||
export class ServerHeartbeatFailedEvent {
|
||||
/** The connection id for the command */
|
||||
connectionId: string;
|
||||
/** The execution time of the event in ms */
|
||||
duration: number;
|
||||
/** The command failure */
|
||||
failure: Error;
|
||||
|
||||
/** @internal */
|
||||
constructor(connectionId: string, duration: number, failure: Error) {
|
||||
this.connectionId = connectionId;
|
||||
this.duration = duration;
|
||||
this.failure = failure;
|
||||
}
|
||||
}
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
|
||||
import { Document, Long } from '../bson';
|
||||
import { connect } from '../cmap/connect';
|
||||
import { Connection, ConnectionOptions } from '../cmap/connection';
|
||||
import { LEGACY_HELLO_COMMAND } from '../constants';
|
||||
import { MongoNetworkError } from '../error';
|
||||
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
|
||||
import type { Callback, InterruptibleAsyncInterval } from '../utils';
|
||||
import {
|
||||
calculateDurationInMs,
|
||||
EventEmitterWithState,
|
||||
makeInterruptibleAsyncInterval,
|
||||
makeStateMachine,
|
||||
now,
|
||||
ns
|
||||
} from '../utils';
|
||||
import { ServerType, STATE_CLOSED, STATE_CLOSING } from './common';
|
||||
import {
|
||||
ServerHeartbeatFailedEvent,
|
||||
ServerHeartbeatStartedEvent,
|
||||
ServerHeartbeatSucceededEvent
|
||||
} from './events';
|
||||
import { Server } from './server';
|
||||
import type { TopologyVersion } from './server_description';
|
||||
|
||||
/** @internal */
|
||||
const kServer = Symbol('server');
|
||||
/** @internal */
|
||||
const kMonitorId = Symbol('monitorId');
|
||||
/** @internal */
|
||||
const kConnection = Symbol('connection');
|
||||
/** @internal */
|
||||
const kCancellationToken = Symbol('cancellationToken');
|
||||
/** @internal */
|
||||
const kRTTPinger = Symbol('rttPinger');
|
||||
/** @internal */
|
||||
const kRoundTripTime = Symbol('roundTripTime');
|
||||
|
||||
const STATE_IDLE = 'idle';
|
||||
const STATE_MONITORING = 'monitoring';
|
||||
const stateTransition = makeStateMachine({
|
||||
[STATE_CLOSING]: [STATE_CLOSING, STATE_IDLE, STATE_CLOSED],
|
||||
[STATE_CLOSED]: [STATE_CLOSED, STATE_MONITORING],
|
||||
[STATE_IDLE]: [STATE_IDLE, STATE_MONITORING, STATE_CLOSING],
|
||||
[STATE_MONITORING]: [STATE_MONITORING, STATE_IDLE, STATE_CLOSING]
|
||||
});
|
||||
|
||||
const INVALID_REQUEST_CHECK_STATES = new Set([STATE_CLOSING, STATE_CLOSED, STATE_MONITORING]);
|
||||
function isInCloseState(monitor: Monitor) {
|
||||
return monitor.s.state === STATE_CLOSED || monitor.s.state === STATE_CLOSING;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface MonitorPrivate {
|
||||
state: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface MonitorOptions
|
||||
extends Omit<ConnectionOptions, 'id' | 'generation' | 'hostAddress'> {
|
||||
connectTimeoutMS: number;
|
||||
heartbeatFrequencyMS: number;
|
||||
minHeartbeatFrequencyMS: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type MonitorEvents = {
|
||||
serverHeartbeatStarted(event: ServerHeartbeatStartedEvent): void;
|
||||
serverHeartbeatSucceeded(event: ServerHeartbeatSucceededEvent): void;
|
||||
serverHeartbeatFailed(event: ServerHeartbeatFailedEvent): void;
|
||||
resetServer(error?: Error): void;
|
||||
resetConnectionPool(): void;
|
||||
close(): void;
|
||||
} & EventEmitterWithState;
|
||||
|
||||
/** @internal */
|
||||
export class Monitor extends TypedEventEmitter<MonitorEvents> {
|
||||
/** @internal */
|
||||
s: MonitorPrivate;
|
||||
address: string;
|
||||
options: Readonly<
|
||||
Pick<MonitorOptions, 'connectTimeoutMS' | 'heartbeatFrequencyMS' | 'minHeartbeatFrequencyMS'>
|
||||
>;
|
||||
connectOptions: ConnectionOptions;
|
||||
[kServer]: Server;
|
||||
[kConnection]?: Connection;
|
||||
[kCancellationToken]: CancellationToken;
|
||||
/** @internal */
|
||||
[kMonitorId]?: InterruptibleAsyncInterval;
|
||||
[kRTTPinger]?: RTTPinger;
|
||||
|
||||
get connection(): Connection | undefined {
|
||||
return this[kConnection];
|
||||
}
|
||||
|
||||
constructor(server: Server, options: MonitorOptions) {
|
||||
super();
|
||||
|
||||
this[kServer] = server;
|
||||
this[kConnection] = undefined;
|
||||
this[kCancellationToken] = new CancellationToken();
|
||||
this[kCancellationToken].setMaxListeners(Infinity);
|
||||
this[kMonitorId] = undefined;
|
||||
this.s = {
|
||||
state: STATE_CLOSED
|
||||
};
|
||||
|
||||
this.address = server.description.address;
|
||||
this.options = Object.freeze({
|
||||
connectTimeoutMS: options.connectTimeoutMS ?? 10000,
|
||||
heartbeatFrequencyMS: options.heartbeatFrequencyMS ?? 10000,
|
||||
minHeartbeatFrequencyMS: options.minHeartbeatFrequencyMS ?? 500
|
||||
});
|
||||
|
||||
const cancellationToken = this[kCancellationToken];
|
||||
// TODO: refactor this to pull it directly from the pool, requires new ConnectionPool integration
|
||||
const connectOptions = Object.assign(
|
||||
{
|
||||
id: '<monitor>' as const,
|
||||
generation: server.s.pool.generation,
|
||||
connectionType: Connection,
|
||||
cancellationToken,
|
||||
hostAddress: server.description.hostAddress
|
||||
},
|
||||
options,
|
||||
// force BSON serialization options
|
||||
{
|
||||
raw: false,
|
||||
promoteLongs: true,
|
||||
promoteValues: true,
|
||||
promoteBuffers: true
|
||||
}
|
||||
);
|
||||
|
||||
// ensure no authentication is used for monitoring
|
||||
delete connectOptions.credentials;
|
||||
if (connectOptions.autoEncrypter) {
|
||||
delete connectOptions.autoEncrypter;
|
||||
}
|
||||
|
||||
this.connectOptions = Object.freeze(connectOptions);
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (this.s.state !== STATE_CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// start
|
||||
const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;
|
||||
const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;
|
||||
this[kMonitorId] = makeInterruptibleAsyncInterval(monitorServer(this), {
|
||||
interval: heartbeatFrequencyMS,
|
||||
minInterval: minHeartbeatFrequencyMS,
|
||||
immediate: true
|
||||
});
|
||||
}
|
||||
|
||||
requestCheck(): void {
|
||||
if (INVALID_REQUEST_CHECK_STATES.has(this.s.state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[kMonitorId]?.wake();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
const topologyVersion = this[kServer].description.topologyVersion;
|
||||
if (isInCloseState(this) || topologyVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateTransition(this, STATE_CLOSING);
|
||||
resetMonitorState(this);
|
||||
|
||||
// restart monitor
|
||||
stateTransition(this, STATE_IDLE);
|
||||
|
||||
// restart monitoring
|
||||
const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;
|
||||
const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;
|
||||
this[kMonitorId] = makeInterruptibleAsyncInterval(monitorServer(this), {
|
||||
interval: heartbeatFrequencyMS,
|
||||
minInterval: minHeartbeatFrequencyMS
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (isInCloseState(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateTransition(this, STATE_CLOSING);
|
||||
resetMonitorState(this);
|
||||
|
||||
// close monitor
|
||||
this.emit('close');
|
||||
stateTransition(this, STATE_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
function resetMonitorState(monitor: Monitor) {
|
||||
monitor[kMonitorId]?.stop();
|
||||
monitor[kMonitorId] = undefined;
|
||||
|
||||
monitor[kRTTPinger]?.close();
|
||||
monitor[kRTTPinger] = undefined;
|
||||
|
||||
monitor[kCancellationToken].emit('cancel');
|
||||
|
||||
monitor[kConnection]?.destroy({ force: true });
|
||||
monitor[kConnection] = undefined;
|
||||
}
|
||||
|
||||
function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
|
||||
let start = now();
|
||||
monitor.emit(Server.SERVER_HEARTBEAT_STARTED, new ServerHeartbeatStartedEvent(monitor.address));
|
||||
|
||||
function failureHandler(err: Error) {
|
||||
monitor[kConnection]?.destroy({ force: true });
|
||||
monitor[kConnection] = undefined;
|
||||
|
||||
monitor.emit(
|
||||
Server.SERVER_HEARTBEAT_FAILED,
|
||||
new ServerHeartbeatFailedEvent(monitor.address, calculateDurationInMs(start), err)
|
||||
);
|
||||
|
||||
monitor.emit('resetServer', err);
|
||||
monitor.emit('resetConnectionPool');
|
||||
callback(err);
|
||||
}
|
||||
|
||||
const connection = monitor[kConnection];
|
||||
if (connection && !connection.closed) {
|
||||
const { serverApi, helloOk } = connection;
|
||||
const connectTimeoutMS = monitor.options.connectTimeoutMS;
|
||||
const maxAwaitTimeMS = monitor.options.heartbeatFrequencyMS;
|
||||
const topologyVersion = monitor[kServer].description.topologyVersion;
|
||||
const isAwaitable = topologyVersion != null;
|
||||
|
||||
const cmd = {
|
||||
[serverApi?.version || helloOk ? 'hello' : LEGACY_HELLO_COMMAND]: true,
|
||||
...(isAwaitable && topologyVersion
|
||||
? { maxAwaitTimeMS, topologyVersion: makeTopologyVersion(topologyVersion) }
|
||||
: {})
|
||||
};
|
||||
|
||||
const options = isAwaitable
|
||||
? {
|
||||
socketTimeoutMS: connectTimeoutMS ? connectTimeoutMS + maxAwaitTimeMS : 0,
|
||||
exhaustAllowed: true
|
||||
}
|
||||
: { socketTimeoutMS: connectTimeoutMS };
|
||||
|
||||
if (isAwaitable && monitor[kRTTPinger] == null) {
|
||||
monitor[kRTTPinger] = new RTTPinger(
|
||||
monitor[kCancellationToken],
|
||||
Object.assign(
|
||||
{ heartbeatFrequencyMS: monitor.options.heartbeatFrequencyMS },
|
||||
monitor.connectOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
connection.command(ns('admin.$cmd'), cmd, options, (err, hello) => {
|
||||
if (err) {
|
||||
return failureHandler(err);
|
||||
}
|
||||
|
||||
if (!('isWritablePrimary' in hello)) {
|
||||
// Provide hello-style response document.
|
||||
hello.isWritablePrimary = hello[LEGACY_HELLO_COMMAND];
|
||||
}
|
||||
|
||||
const rttPinger = monitor[kRTTPinger];
|
||||
const duration =
|
||||
isAwaitable && rttPinger ? rttPinger.roundTripTime : calculateDurationInMs(start);
|
||||
|
||||
monitor.emit(
|
||||
Server.SERVER_HEARTBEAT_SUCCEEDED,
|
||||
new ServerHeartbeatSucceededEvent(monitor.address, duration, hello)
|
||||
);
|
||||
|
||||
// if we are using the streaming protocol then we immediately issue another `started`
|
||||
// event, otherwise the "check" is complete and return to the main monitor loop
|
||||
if (isAwaitable && hello.topologyVersion) {
|
||||
monitor.emit(
|
||||
Server.SERVER_HEARTBEAT_STARTED,
|
||||
new ServerHeartbeatStartedEvent(monitor.address)
|
||||
);
|
||||
start = now();
|
||||
} else {
|
||||
monitor[kRTTPinger]?.close();
|
||||
monitor[kRTTPinger] = undefined;
|
||||
|
||||
callback(undefined, hello);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// connecting does an implicit `hello`
|
||||
connect(monitor.connectOptions, (err, conn) => {
|
||||
if (err) {
|
||||
monitor[kConnection] = undefined;
|
||||
|
||||
// we already reset the connection pool on network errors in all cases
|
||||
if (!(err instanceof MongoNetworkError)) {
|
||||
monitor.emit('resetConnectionPool');
|
||||
}
|
||||
|
||||
failureHandler(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (conn) {
|
||||
// Tell the connection that we are using the streaming protocol so that the
|
||||
// connection's message stream will only read the last hello on the buffer.
|
||||
conn.isMonitoringConnection = true;
|
||||
|
||||
if (isInCloseState(monitor)) {
|
||||
conn.destroy({ force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
monitor[kConnection] = conn;
|
||||
monitor.emit(
|
||||
Server.SERVER_HEARTBEAT_SUCCEEDED,
|
||||
new ServerHeartbeatSucceededEvent(monitor.address, calculateDurationInMs(start), conn.hello)
|
||||
);
|
||||
|
||||
callback(undefined, conn.hello);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function monitorServer(monitor: Monitor) {
|
||||
return (callback: Callback) => {
|
||||
stateTransition(monitor, STATE_MONITORING);
|
||||
function done() {
|
||||
if (!isInCloseState(monitor)) {
|
||||
stateTransition(monitor, STATE_IDLE);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
checkServer(monitor, (err, hello) => {
|
||||
if (err) {
|
||||
// otherwise an error occurred on initial discovery, also bail
|
||||
if (monitor[kServer].description.type === ServerType.Unknown) {
|
||||
monitor.emit('resetServer', err);
|
||||
return done();
|
||||
}
|
||||
}
|
||||
|
||||
// if the check indicates streaming is supported, immediately reschedule monitoring
|
||||
if (hello && hello.topologyVersion) {
|
||||
setTimeout(() => {
|
||||
if (!isInCloseState(monitor)) {
|
||||
monitor[kMonitorId]?.wake();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function makeTopologyVersion(tv: TopologyVersion) {
|
||||
return {
|
||||
processId: tv.processId,
|
||||
// tests mock counter as just number, but in a real situation counter should always be a Long
|
||||
// TODO(NODE-2674): Preserve int64 sent from MongoDB
|
||||
counter: Long.isLong(tv.counter) ? tv.counter : Long.fromNumber(tv.counter)
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface RTTPingerOptions extends ConnectionOptions {
|
||||
heartbeatFrequencyMS: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class RTTPinger {
|
||||
/** @internal */
|
||||
[kConnection]?: Connection;
|
||||
/** @internal */
|
||||
[kCancellationToken]: CancellationToken;
|
||||
/** @internal */
|
||||
[kRoundTripTime]: number;
|
||||
/** @internal */
|
||||
[kMonitorId]: NodeJS.Timeout;
|
||||
closed: boolean;
|
||||
|
||||
constructor(cancellationToken: CancellationToken, options: RTTPingerOptions) {
|
||||
this[kConnection] = undefined;
|
||||
this[kCancellationToken] = cancellationToken;
|
||||
this[kRoundTripTime] = 0;
|
||||
this.closed = false;
|
||||
|
||||
const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
|
||||
this[kMonitorId] = setTimeout(() => measureRoundTripTime(this, options), heartbeatFrequencyMS);
|
||||
}
|
||||
|
||||
get roundTripTime(): number {
|
||||
return this[kRoundTripTime];
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.closed = true;
|
||||
clearTimeout(this[kMonitorId]);
|
||||
|
||||
this[kConnection]?.destroy({ force: true });
|
||||
this[kConnection] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function measureRoundTripTime(rttPinger: RTTPinger, options: RTTPingerOptions) {
|
||||
const start = now();
|
||||
options.cancellationToken = rttPinger[kCancellationToken];
|
||||
const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
|
||||
|
||||
if (rttPinger.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
function measureAndReschedule(conn?: Connection) {
|
||||
if (rttPinger.closed) {
|
||||
conn?.destroy({ force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (rttPinger[kConnection] == null) {
|
||||
rttPinger[kConnection] = conn;
|
||||
}
|
||||
|
||||
rttPinger[kRoundTripTime] = calculateDurationInMs(start);
|
||||
rttPinger[kMonitorId] = setTimeout(
|
||||
() => measureRoundTripTime(rttPinger, options),
|
||||
heartbeatFrequencyMS
|
||||
);
|
||||
}
|
||||
|
||||
const connection = rttPinger[kConnection];
|
||||
if (connection == null) {
|
||||
connect(options, (err, conn) => {
|
||||
if (err) {
|
||||
rttPinger[kConnection] = undefined;
|
||||
rttPinger[kRoundTripTime] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
measureAndReschedule(conn);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
connection.command(ns('admin.$cmd'), { [LEGACY_HELLO_COMMAND]: 1 }, undefined, err => {
|
||||
if (err) {
|
||||
rttPinger[kConnection] = undefined;
|
||||
rttPinger[kRoundTripTime] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
measureAndReschedule();
|
||||
});
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
import type { Document } from '../bson';
|
||||
import { CommandOptions, Connection, DestroyOptions, GetMoreOptions } from '../cmap/connection';
|
||||
import {
|
||||
ConnectionPool,
|
||||
ConnectionPoolEvents,
|
||||
ConnectionPoolOptions
|
||||
} from '../cmap/connection_pool';
|
||||
import {
|
||||
APM_EVENTS,
|
||||
CLOSED,
|
||||
CMAP_EVENTS,
|
||||
CONNECT,
|
||||
DESCRIPTION_RECEIVED,
|
||||
ENDED,
|
||||
HEARTBEAT_EVENTS,
|
||||
SERVER_HEARTBEAT_FAILED,
|
||||
SERVER_HEARTBEAT_STARTED,
|
||||
SERVER_HEARTBEAT_SUCCEEDED
|
||||
} from '../constants';
|
||||
import type { AutoEncrypter } from '../deps';
|
||||
import {
|
||||
isNetworkErrorBeforeHandshake,
|
||||
isNodeShuttingDownError,
|
||||
isSDAMUnrecoverableError,
|
||||
MongoCompatibilityError,
|
||||
MongoError,
|
||||
MongoErrorLabel,
|
||||
MongoInvalidArgumentError,
|
||||
MongoNetworkError,
|
||||
MongoNetworkTimeoutError,
|
||||
MongoServerClosedError,
|
||||
MongoServerError,
|
||||
MongoUnexpectedServerResponseError,
|
||||
needsRetryableWriteLabel
|
||||
} from '../error';
|
||||
import { Logger } from '../logger';
|
||||
import type { ServerApi } from '../mongo_client';
|
||||
import { TypedEventEmitter } from '../mongo_types';
|
||||
import type { ClientSession } from '../sessions';
|
||||
import { isTransactionCommand } from '../transactions';
|
||||
import {
|
||||
Callback,
|
||||
collationNotSupported,
|
||||
EventEmitterWithState,
|
||||
makeStateMachine,
|
||||
maxWireVersion,
|
||||
MongoDBNamespace,
|
||||
supportsRetryableWrites
|
||||
} from '../utils';
|
||||
import {
|
||||
ClusterTime,
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
STATE_CONNECTED,
|
||||
STATE_CONNECTING,
|
||||
TopologyType
|
||||
} from './common';
|
||||
import type {
|
||||
ServerHeartbeatFailedEvent,
|
||||
ServerHeartbeatStartedEvent,
|
||||
ServerHeartbeatSucceededEvent
|
||||
} from './events';
|
||||
import { Monitor, MonitorOptions } from './monitor';
|
||||
import { compareTopologyVersion, ServerDescription } from './server_description';
|
||||
import type { Topology } from './topology';
|
||||
|
||||
const stateTransition = makeStateMachine({
|
||||
[STATE_CLOSED]: [STATE_CLOSED, STATE_CONNECTING],
|
||||
[STATE_CONNECTING]: [STATE_CONNECTING, STATE_CLOSING, STATE_CONNECTED, STATE_CLOSED],
|
||||
[STATE_CONNECTED]: [STATE_CONNECTED, STATE_CLOSING, STATE_CLOSED],
|
||||
[STATE_CLOSING]: [STATE_CLOSING, STATE_CLOSED]
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
const kMonitor = Symbol('monitor');
|
||||
|
||||
/** @public */
|
||||
export type ServerOptions = Omit<ConnectionPoolOptions, 'id' | 'generation' | 'hostAddress'> &
|
||||
MonitorOptions;
|
||||
|
||||
/** @internal */
|
||||
export interface ServerPrivate {
|
||||
/** The server description for this server */
|
||||
description: ServerDescription;
|
||||
/** A copy of the options used to construct this instance */
|
||||
options: ServerOptions;
|
||||
/** A logger instance */
|
||||
logger: Logger;
|
||||
/** The current state of the Server */
|
||||
state: string;
|
||||
/** The topology this server is a part of */
|
||||
topology: Topology;
|
||||
/** A connection pool for this server */
|
||||
pool: ConnectionPool;
|
||||
/** MongoDB server API version */
|
||||
serverApi?: ServerApi;
|
||||
/** A count of the operations currently running against the server. */
|
||||
operationCount: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ServerEvents = {
|
||||
serverHeartbeatStarted(event: ServerHeartbeatStartedEvent): void;
|
||||
serverHeartbeatSucceeded(event: ServerHeartbeatSucceededEvent): void;
|
||||
serverHeartbeatFailed(event: ServerHeartbeatFailedEvent): void;
|
||||
/** Top level MongoClient doesn't emit this so it is marked: @internal */
|
||||
connect(server: Server): void;
|
||||
descriptionReceived(description: ServerDescription): void;
|
||||
closed(): void;
|
||||
ended(): void;
|
||||
} & ConnectionPoolEvents &
|
||||
EventEmitterWithState;
|
||||
|
||||
/** @internal */
|
||||
export class Server extends TypedEventEmitter<ServerEvents> {
|
||||
/** @internal */
|
||||
s: ServerPrivate;
|
||||
serverApi?: ServerApi;
|
||||
hello?: Document;
|
||||
[kMonitor]: Monitor | null;
|
||||
|
||||
/** @event */
|
||||
static readonly SERVER_HEARTBEAT_STARTED = SERVER_HEARTBEAT_STARTED;
|
||||
/** @event */
|
||||
static readonly SERVER_HEARTBEAT_SUCCEEDED = SERVER_HEARTBEAT_SUCCEEDED;
|
||||
/** @event */
|
||||
static readonly SERVER_HEARTBEAT_FAILED = SERVER_HEARTBEAT_FAILED;
|
||||
/** @event */
|
||||
static readonly CONNECT = CONNECT;
|
||||
/** @event */
|
||||
static readonly DESCRIPTION_RECEIVED = DESCRIPTION_RECEIVED;
|
||||
/** @event */
|
||||
static readonly CLOSED = CLOSED;
|
||||
/** @event */
|
||||
static readonly ENDED = ENDED;
|
||||
|
||||
/**
|
||||
* Create a server
|
||||
*/
|
||||
constructor(topology: Topology, description: ServerDescription, options: ServerOptions) {
|
||||
super();
|
||||
|
||||
this.serverApi = options.serverApi;
|
||||
|
||||
const poolOptions = { hostAddress: description.hostAddress, ...options };
|
||||
|
||||
this.s = {
|
||||
description,
|
||||
options,
|
||||
logger: new Logger('Server'),
|
||||
state: STATE_CLOSED,
|
||||
topology,
|
||||
pool: new ConnectionPool(poolOptions),
|
||||
operationCount: 0
|
||||
};
|
||||
|
||||
for (const event of [...CMAP_EVENTS, ...APM_EVENTS]) {
|
||||
this.s.pool.on(event, (e: any) => this.emit(event, e));
|
||||
}
|
||||
|
||||
this.s.pool.on(Connection.CLUSTER_TIME_RECEIVED, (clusterTime: ClusterTime) => {
|
||||
this.clusterTime = clusterTime;
|
||||
});
|
||||
|
||||
if (this.loadBalanced) {
|
||||
this[kMonitor] = null;
|
||||
// monitoring is disabled in load balancing mode
|
||||
return;
|
||||
}
|
||||
|
||||
// create the monitor
|
||||
// TODO(NODE-4144): Remove new variable for type narrowing
|
||||
const monitor = new Monitor(this, this.s.options);
|
||||
this[kMonitor] = monitor;
|
||||
|
||||
for (const event of HEARTBEAT_EVENTS) {
|
||||
monitor.on(event, (e: any) => this.emit(event, e));
|
||||
}
|
||||
|
||||
monitor.on('resetConnectionPool', () => {
|
||||
this.s.pool.clear();
|
||||
});
|
||||
|
||||
monitor.on('resetServer', (error: MongoError) => markServerUnknown(this, error));
|
||||
monitor.on(Server.SERVER_HEARTBEAT_SUCCEEDED, (event: ServerHeartbeatSucceededEvent) => {
|
||||
this.emit(
|
||||
Server.DESCRIPTION_RECEIVED,
|
||||
new ServerDescription(this.description.hostAddress, event.reply, {
|
||||
roundTripTime: calculateRoundTripTime(this.description.roundTripTime, event.duration)
|
||||
})
|
||||
);
|
||||
|
||||
if (this.s.state === STATE_CONNECTING) {
|
||||
stateTransition(this, STATE_CONNECTED);
|
||||
this.emit(Server.CONNECT, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get clusterTime(): ClusterTime | undefined {
|
||||
return this.s.topology.clusterTime;
|
||||
}
|
||||
|
||||
set clusterTime(clusterTime: ClusterTime | undefined) {
|
||||
this.s.topology.clusterTime = clusterTime;
|
||||
}
|
||||
|
||||
get description(): ServerDescription {
|
||||
return this.s.description;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.s.description.address;
|
||||
}
|
||||
|
||||
get autoEncrypter(): AutoEncrypter | undefined {
|
||||
if (this.s.options && this.s.options.autoEncrypter) {
|
||||
return this.s.options.autoEncrypter;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
get loadBalanced(): boolean {
|
||||
return this.s.topology.description.type === TopologyType.LoadBalanced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate server connect
|
||||
*/
|
||||
connect(): void {
|
||||
if (this.s.state !== STATE_CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateTransition(this, STATE_CONNECTING);
|
||||
|
||||
// If in load balancer mode we automatically set the server to
|
||||
// a load balancer. It never transitions out of this state and
|
||||
// has no monitor.
|
||||
if (!this.loadBalanced) {
|
||||
this[kMonitor]?.connect();
|
||||
} else {
|
||||
stateTransition(this, STATE_CONNECTED);
|
||||
this.emit(Server.CONNECT, this);
|
||||
}
|
||||
}
|
||||
|
||||
/** Destroy the server connection */
|
||||
destroy(options?: DestroyOptions, callback?: Callback): void {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = Object.assign({}, { force: false }, options);
|
||||
|
||||
if (this.s.state === STATE_CLOSED) {
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
stateTransition(this, STATE_CLOSING);
|
||||
|
||||
if (!this.loadBalanced) {
|
||||
this[kMonitor]?.close();
|
||||
}
|
||||
|
||||
this.s.pool.close(options, err => {
|
||||
stateTransition(this, STATE_CLOSED);
|
||||
this.emit('closed');
|
||||
if (typeof callback === 'function') {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately schedule monitoring of this server. If there already an attempt being made
|
||||
* this will be a no-op.
|
||||
*/
|
||||
requestCheck(): void {
|
||||
if (!this.loadBalanced) {
|
||||
this[kMonitor]?.requestCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command
|
||||
* @internal
|
||||
*/
|
||||
command(
|
||||
ns: MongoDBNamespace,
|
||||
cmd: Document,
|
||||
options: CommandOptions,
|
||||
callback: Callback<Document>
|
||||
): void {
|
||||
if (callback == null) {
|
||||
throw new MongoInvalidArgumentError('Callback must be provided');
|
||||
}
|
||||
|
||||
if (ns.db == null || typeof ns === 'string') {
|
||||
throw new MongoInvalidArgumentError('Namespace must not be a string');
|
||||
}
|
||||
|
||||
if (this.s.state === STATE_CLOSING || this.s.state === STATE_CLOSED) {
|
||||
callback(new MongoServerClosedError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone the options
|
||||
const finalOptions = Object.assign({}, options, { wireProtocolCommand: false });
|
||||
|
||||
// There are cases where we need to flag the read preference not to get sent in
|
||||
// the command, such as pre-5.0 servers attempting to perform an aggregate write
|
||||
// with a non-primary read preference. In this case the effective read preference
|
||||
// (primary) is not the same as the provided and must be removed completely.
|
||||
if (finalOptions.omitReadPreference) {
|
||||
delete finalOptions.readPreference;
|
||||
}
|
||||
|
||||
// error if collation not supported
|
||||
if (collationNotSupported(this, cmd)) {
|
||||
callback(new MongoCompatibilityError(`Server ${this.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
const session = finalOptions.session;
|
||||
const conn = session?.pinnedConnection;
|
||||
|
||||
// NOTE: This is a hack! We can't retrieve the connections used for executing an operation
|
||||
// (and prevent them from being checked back in) at the point of operation execution.
|
||||
// This should be considered as part of the work for NODE-2882
|
||||
// NOTE:
|
||||
// When incrementing operation count, it's important that we increment it before we
|
||||
// attempt to check out a connection from the pool. This ensures that operations that
|
||||
// are waiting for a connection are included in the operation count. Load balanced
|
||||
// mode will only ever have a single server, so the operation count doesn't matter.
|
||||
// Incrementing the operation count above the logic to handle load balanced mode would
|
||||
// require special logic to decrement it again, or would double increment (the load
|
||||
// balanced code makes a recursive call). Instead, we increment the count after this
|
||||
// check.
|
||||
if (this.loadBalanced && session && conn == null && isPinnableCommand(cmd, session)) {
|
||||
this.s.pool.checkOut((err, checkedOut) => {
|
||||
if (err || checkedOut == null) {
|
||||
if (callback) return callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
session.pin(checkedOut);
|
||||
this.command(ns, cmd, finalOptions, callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.s.operationCount += 1;
|
||||
|
||||
this.s.pool.withConnection(
|
||||
conn,
|
||||
(err, conn, cb) => {
|
||||
if (err || !conn) {
|
||||
this.s.operationCount -= 1;
|
||||
markServerUnknown(this, err);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
conn.command(
|
||||
ns,
|
||||
cmd,
|
||||
finalOptions,
|
||||
makeOperationHandler(this, conn, cmd, finalOptions, (error, response) => {
|
||||
this.s.operationCount -= 1;
|
||||
cb(error, response);
|
||||
})
|
||||
);
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateRoundTripTime(oldRtt: number, duration: number): number {
|
||||
if (oldRtt === -1) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
const alpha = 0.2;
|
||||
return alpha * duration + (1 - alpha) * oldRtt;
|
||||
}
|
||||
|
||||
function markServerUnknown(server: Server, error?: MongoServerError) {
|
||||
// Load balancer servers can never be marked unknown.
|
||||
if (server.loadBalanced) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof MongoNetworkError && !(error instanceof MongoNetworkTimeoutError)) {
|
||||
server[kMonitor]?.reset();
|
||||
}
|
||||
|
||||
server.emit(
|
||||
Server.DESCRIPTION_RECEIVED,
|
||||
new ServerDescription(server.description.hostAddress, undefined, { error })
|
||||
);
|
||||
}
|
||||
|
||||
function isPinnableCommand(cmd: Document, session?: ClientSession): boolean {
|
||||
if (session) {
|
||||
return (
|
||||
session.inTransaction() ||
|
||||
'aggregate' in cmd ||
|
||||
'find' in cmd ||
|
||||
'getMore' in cmd ||
|
||||
'listCollections' in cmd ||
|
||||
'listIndexes' in cmd
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function connectionIsStale(pool: ConnectionPool, connection: Connection) {
|
||||
if (connection.serviceId) {
|
||||
return (
|
||||
connection.generation !== pool.serviceGenerations.get(connection.serviceId.toHexString())
|
||||
);
|
||||
}
|
||||
|
||||
return connection.generation !== pool.generation;
|
||||
}
|
||||
|
||||
function shouldHandleStateChangeError(server: Server, err: MongoError) {
|
||||
const etv = err.topologyVersion;
|
||||
const stv = server.description.topologyVersion;
|
||||
return compareTopologyVersion(stv, etv) < 0;
|
||||
}
|
||||
|
||||
function inActiveTransaction(session: ClientSession | undefined, cmd: Document) {
|
||||
return session && session.inTransaction() && !isTransactionCommand(cmd);
|
||||
}
|
||||
|
||||
/** this checks the retryWrites option passed down from the client options, it
|
||||
* does not check if the server supports retryable writes */
|
||||
function isRetryableWritesEnabled(topology: Topology) {
|
||||
return topology.s.options.retryWrites !== false;
|
||||
}
|
||||
|
||||
function makeOperationHandler(
|
||||
server: Server,
|
||||
connection: Connection,
|
||||
cmd: Document,
|
||||
options: CommandOptions | GetMoreOptions | undefined,
|
||||
callback: Callback
|
||||
): Callback {
|
||||
const session = options?.session;
|
||||
return function handleOperationResult(error, result) {
|
||||
if (result != null) {
|
||||
return callback(undefined, result);
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
return callback(new MongoUnexpectedServerResponseError('Empty response with no error'));
|
||||
}
|
||||
|
||||
if (!(error instanceof MongoError)) {
|
||||
// Node.js or some other error we have not special handling for
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (connectionIsStale(server.s.pool, connection)) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (error instanceof MongoNetworkError) {
|
||||
if (session && !session.hasEnded && session.serverSession) {
|
||||
session.serverSession.isDirty = true;
|
||||
}
|
||||
|
||||
// inActiveTransaction check handles commit and abort.
|
||||
if (
|
||||
inActiveTransaction(session, cmd) &&
|
||||
!error.hasErrorLabel(MongoErrorLabel.TransientTransactionError)
|
||||
) {
|
||||
error.addErrorLabel(MongoErrorLabel.TransientTransactionError);
|
||||
}
|
||||
|
||||
if (
|
||||
(isRetryableWritesEnabled(server.s.topology) || isTransactionCommand(cmd)) &&
|
||||
supportsRetryableWrites(server) &&
|
||||
!inActiveTransaction(session, cmd)
|
||||
) {
|
||||
error.addErrorLabel(MongoErrorLabel.RetryableWriteError);
|
||||
}
|
||||
|
||||
if (!(error instanceof MongoNetworkTimeoutError) || isNetworkErrorBeforeHandshake(error)) {
|
||||
// In load balanced mode we never mark the server as unknown and always
|
||||
// clear for the specific service id.
|
||||
|
||||
server.s.pool.clear(connection.serviceId);
|
||||
if (!server.loadBalanced) {
|
||||
markServerUnknown(server, error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
(isRetryableWritesEnabled(server.s.topology) || isTransactionCommand(cmd)) &&
|
||||
needsRetryableWriteLabel(error, maxWireVersion(server)) &&
|
||||
!inActiveTransaction(session, cmd)
|
||||
) {
|
||||
error.addErrorLabel(MongoErrorLabel.RetryableWriteError);
|
||||
}
|
||||
|
||||
if (isSDAMUnrecoverableError(error)) {
|
||||
if (shouldHandleStateChangeError(server, error)) {
|
||||
if (maxWireVersion(server) <= 7 || isNodeShuttingDownError(error)) {
|
||||
server.s.pool.clear(connection.serviceId);
|
||||
}
|
||||
|
||||
if (!server.loadBalanced) {
|
||||
markServerUnknown(server, error);
|
||||
process.nextTick(() => server.requestCheck());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
session &&
|
||||
session.isPinned &&
|
||||
error.hasErrorLabel(MongoErrorLabel.TransientTransactionError)
|
||||
) {
|
||||
session.unpin({ force: true });
|
||||
}
|
||||
|
||||
return callback(error);
|
||||
};
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
import { Document, Long, ObjectId } from '../bson';
|
||||
import { MongoRuntimeError, MongoServerError } from '../error';
|
||||
import { arrayStrictEqual, compareObjectId, errorStrictEqual, HostAddress, now } from '../utils';
|
||||
import type { ClusterTime } from './common';
|
||||
import { ServerType } from './common';
|
||||
|
||||
const WRITABLE_SERVER_TYPES = new Set<ServerType>([
|
||||
ServerType.RSPrimary,
|
||||
ServerType.Standalone,
|
||||
ServerType.Mongos,
|
||||
ServerType.LoadBalancer
|
||||
]);
|
||||
|
||||
const DATA_BEARING_SERVER_TYPES = new Set<ServerType>([
|
||||
ServerType.RSPrimary,
|
||||
ServerType.RSSecondary,
|
||||
ServerType.Mongos,
|
||||
ServerType.Standalone,
|
||||
ServerType.LoadBalancer
|
||||
]);
|
||||
|
||||
/** @public */
|
||||
export interface TopologyVersion {
|
||||
processId: ObjectId;
|
||||
counter: Long;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TagSet = { [key: string]: string };
|
||||
|
||||
/** @internal */
|
||||
export interface ServerDescriptionOptions {
|
||||
/** An Error used for better reporting debugging */
|
||||
error?: MongoServerError;
|
||||
|
||||
/** The round trip time to ping this server (in ms) */
|
||||
roundTripTime?: number;
|
||||
|
||||
/** If the client is in load balancing mode. */
|
||||
loadBalanced?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The client's view of a single server, based on the most recent hello outcome.
|
||||
*
|
||||
* Internal type, not meant to be directly instantiated
|
||||
* @public
|
||||
*/
|
||||
export class ServerDescription {
|
||||
address: string;
|
||||
type: ServerType;
|
||||
hosts: string[];
|
||||
passives: string[];
|
||||
arbiters: string[];
|
||||
tags: TagSet;
|
||||
error: MongoServerError | null;
|
||||
topologyVersion: TopologyVersion | null;
|
||||
minWireVersion: number;
|
||||
maxWireVersion: number;
|
||||
roundTripTime: number;
|
||||
lastUpdateTime: number;
|
||||
lastWriteDate: number;
|
||||
me: string | null;
|
||||
primary: string | null;
|
||||
setName: string | null;
|
||||
setVersion: number | null;
|
||||
electionId: ObjectId | null;
|
||||
logicalSessionTimeoutMinutes: number | null;
|
||||
|
||||
// NOTE: does this belong here? It seems we should gossip the cluster time at the CMAP level
|
||||
$clusterTime?: ClusterTime;
|
||||
|
||||
/**
|
||||
* Create a ServerDescription
|
||||
* @internal
|
||||
*
|
||||
* @param address - The address of the server
|
||||
* @param hello - An optional hello response for this server
|
||||
*/
|
||||
constructor(
|
||||
address: HostAddress | string,
|
||||
hello?: Document,
|
||||
options: ServerDescriptionOptions = {}
|
||||
) {
|
||||
if (address == null || address === '') {
|
||||
throw new MongoRuntimeError('ServerDescription must be provided with a non-empty address');
|
||||
}
|
||||
|
||||
this.address =
|
||||
typeof address === 'string'
|
||||
? HostAddress.fromString(address).toString(false) // Use HostAddress to normalize
|
||||
: address.toString(false);
|
||||
this.type = parseServerType(hello, options);
|
||||
this.hosts = hello?.hosts?.map((host: string) => host.toLowerCase()) ?? [];
|
||||
this.passives = hello?.passives?.map((host: string) => host.toLowerCase()) ?? [];
|
||||
this.arbiters = hello?.arbiters?.map((host: string) => host.toLowerCase()) ?? [];
|
||||
this.tags = hello?.tags ?? {};
|
||||
this.minWireVersion = hello?.minWireVersion ?? 0;
|
||||
this.maxWireVersion = hello?.maxWireVersion ?? 0;
|
||||
this.roundTripTime = options?.roundTripTime ?? -1;
|
||||
this.lastUpdateTime = now();
|
||||
this.lastWriteDate = hello?.lastWrite?.lastWriteDate ?? 0;
|
||||
this.error = options.error ?? null;
|
||||
// TODO(NODE-2674): Preserve int64 sent from MongoDB
|
||||
this.topologyVersion = this.error?.topologyVersion ?? hello?.topologyVersion ?? null;
|
||||
this.setName = hello?.setName ?? null;
|
||||
this.setVersion = hello?.setVersion ?? null;
|
||||
this.electionId = hello?.electionId ?? null;
|
||||
this.logicalSessionTimeoutMinutes = hello?.logicalSessionTimeoutMinutes ?? null;
|
||||
this.primary = hello?.primary ?? null;
|
||||
this.me = hello?.me?.toLowerCase() ?? null;
|
||||
this.$clusterTime = hello?.$clusterTime ?? null;
|
||||
}
|
||||
|
||||
get hostAddress(): HostAddress {
|
||||
return HostAddress.fromString(this.address);
|
||||
}
|
||||
|
||||
get allHosts(): string[] {
|
||||
return this.hosts.concat(this.arbiters).concat(this.passives);
|
||||
}
|
||||
|
||||
/** Is this server available for reads*/
|
||||
get isReadable(): boolean {
|
||||
return this.type === ServerType.RSSecondary || this.isWritable;
|
||||
}
|
||||
|
||||
/** Is this server data bearing */
|
||||
get isDataBearing(): boolean {
|
||||
return DATA_BEARING_SERVER_TYPES.has(this.type);
|
||||
}
|
||||
|
||||
/** Is this server available for writes */
|
||||
get isWritable(): boolean {
|
||||
return WRITABLE_SERVER_TYPES.has(this.type);
|
||||
}
|
||||
|
||||
get host(): string {
|
||||
const chopLength = `:${this.port}`.length;
|
||||
return this.address.slice(0, -chopLength);
|
||||
}
|
||||
|
||||
get port(): number {
|
||||
const port = this.address.split(':').pop();
|
||||
return port ? Number.parseInt(port, 10) : 27017;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if another `ServerDescription` is equal to this one per the rules defined
|
||||
* in the {@link https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#serverdescription|SDAM spec}
|
||||
*/
|
||||
equals(other?: ServerDescription | null): boolean {
|
||||
// Despite using the comparator that would determine a nullish topologyVersion as greater than
|
||||
// for equality we should only always perform direct equality comparison
|
||||
const topologyVersionsEqual =
|
||||
this.topologyVersion === other?.topologyVersion ||
|
||||
compareTopologyVersion(this.topologyVersion, other?.topologyVersion) === 0;
|
||||
|
||||
const electionIdsEqual =
|
||||
this.electionId != null && other?.electionId != null
|
||||
? compareObjectId(this.electionId, other.electionId) === 0
|
||||
: this.electionId === other?.electionId;
|
||||
|
||||
return (
|
||||
other != null &&
|
||||
errorStrictEqual(this.error, other.error) &&
|
||||
this.type === other.type &&
|
||||
this.minWireVersion === other.minWireVersion &&
|
||||
arrayStrictEqual(this.hosts, other.hosts) &&
|
||||
tagsStrictEqual(this.tags, other.tags) &&
|
||||
this.setName === other.setName &&
|
||||
this.setVersion === other.setVersion &&
|
||||
electionIdsEqual &&
|
||||
this.primary === other.primary &&
|
||||
this.logicalSessionTimeoutMinutes === other.logicalSessionTimeoutMinutes &&
|
||||
topologyVersionsEqual
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Parses a `hello` message and determines the server type
|
||||
export function parseServerType(hello?: Document, options?: ServerDescriptionOptions): ServerType {
|
||||
if (options?.loadBalanced) {
|
||||
return ServerType.LoadBalancer;
|
||||
}
|
||||
|
||||
if (!hello || !hello.ok) {
|
||||
return ServerType.Unknown;
|
||||
}
|
||||
|
||||
if (hello.isreplicaset) {
|
||||
return ServerType.RSGhost;
|
||||
}
|
||||
|
||||
if (hello.msg && hello.msg === 'isdbgrid') {
|
||||
return ServerType.Mongos;
|
||||
}
|
||||
|
||||
if (hello.setName) {
|
||||
if (hello.hidden) {
|
||||
return ServerType.RSOther;
|
||||
} else if (hello.isWritablePrimary) {
|
||||
return ServerType.RSPrimary;
|
||||
} else if (hello.secondary) {
|
||||
return ServerType.RSSecondary;
|
||||
} else if (hello.arbiterOnly) {
|
||||
return ServerType.RSArbiter;
|
||||
} else {
|
||||
return ServerType.RSOther;
|
||||
}
|
||||
}
|
||||
|
||||
return ServerType.Standalone;
|
||||
}
|
||||
|
||||
function tagsStrictEqual(tags: TagSet, tags2: TagSet): boolean {
|
||||
const tagsKeys = Object.keys(tags);
|
||||
const tags2Keys = Object.keys(tags2);
|
||||
|
||||
return (
|
||||
tagsKeys.length === tags2Keys.length &&
|
||||
tagsKeys.every((key: string) => tags2[key] === tags[key])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two topology versions.
|
||||
*
|
||||
* 1. If the response topologyVersion is unset or the ServerDescription's
|
||||
* topologyVersion is null, the client MUST assume the response is more recent.
|
||||
* 1. If the response's topologyVersion.processId is not equal to the
|
||||
* ServerDescription's, the client MUST assume the response is more recent.
|
||||
* 1. If the response's topologyVersion.processId is equal to the
|
||||
* ServerDescription's, the client MUST use the counter field to determine
|
||||
* which topologyVersion is more recent.
|
||||
*
|
||||
* ```ts
|
||||
* currentTv < newTv === -1
|
||||
* currentTv === newTv === 0
|
||||
* currentTv > newTv === 1
|
||||
* ```
|
||||
*/
|
||||
export function compareTopologyVersion(
|
||||
currentTv?: TopologyVersion | null,
|
||||
newTv?: TopologyVersion | null
|
||||
): 0 | -1 | 1 {
|
||||
if (currentTv == null || newTv == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!currentTv.processId.equals(newTv.processId)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO(NODE-2674): Preserve int64 sent from MongoDB
|
||||
const currentCounter = Long.isLong(currentTv.counter)
|
||||
? currentTv.counter
|
||||
: Long.fromNumber(currentTv.counter);
|
||||
const newCounter = Long.isLong(newTv.counter) ? newTv.counter : Long.fromNumber(newTv.counter);
|
||||
|
||||
return currentCounter.compare(newCounter);
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
import { MongoCompatibilityError, MongoInvalidArgumentError } from '../error';
|
||||
import { ReadPreference } from '../read_preference';
|
||||
import { ServerType, TopologyType } from './common';
|
||||
import type { ServerDescription, TagSet } from './server_description';
|
||||
import type { TopologyDescription } from './topology_description';
|
||||
|
||||
// max staleness constants
|
||||
const IDLE_WRITE_PERIOD = 10000;
|
||||
const SMALLEST_MAX_STALENESS_SECONDS = 90;
|
||||
|
||||
// Minimum version to try writes on secondaries.
|
||||
export const MIN_SECONDARY_WRITE_WIRE_VERSION = 13;
|
||||
|
||||
/** @public */
|
||||
export type ServerSelector = (
|
||||
topologyDescription: TopologyDescription,
|
||||
servers: ServerDescription[]
|
||||
) => ServerDescription[];
|
||||
|
||||
/**
|
||||
* Returns a server selector that selects for writable servers
|
||||
*/
|
||||
export function writableServerSelector(): ServerSelector {
|
||||
return (
|
||||
topologyDescription: TopologyDescription,
|
||||
servers: ServerDescription[]
|
||||
): ServerDescription[] =>
|
||||
latencyWindowReducer(
|
||||
topologyDescription,
|
||||
servers.filter((s: ServerDescription) => s.isWritable)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The purpose of this selector is to select the same server, only
|
||||
* if it is in a state that it can have commands sent to it.
|
||||
*/
|
||||
export function sameServerSelector(description?: ServerDescription): ServerSelector {
|
||||
return (
|
||||
topologyDescription: TopologyDescription,
|
||||
servers: ServerDescription[]
|
||||
): ServerDescription[] => {
|
||||
if (!description) return [];
|
||||
// Filter the servers to match the provided description only if
|
||||
// the type is not unknown.
|
||||
return servers.filter(sd => {
|
||||
return sd.address === description.address && sd.type !== ServerType.Unknown;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a server selector that uses a read preference to select a
|
||||
* server potentially for a write on a secondary.
|
||||
*/
|
||||
export function secondaryWritableServerSelector(
|
||||
wireVersion?: number,
|
||||
readPreference?: ReadPreference
|
||||
): ServerSelector {
|
||||
// If server version < 5.0, read preference always primary.
|
||||
// If server version >= 5.0...
|
||||
// - If read preference is supplied, use that.
|
||||
// - If no read preference is supplied, use primary.
|
||||
if (
|
||||
!readPreference ||
|
||||
!wireVersion ||
|
||||
(wireVersion && wireVersion < MIN_SECONDARY_WRITE_WIRE_VERSION)
|
||||
) {
|
||||
return readPreferenceServerSelector(ReadPreference.primary);
|
||||
}
|
||||
return readPreferenceServerSelector(readPreference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the passed in array of servers by the rules of the "Max Staleness" specification
|
||||
* found here: https://github.com/mongodb/specifications/blob/master/source/max-staleness/max-staleness.rst
|
||||
*
|
||||
* @param readPreference - The read preference providing max staleness guidance
|
||||
* @param topologyDescription - The topology description
|
||||
* @param servers - The list of server descriptions to be reduced
|
||||
* @returns The list of servers that satisfy the requirements of max staleness
|
||||
*/
|
||||
function maxStalenessReducer(
|
||||
readPreference: ReadPreference,
|
||||
topologyDescription: TopologyDescription,
|
||||
servers: ServerDescription[]
|
||||
): ServerDescription[] {
|
||||
if (readPreference.maxStalenessSeconds == null || readPreference.maxStalenessSeconds < 0) {
|
||||
return servers;
|
||||
}
|
||||
|
||||
const maxStaleness = readPreference.maxStalenessSeconds;
|
||||
const maxStalenessVariance =
|
||||
(topologyDescription.heartbeatFrequencyMS + IDLE_WRITE_PERIOD) / 1000;
|
||||
if (maxStaleness < maxStalenessVariance) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Option "maxStalenessSeconds" must be at least ${maxStalenessVariance} seconds`
|
||||
);
|
||||
}
|
||||
|
||||
if (maxStaleness < SMALLEST_MAX_STALENESS_SECONDS) {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Option "maxStalenessSeconds" must be at least ${SMALLEST_MAX_STALENESS_SECONDS} seconds`
|
||||
);
|
||||
}
|
||||
|
||||
if (topologyDescription.type === TopologyType.ReplicaSetWithPrimary) {
|
||||
const primary: ServerDescription = Array.from(topologyDescription.servers.values()).filter(
|
||||
primaryFilter
|
||||
)[0];
|
||||
|
||||
return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
|
||||
const stalenessMS =
|
||||
server.lastUpdateTime -
|
||||
server.lastWriteDate -
|
||||
(primary.lastUpdateTime - primary.lastWriteDate) +
|
||||
topologyDescription.heartbeatFrequencyMS;
|
||||
|
||||
const staleness = stalenessMS / 1000;
|
||||
const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
|
||||
if (staleness <= maxStalenessSeconds) {
|
||||
result.push(server);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (topologyDescription.type === TopologyType.ReplicaSetNoPrimary) {
|
||||
if (servers.length === 0) {
|
||||
return servers;
|
||||
}
|
||||
|
||||
const sMax = servers.reduce((max: ServerDescription, s: ServerDescription) =>
|
||||
s.lastWriteDate > max.lastWriteDate ? s : max
|
||||
);
|
||||
|
||||
return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
|
||||
const stalenessMS =
|
||||
sMax.lastWriteDate - server.lastWriteDate + topologyDescription.heartbeatFrequencyMS;
|
||||
|
||||
const staleness = stalenessMS / 1000;
|
||||
const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
|
||||
if (staleness <= maxStalenessSeconds) {
|
||||
result.push(server);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a server's tags match a given set of tags
|
||||
*
|
||||
* @param tagSet - The requested tag set to match
|
||||
* @param serverTags - The server's tags
|
||||
*/
|
||||
function tagSetMatch(tagSet: TagSet, serverTags: TagSet) {
|
||||
const keys = Object.keys(tagSet);
|
||||
const serverTagKeys = Object.keys(serverTags);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i];
|
||||
if (serverTagKeys.indexOf(key) === -1 || serverTags[key] !== tagSet[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a set of server descriptions based on tags requested by the read preference
|
||||
*
|
||||
* @param readPreference - The read preference providing the requested tags
|
||||
* @param servers - The list of server descriptions to reduce
|
||||
* @returns The list of servers matching the requested tags
|
||||
*/
|
||||
function tagSetReducer(
|
||||
readPreference: ReadPreference,
|
||||
servers: ServerDescription[]
|
||||
): ServerDescription[] {
|
||||
if (
|
||||
readPreference.tags == null ||
|
||||
(Array.isArray(readPreference.tags) && readPreference.tags.length === 0)
|
||||
) {
|
||||
return servers;
|
||||
}
|
||||
|
||||
for (let i = 0; i < readPreference.tags.length; ++i) {
|
||||
const tagSet = readPreference.tags[i];
|
||||
const serversMatchingTagset = servers.reduce(
|
||||
(matched: ServerDescription[], server: ServerDescription) => {
|
||||
if (tagSetMatch(tagSet, server.tags)) matched.push(server);
|
||||
return matched;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (serversMatchingTagset.length) {
|
||||
return serversMatchingTagset;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a list of servers to ensure they fall within an acceptable latency window. This is
|
||||
* further specified in the "Server Selection" specification, found here:
|
||||
* https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst
|
||||
*
|
||||
* @param topologyDescription - The topology description
|
||||
* @param servers - The list of servers to reduce
|
||||
* @returns The servers which fall within an acceptable latency window
|
||||
*/
|
||||
function latencyWindowReducer(
|
||||
topologyDescription: TopologyDescription,
|
||||
servers: ServerDescription[]
|
||||
): ServerDescription[] {
|
||||
const low = servers.reduce(
|
||||
(min: number, server: ServerDescription) =>
|
||||
min === -1 ? server.roundTripTime : Math.min(server.roundTripTime, min),
|
||||
-1
|
||||
);
|
||||
|
||||
const high = low + topologyDescription.localThresholdMS;
|
||||
return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
|
||||
if (server.roundTripTime <= high && server.roundTripTime >= low) result.push(server);
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// filters
|
||||
function primaryFilter(server: ServerDescription): boolean {
|
||||
return server.type === ServerType.RSPrimary;
|
||||
}
|
||||
|
||||
function secondaryFilter(server: ServerDescription): boolean {
|
||||
return server.type === ServerType.RSSecondary;
|
||||
}
|
||||
|
||||
function nearestFilter(server: ServerDescription): boolean {
|
||||
return server.type === ServerType.RSSecondary || server.type === ServerType.RSPrimary;
|
||||
}
|
||||
|
||||
function knownFilter(server: ServerDescription): boolean {
|
||||
return server.type !== ServerType.Unknown;
|
||||
}
|
||||
|
||||
function loadBalancerFilter(server: ServerDescription): boolean {
|
||||
return server.type === ServerType.LoadBalancer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function which selects servers based on a provided read preference
|
||||
*
|
||||
* @param readPreference - The read preference to select with
|
||||
*/
|
||||
export function readPreferenceServerSelector(readPreference: ReadPreference): ServerSelector {
|
||||
if (!readPreference.isValid()) {
|
||||
throw new MongoInvalidArgumentError('Invalid read preference specified');
|
||||
}
|
||||
|
||||
return (
|
||||
topologyDescription: TopologyDescription,
|
||||
servers: ServerDescription[]
|
||||
): ServerDescription[] => {
|
||||
const commonWireVersion = topologyDescription.commonWireVersion;
|
||||
if (
|
||||
commonWireVersion &&
|
||||
readPreference.minWireVersion &&
|
||||
readPreference.minWireVersion > commonWireVersion
|
||||
) {
|
||||
throw new MongoCompatibilityError(
|
||||
`Minimum wire version '${readPreference.minWireVersion}' required, but found '${commonWireVersion}'`
|
||||
);
|
||||
}
|
||||
|
||||
if (topologyDescription.type === TopologyType.LoadBalanced) {
|
||||
return servers.filter(loadBalancerFilter);
|
||||
}
|
||||
|
||||
if (topologyDescription.type === TopologyType.Unknown) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
topologyDescription.type === TopologyType.Single ||
|
||||
topologyDescription.type === TopologyType.Sharded
|
||||
) {
|
||||
return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
|
||||
}
|
||||
|
||||
const mode = readPreference.mode;
|
||||
if (mode === ReadPreference.PRIMARY) {
|
||||
return servers.filter(primaryFilter);
|
||||
}
|
||||
|
||||
if (mode === ReadPreference.PRIMARY_PREFERRED) {
|
||||
const result = servers.filter(primaryFilter);
|
||||
if (result.length) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const filter = mode === ReadPreference.NEAREST ? nearestFilter : secondaryFilter;
|
||||
const selectedServers = latencyWindowReducer(
|
||||
topologyDescription,
|
||||
tagSetReducer(
|
||||
readPreference,
|
||||
maxStalenessReducer(readPreference, topologyDescription, servers.filter(filter))
|
||||
)
|
||||
);
|
||||
|
||||
if (mode === ReadPreference.SECONDARY_PREFERRED && selectedServers.length === 0) {
|
||||
return servers.filter(primaryFilter);
|
||||
}
|
||||
|
||||
return selectedServers;
|
||||
};
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import * as dns from 'dns';
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
|
||||
import { MongoRuntimeError } from '../error';
|
||||
import { Logger, LoggerOptions } from '../logger';
|
||||
import { TypedEventEmitter } from '../mongo_types';
|
||||
import { HostAddress } from '../utils';
|
||||
|
||||
/**
|
||||
* Determines whether a provided address matches the provided parent domain in order
|
||||
* to avoid certain attack vectors.
|
||||
*
|
||||
* @param srvAddress - The address to check against a domain
|
||||
* @param parentDomain - The domain to check the provided address against
|
||||
* @returns Whether the provided address matches the parent domain
|
||||
*/
|
||||
function matchesParentDomain(srvAddress: string, parentDomain: string): boolean {
|
||||
const regex = /^.*?\./;
|
||||
const srv = `.${srvAddress.replace(regex, '')}`;
|
||||
const parent = `.${parentDomain.replace(regex, '')}`;
|
||||
return srv.endsWith(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @category Event
|
||||
*/
|
||||
export class SrvPollingEvent {
|
||||
srvRecords: dns.SrvRecord[];
|
||||
constructor(srvRecords: dns.SrvRecord[]) {
|
||||
this.srvRecords = srvRecords;
|
||||
}
|
||||
|
||||
hostnames(): Set<string> {
|
||||
return new Set(this.srvRecords.map(r => HostAddress.fromSrvRecord(r).toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface SrvPollerOptions extends LoggerOptions {
|
||||
srvServiceName: string;
|
||||
srvMaxHosts: number;
|
||||
srvHost: string;
|
||||
heartbeatFrequencyMS: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type SrvPollerEvents = {
|
||||
srvRecordDiscovery(event: SrvPollingEvent): void;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export class SrvPoller extends TypedEventEmitter<SrvPollerEvents> {
|
||||
srvHost: string;
|
||||
rescanSrvIntervalMS: number;
|
||||
heartbeatFrequencyMS: number;
|
||||
logger: Logger;
|
||||
haMode: boolean;
|
||||
generation: number;
|
||||
srvMaxHosts: number;
|
||||
srvServiceName: string;
|
||||
_timeout?: NodeJS.Timeout;
|
||||
|
||||
/** @event */
|
||||
static readonly SRV_RECORD_DISCOVERY = 'srvRecordDiscovery' as const;
|
||||
|
||||
constructor(options: SrvPollerOptions) {
|
||||
super();
|
||||
|
||||
if (!options || !options.srvHost) {
|
||||
throw new MongoRuntimeError('Options for SrvPoller must exist and include srvHost');
|
||||
}
|
||||
|
||||
this.srvHost = options.srvHost;
|
||||
this.srvMaxHosts = options.srvMaxHosts ?? 0;
|
||||
this.srvServiceName = options.srvServiceName ?? 'mongodb';
|
||||
this.rescanSrvIntervalMS = 60000;
|
||||
this.heartbeatFrequencyMS = options.heartbeatFrequencyMS ?? 10000;
|
||||
this.logger = new Logger('srvPoller', options);
|
||||
|
||||
this.haMode = false;
|
||||
this.generation = 0;
|
||||
|
||||
this._timeout = undefined;
|
||||
}
|
||||
|
||||
get srvAddress(): string {
|
||||
return `_${this.srvServiceName}._tcp.${this.srvHost}`;
|
||||
}
|
||||
|
||||
get intervalMS(): number {
|
||||
return this.haMode ? this.heartbeatFrequencyMS : this.rescanSrvIntervalMS;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (!this._timeout) {
|
||||
this.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this.generation += 1;
|
||||
this._timeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
schedule(): void {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
|
||||
this._timeout = setTimeout(() => this._poll(), this.intervalMS);
|
||||
}
|
||||
|
||||
success(srvRecords: dns.SrvRecord[]): void {
|
||||
this.haMode = false;
|
||||
this.schedule();
|
||||
this.emit(SrvPoller.SRV_RECORD_DISCOVERY, new SrvPollingEvent(srvRecords));
|
||||
}
|
||||
|
||||
failure(message: string, obj?: NodeJS.ErrnoException): void {
|
||||
this.logger.warn(message, obj);
|
||||
this.haMode = true;
|
||||
this.schedule();
|
||||
}
|
||||
|
||||
parentDomainMismatch(srvRecord: dns.SrvRecord): void {
|
||||
this.logger.warn(
|
||||
`parent domain mismatch on SRV record (${srvRecord.name}:${srvRecord.port})`,
|
||||
srvRecord
|
||||
);
|
||||
}
|
||||
|
||||
_poll(): void {
|
||||
const generation = this.generation;
|
||||
dns.resolveSrv(this.srvAddress, (err, srvRecords) => {
|
||||
if (generation !== this.generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
this.failure('DNS error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalAddresses: dns.SrvRecord[] = [];
|
||||
for (const record of srvRecords) {
|
||||
if (matchesParentDomain(record.name, this.srvHost)) {
|
||||
finalAddresses.push(record);
|
||||
} else {
|
||||
this.parentDomainMismatch(record);
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalAddresses.length) {
|
||||
this.failure('No valid addresses found at host');
|
||||
return;
|
||||
}
|
||||
|
||||
this.success(finalAddresses);
|
||||
});
|
||||
}
|
||||
}
|
||||
+1007
File diff suppressed because it is too large
Load Diff
+486
@@ -0,0 +1,486 @@
|
||||
import type { ObjectId } from '../bson';
|
||||
import * as WIRE_CONSTANTS from '../cmap/wire_protocol/constants';
|
||||
import { MongoRuntimeError, MongoServerError } from '../error';
|
||||
import { compareObjectId, shuffle } from '../utils';
|
||||
import { ServerType, TopologyType } from './common';
|
||||
import { ServerDescription } from './server_description';
|
||||
import type { SrvPollingEvent } from './srv_polling';
|
||||
|
||||
// constants related to compatibility checks
|
||||
const MIN_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_SERVER_VERSION;
|
||||
const MAX_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_SERVER_VERSION;
|
||||
const MIN_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_WIRE_VERSION;
|
||||
const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
|
||||
|
||||
const MONGOS_OR_UNKNOWN = new Set<ServerType>([ServerType.Mongos, ServerType.Unknown]);
|
||||
const MONGOS_OR_STANDALONE = new Set<ServerType>([ServerType.Mongos, ServerType.Standalone]);
|
||||
const NON_PRIMARY_RS_MEMBERS = new Set<ServerType>([
|
||||
ServerType.RSSecondary,
|
||||
ServerType.RSArbiter,
|
||||
ServerType.RSOther
|
||||
]);
|
||||
|
||||
/** @public */
|
||||
export interface TopologyDescriptionOptions {
|
||||
heartbeatFrequencyMS?: number;
|
||||
localThresholdMS?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a deployment of servers
|
||||
* @public
|
||||
*/
|
||||
export class TopologyDescription {
|
||||
type: TopologyType;
|
||||
setName: string | null;
|
||||
maxSetVersion: number | null;
|
||||
maxElectionId: ObjectId | null;
|
||||
servers: Map<string, ServerDescription>;
|
||||
stale: boolean;
|
||||
compatible: boolean;
|
||||
compatibilityError?: string;
|
||||
logicalSessionTimeoutMinutes: number | null;
|
||||
heartbeatFrequencyMS: number;
|
||||
localThresholdMS: number;
|
||||
commonWireVersion: number;
|
||||
|
||||
/**
|
||||
* Create a TopologyDescription
|
||||
*/
|
||||
constructor(
|
||||
topologyType: TopologyType,
|
||||
serverDescriptions: Map<string, ServerDescription> | null = null,
|
||||
setName: string | null = null,
|
||||
maxSetVersion: number | null = null,
|
||||
maxElectionId: ObjectId | null = null,
|
||||
commonWireVersion: number | null = null,
|
||||
options: TopologyDescriptionOptions | null = null
|
||||
) {
|
||||
options = options ?? {};
|
||||
|
||||
this.type = topologyType ?? TopologyType.Unknown;
|
||||
this.servers = serverDescriptions ?? new Map();
|
||||
this.stale = false;
|
||||
this.compatible = true;
|
||||
this.heartbeatFrequencyMS = options.heartbeatFrequencyMS ?? 0;
|
||||
this.localThresholdMS = options.localThresholdMS ?? 15;
|
||||
this.setName = setName ?? null;
|
||||
this.maxElectionId = maxElectionId ?? null;
|
||||
this.maxSetVersion = maxSetVersion ?? null;
|
||||
this.commonWireVersion = commonWireVersion ?? 0;
|
||||
|
||||
// determine server compatibility
|
||||
for (const serverDescription of this.servers.values()) {
|
||||
// Load balancer mode is always compatible.
|
||||
if (
|
||||
serverDescription.type === ServerType.Unknown ||
|
||||
serverDescription.type === ServerType.LoadBalancer
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (serverDescription.minWireVersion > MAX_SUPPORTED_WIRE_VERSION) {
|
||||
this.compatible = false;
|
||||
this.compatibilityError = `Server at ${serverDescription.address} requires wire version ${serverDescription.minWireVersion}, but this version of the driver only supports up to ${MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${MAX_SUPPORTED_SERVER_VERSION})`;
|
||||
}
|
||||
|
||||
if (serverDescription.maxWireVersion < MIN_SUPPORTED_WIRE_VERSION) {
|
||||
this.compatible = false;
|
||||
this.compatibilityError = `Server at ${serverDescription.address} reports wire version ${serverDescription.maxWireVersion}, but this version of the driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION}).`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever a client updates the TopologyDescription from a hello response, it MUST set
|
||||
// TopologyDescription.logicalSessionTimeoutMinutes to the smallest logicalSessionTimeoutMinutes
|
||||
// value among ServerDescriptions of all data-bearing server types. If any have a null
|
||||
// logicalSessionTimeoutMinutes, then TopologyDescription.logicalSessionTimeoutMinutes MUST be
|
||||
// set to null.
|
||||
this.logicalSessionTimeoutMinutes = null;
|
||||
for (const [, server] of this.servers) {
|
||||
if (server.isReadable) {
|
||||
if (server.logicalSessionTimeoutMinutes == null) {
|
||||
// If any of the servers have a null logicalSessionsTimeout, then the whole topology does
|
||||
this.logicalSessionTimeoutMinutes = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.logicalSessionTimeoutMinutes == null) {
|
||||
// First server with a non null logicalSessionsTimeout
|
||||
this.logicalSessionTimeoutMinutes = server.logicalSessionTimeoutMinutes;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always select the smaller of the:
|
||||
// current server logicalSessionsTimeout and the topologies logicalSessionsTimeout
|
||||
this.logicalSessionTimeoutMinutes = Math.min(
|
||||
this.logicalSessionTimeoutMinutes,
|
||||
server.logicalSessionTimeoutMinutes
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new TopologyDescription based on the SrvPollingEvent
|
||||
* @internal
|
||||
*/
|
||||
updateFromSrvPollingEvent(ev: SrvPollingEvent, srvMaxHosts = 0): TopologyDescription {
|
||||
/** The SRV addresses defines the set of addresses we should be using */
|
||||
const incomingHostnames = ev.hostnames();
|
||||
const currentHostnames = new Set(this.servers.keys());
|
||||
|
||||
const hostnamesToAdd = new Set<string>(incomingHostnames);
|
||||
const hostnamesToRemove = new Set<string>();
|
||||
for (const hostname of currentHostnames) {
|
||||
// filter hostnamesToAdd (made from incomingHostnames) down to what is *not* present in currentHostnames
|
||||
hostnamesToAdd.delete(hostname);
|
||||
if (!incomingHostnames.has(hostname)) {
|
||||
// If the SRV Records no longer include this hostname
|
||||
// we have to stop using it
|
||||
hostnamesToRemove.add(hostname);
|
||||
}
|
||||
}
|
||||
|
||||
if (hostnamesToAdd.size === 0 && hostnamesToRemove.size === 0) {
|
||||
// No new hosts to add and none to remove
|
||||
return this;
|
||||
}
|
||||
|
||||
const serverDescriptions = new Map(this.servers);
|
||||
for (const removedHost of hostnamesToRemove) {
|
||||
serverDescriptions.delete(removedHost);
|
||||
}
|
||||
|
||||
if (hostnamesToAdd.size > 0) {
|
||||
if (srvMaxHosts === 0) {
|
||||
// Add all!
|
||||
for (const hostToAdd of hostnamesToAdd) {
|
||||
serverDescriptions.set(hostToAdd, new ServerDescription(hostToAdd));
|
||||
}
|
||||
} else if (serverDescriptions.size < srvMaxHosts) {
|
||||
// Add only the amount needed to get us back to srvMaxHosts
|
||||
const selectedHosts = shuffle(hostnamesToAdd, srvMaxHosts - serverDescriptions.size);
|
||||
for (const selectedHostToAdd of selectedHosts) {
|
||||
serverDescriptions.set(selectedHostToAdd, new ServerDescription(selectedHostToAdd));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TopologyDescription(
|
||||
this.type,
|
||||
serverDescriptions,
|
||||
this.setName,
|
||||
this.maxSetVersion,
|
||||
this.maxElectionId,
|
||||
this.commonWireVersion,
|
||||
{ heartbeatFrequencyMS: this.heartbeatFrequencyMS, localThresholdMS: this.localThresholdMS }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this description updated with a given ServerDescription
|
||||
* @internal
|
||||
*/
|
||||
update(serverDescription: ServerDescription): TopologyDescription {
|
||||
const address = serverDescription.address;
|
||||
|
||||
// potentially mutated values
|
||||
let { type: topologyType, setName, maxSetVersion, maxElectionId, commonWireVersion } = this;
|
||||
|
||||
const serverType = serverDescription.type;
|
||||
const serverDescriptions = new Map(this.servers);
|
||||
|
||||
// update common wire version
|
||||
if (serverDescription.maxWireVersion !== 0) {
|
||||
if (commonWireVersion == null) {
|
||||
commonWireVersion = serverDescription.maxWireVersion;
|
||||
} else {
|
||||
commonWireVersion = Math.min(commonWireVersion, serverDescription.maxWireVersion);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof serverDescription.setName === 'string' &&
|
||||
typeof setName === 'string' &&
|
||||
serverDescription.setName !== setName
|
||||
) {
|
||||
if (topologyType === TopologyType.Single) {
|
||||
// "Single" Topology with setName mismatch is direct connection usage, mark unknown do not remove
|
||||
serverDescription = new ServerDescription(address);
|
||||
} else {
|
||||
serverDescriptions.delete(address);
|
||||
}
|
||||
}
|
||||
|
||||
// update the actual server description
|
||||
serverDescriptions.set(address, serverDescription);
|
||||
|
||||
if (topologyType === TopologyType.Single) {
|
||||
// once we are defined as single, that never changes
|
||||
return new TopologyDescription(
|
||||
TopologyType.Single,
|
||||
serverDescriptions,
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId,
|
||||
commonWireVersion,
|
||||
{ heartbeatFrequencyMS: this.heartbeatFrequencyMS, localThresholdMS: this.localThresholdMS }
|
||||
);
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.Unknown) {
|
||||
if (serverType === ServerType.Standalone && this.servers.size !== 1) {
|
||||
serverDescriptions.delete(address);
|
||||
} else {
|
||||
topologyType = topologyTypeForServerType(serverType);
|
||||
}
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.Sharded) {
|
||||
if (!MONGOS_OR_UNKNOWN.has(serverType)) {
|
||||
serverDescriptions.delete(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.ReplicaSetNoPrimary) {
|
||||
if (MONGOS_OR_STANDALONE.has(serverType)) {
|
||||
serverDescriptions.delete(address);
|
||||
}
|
||||
|
||||
if (serverType === ServerType.RSPrimary) {
|
||||
const result = updateRsFromPrimary(
|
||||
serverDescriptions,
|
||||
serverDescription,
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId
|
||||
);
|
||||
|
||||
topologyType = result[0];
|
||||
setName = result[1];
|
||||
maxSetVersion = result[2];
|
||||
maxElectionId = result[3];
|
||||
} else if (NON_PRIMARY_RS_MEMBERS.has(serverType)) {
|
||||
const result = updateRsNoPrimaryFromMember(serverDescriptions, serverDescription, setName);
|
||||
topologyType = result[0];
|
||||
setName = result[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.ReplicaSetWithPrimary) {
|
||||
if (MONGOS_OR_STANDALONE.has(serverType)) {
|
||||
serverDescriptions.delete(address);
|
||||
topologyType = checkHasPrimary(serverDescriptions);
|
||||
} else if (serverType === ServerType.RSPrimary) {
|
||||
const result = updateRsFromPrimary(
|
||||
serverDescriptions,
|
||||
serverDescription,
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId
|
||||
);
|
||||
|
||||
topologyType = result[0];
|
||||
setName = result[1];
|
||||
maxSetVersion = result[2];
|
||||
maxElectionId = result[3];
|
||||
} else if (NON_PRIMARY_RS_MEMBERS.has(serverType)) {
|
||||
topologyType = updateRsWithPrimaryFromMember(
|
||||
serverDescriptions,
|
||||
serverDescription,
|
||||
setName
|
||||
);
|
||||
} else {
|
||||
topologyType = checkHasPrimary(serverDescriptions);
|
||||
}
|
||||
}
|
||||
|
||||
return new TopologyDescription(
|
||||
topologyType,
|
||||
serverDescriptions,
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId,
|
||||
commonWireVersion,
|
||||
{ heartbeatFrequencyMS: this.heartbeatFrequencyMS, localThresholdMS: this.localThresholdMS }
|
||||
);
|
||||
}
|
||||
|
||||
get error(): MongoServerError | null {
|
||||
const descriptionsWithError = Array.from(this.servers.values()).filter(
|
||||
(sd: ServerDescription) => sd.error
|
||||
);
|
||||
|
||||
if (descriptionsWithError.length > 0) {
|
||||
return descriptionsWithError[0].error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the topology description has any known servers
|
||||
*/
|
||||
get hasKnownServers(): boolean {
|
||||
return Array.from(this.servers.values()).some(
|
||||
(sd: ServerDescription) => sd.type !== ServerType.Unknown
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this topology description has a data-bearing server available.
|
||||
*/
|
||||
get hasDataBearingServers(): boolean {
|
||||
return Array.from(this.servers.values()).some((sd: ServerDescription) => sd.isDataBearing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the topology has a definition for the provided address
|
||||
* @internal
|
||||
*/
|
||||
hasServer(address: string): boolean {
|
||||
return this.servers.has(address);
|
||||
}
|
||||
}
|
||||
|
||||
function topologyTypeForServerType(serverType: ServerType): TopologyType {
|
||||
switch (serverType) {
|
||||
case ServerType.Standalone:
|
||||
return TopologyType.Single;
|
||||
case ServerType.Mongos:
|
||||
return TopologyType.Sharded;
|
||||
case ServerType.RSPrimary:
|
||||
return TopologyType.ReplicaSetWithPrimary;
|
||||
case ServerType.RSOther:
|
||||
case ServerType.RSSecondary:
|
||||
return TopologyType.ReplicaSetNoPrimary;
|
||||
default:
|
||||
return TopologyType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRsFromPrimary(
|
||||
serverDescriptions: Map<string, ServerDescription>,
|
||||
serverDescription: ServerDescription,
|
||||
setName: string | null = null,
|
||||
maxSetVersion: number | null = null,
|
||||
maxElectionId: ObjectId | null = null
|
||||
): [TopologyType, string | null, number | null, ObjectId | null] {
|
||||
setName = setName || serverDescription.setName;
|
||||
if (setName !== serverDescription.setName) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||||
}
|
||||
|
||||
const electionId = serverDescription.electionId ? serverDescription.electionId : null;
|
||||
if (serverDescription.setVersion && electionId) {
|
||||
if (maxSetVersion && maxElectionId) {
|
||||
if (
|
||||
maxSetVersion > serverDescription.setVersion ||
|
||||
compareObjectId(maxElectionId, electionId) > 0
|
||||
) {
|
||||
// this primary is stale, we must remove it
|
||||
serverDescriptions.set(
|
||||
serverDescription.address,
|
||||
new ServerDescription(serverDescription.address)
|
||||
);
|
||||
|
||||
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||||
}
|
||||
}
|
||||
|
||||
maxElectionId = serverDescription.electionId;
|
||||
}
|
||||
|
||||
if (
|
||||
serverDescription.setVersion != null &&
|
||||
(maxSetVersion == null || serverDescription.setVersion > maxSetVersion)
|
||||
) {
|
||||
maxSetVersion = serverDescription.setVersion;
|
||||
}
|
||||
|
||||
// We've heard from the primary. Is it the same primary as before?
|
||||
for (const [address, server] of serverDescriptions) {
|
||||
if (server.type === ServerType.RSPrimary && server.address !== serverDescription.address) {
|
||||
// Reset old primary's type to Unknown.
|
||||
serverDescriptions.set(address, new ServerDescription(server.address));
|
||||
|
||||
// There can only be one primary
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Discover new hosts from this primary's response.
|
||||
serverDescription.allHosts.forEach((address: string) => {
|
||||
if (!serverDescriptions.has(address)) {
|
||||
serverDescriptions.set(address, new ServerDescription(address));
|
||||
}
|
||||
});
|
||||
|
||||
// Remove hosts not in the response.
|
||||
const currentAddresses = Array.from(serverDescriptions.keys());
|
||||
const responseAddresses = serverDescription.allHosts;
|
||||
currentAddresses
|
||||
.filter((addr: string) => responseAddresses.indexOf(addr) === -1)
|
||||
.forEach((address: string) => {
|
||||
serverDescriptions.delete(address);
|
||||
});
|
||||
|
||||
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||||
}
|
||||
|
||||
function updateRsWithPrimaryFromMember(
|
||||
serverDescriptions: Map<string, ServerDescription>,
|
||||
serverDescription: ServerDescription,
|
||||
setName: string | null = null
|
||||
): TopologyType {
|
||||
if (setName == null) {
|
||||
// TODO(NODE-3483): should be an appropriate runtime error
|
||||
throw new MongoRuntimeError('Argument "setName" is required if connected to a replica set');
|
||||
}
|
||||
|
||||
if (
|
||||
setName !== serverDescription.setName ||
|
||||
(serverDescription.me && serverDescription.address !== serverDescription.me)
|
||||
) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
}
|
||||
|
||||
return checkHasPrimary(serverDescriptions);
|
||||
}
|
||||
|
||||
function updateRsNoPrimaryFromMember(
|
||||
serverDescriptions: Map<string, ServerDescription>,
|
||||
serverDescription: ServerDescription,
|
||||
setName: string | null = null
|
||||
): [TopologyType, string | null] {
|
||||
const topologyType = TopologyType.ReplicaSetNoPrimary;
|
||||
setName = setName ?? serverDescription.setName;
|
||||
if (setName !== serverDescription.setName) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
return [topologyType, setName];
|
||||
}
|
||||
|
||||
serverDescription.allHosts.forEach((address: string) => {
|
||||
if (!serverDescriptions.has(address)) {
|
||||
serverDescriptions.set(address, new ServerDescription(address));
|
||||
}
|
||||
});
|
||||
|
||||
if (serverDescription.me && serverDescription.address !== serverDescription.me) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
}
|
||||
|
||||
return [topologyType, setName];
|
||||
}
|
||||
|
||||
function checkHasPrimary(serverDescriptions: Map<string, ServerDescription>): TopologyType {
|
||||
for (const serverDescription of serverDescriptions.values()) {
|
||||
if (serverDescription.type === ServerType.RSPrimary) {
|
||||
return TopologyType.ReplicaSetWithPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
return TopologyType.ReplicaSetNoPrimary;
|
||||
}
|
||||
+1075
File diff suppressed because it is too large
Load Diff
+132
@@ -0,0 +1,132 @@
|
||||
import { MongoInvalidArgumentError } from './error';
|
||||
|
||||
/** @public */
|
||||
export type SortDirection =
|
||||
| 1
|
||||
| -1
|
||||
| 'asc'
|
||||
| 'desc'
|
||||
| 'ascending'
|
||||
| 'descending'
|
||||
| { $meta: string };
|
||||
|
||||
/** @public */
|
||||
export type Sort =
|
||||
| string
|
||||
| Exclude<SortDirection, { $meta: string }>
|
||||
| string[]
|
||||
| { [key: string]: SortDirection }
|
||||
| Map<string, SortDirection>
|
||||
| [string, SortDirection][]
|
||||
| [string, SortDirection];
|
||||
|
||||
/** Below stricter types were created for sort that correspond with type that the cmd takes */
|
||||
|
||||
/** @internal */
|
||||
export type SortDirectionForCmd = 1 | -1 | { $meta: string };
|
||||
|
||||
/** @internal */
|
||||
export type SortForCmd = Map<string, SortDirectionForCmd>;
|
||||
|
||||
/** @internal */
|
||||
type SortPairForCmd = [string, SortDirectionForCmd];
|
||||
|
||||
/** @internal */
|
||||
function prepareDirection(direction: any = 1): SortDirectionForCmd {
|
||||
const value = `${direction}`.toLowerCase();
|
||||
if (isMeta(direction)) return direction;
|
||||
switch (value) {
|
||||
case 'ascending':
|
||||
case 'asc':
|
||||
case '1':
|
||||
return 1;
|
||||
case 'descending':
|
||||
case 'desc':
|
||||
case '-1':
|
||||
return -1;
|
||||
default:
|
||||
throw new MongoInvalidArgumentError(`Invalid sort direction: ${JSON.stringify(direction)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function isMeta(t: SortDirection): t is { $meta: string } {
|
||||
return typeof t === 'object' && t != null && '$meta' in t && typeof t.$meta === 'string';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function isPair(t: Sort): t is [string, SortDirection] {
|
||||
if (Array.isArray(t) && t.length === 2) {
|
||||
try {
|
||||
prepareDirection(t[1]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDeep(t: Sort): t is [string, SortDirection][] {
|
||||
return Array.isArray(t) && Array.isArray(t[0]);
|
||||
}
|
||||
|
||||
function isMap(t: Sort): t is Map<string, SortDirection> {
|
||||
return t instanceof Map && t.size > 0;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function pairToMap(v: [string, SortDirection]): SortForCmd {
|
||||
return new Map([[`${v[0]}`, prepareDirection([v[1]])]]);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function deepToMap(t: [string, SortDirection][]): SortForCmd {
|
||||
const sortEntries: SortPairForCmd[] = t.map(([k, v]) => [`${k}`, prepareDirection(v)]);
|
||||
return new Map(sortEntries);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function stringsToMap(t: string[]): SortForCmd {
|
||||
const sortEntries: SortPairForCmd[] = t.map(key => [`${key}`, 1]);
|
||||
return new Map(sortEntries);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function objectToMap(t: { [key: string]: SortDirection }): SortForCmd {
|
||||
const sortEntries: SortPairForCmd[] = Object.entries(t).map(([k, v]) => [
|
||||
`${k}`,
|
||||
prepareDirection(v)
|
||||
]);
|
||||
return new Map(sortEntries);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function mapToMap(t: Map<string, SortDirection>): SortForCmd {
|
||||
const sortEntries: SortPairForCmd[] = Array.from(t).map(([k, v]) => [
|
||||
`${k}`,
|
||||
prepareDirection(v)
|
||||
]);
|
||||
return new Map(sortEntries);
|
||||
}
|
||||
|
||||
/** converts a Sort type into a type that is valid for the server (SortForCmd) */
|
||||
export function formatSort(
|
||||
sort: Sort | undefined,
|
||||
direction?: SortDirection
|
||||
): SortForCmd | undefined {
|
||||
if (sort == null) return undefined;
|
||||
if (typeof sort === 'string') return new Map([[sort, prepareDirection(direction)]]);
|
||||
if (typeof sort !== 'object') {
|
||||
throw new MongoInvalidArgumentError(
|
||||
`Invalid sort format: ${JSON.stringify(sort)} Sort must be a valid object`
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(sort)) {
|
||||
return isMap(sort) ? mapToMap(sort) : Object.keys(sort).length ? objectToMap(sort) : undefined;
|
||||
}
|
||||
if (!sort.length) return undefined;
|
||||
if (isDeep(sort)) return deepToMap(sort);
|
||||
if (isPair(sort)) return pairToMap(sort);
|
||||
return stringsToMap(sort);
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import type { Document } from './bson';
|
||||
import { MongoRuntimeError, MongoTransactionError } from './error';
|
||||
import type { CommandOperationOptions } from './operations/command';
|
||||
import { ReadConcern, ReadConcernLike } from './read_concern';
|
||||
import { ReadPreference } from './read_preference';
|
||||
import type { Server } from './sdam/server';
|
||||
import { WriteConcern } from './write_concern';
|
||||
|
||||
/** @internal */
|
||||
export const TxnState = Object.freeze({
|
||||
NO_TRANSACTION: 'NO_TRANSACTION',
|
||||
STARTING_TRANSACTION: 'STARTING_TRANSACTION',
|
||||
TRANSACTION_IN_PROGRESS: 'TRANSACTION_IN_PROGRESS',
|
||||
TRANSACTION_COMMITTED: 'TRANSACTION_COMMITTED',
|
||||
TRANSACTION_COMMITTED_EMPTY: 'TRANSACTION_COMMITTED_EMPTY',
|
||||
TRANSACTION_ABORTED: 'TRANSACTION_ABORTED'
|
||||
} as const);
|
||||
|
||||
/** @internal */
|
||||
export type TxnState = typeof TxnState[keyof typeof TxnState];
|
||||
|
||||
const stateMachine: { [state in TxnState]: TxnState[] } = {
|
||||
[TxnState.NO_TRANSACTION]: [TxnState.NO_TRANSACTION, TxnState.STARTING_TRANSACTION],
|
||||
[TxnState.STARTING_TRANSACTION]: [
|
||||
TxnState.TRANSACTION_IN_PROGRESS,
|
||||
TxnState.TRANSACTION_COMMITTED,
|
||||
TxnState.TRANSACTION_COMMITTED_EMPTY,
|
||||
TxnState.TRANSACTION_ABORTED
|
||||
],
|
||||
[TxnState.TRANSACTION_IN_PROGRESS]: [
|
||||
TxnState.TRANSACTION_IN_PROGRESS,
|
||||
TxnState.TRANSACTION_COMMITTED,
|
||||
TxnState.TRANSACTION_ABORTED
|
||||
],
|
||||
[TxnState.TRANSACTION_COMMITTED]: [
|
||||
TxnState.TRANSACTION_COMMITTED,
|
||||
TxnState.TRANSACTION_COMMITTED_EMPTY,
|
||||
TxnState.STARTING_TRANSACTION,
|
||||
TxnState.NO_TRANSACTION
|
||||
],
|
||||
[TxnState.TRANSACTION_ABORTED]: [TxnState.STARTING_TRANSACTION, TxnState.NO_TRANSACTION],
|
||||
[TxnState.TRANSACTION_COMMITTED_EMPTY]: [
|
||||
TxnState.TRANSACTION_COMMITTED_EMPTY,
|
||||
TxnState.NO_TRANSACTION
|
||||
]
|
||||
};
|
||||
|
||||
const ACTIVE_STATES: Set<TxnState> = new Set([
|
||||
TxnState.STARTING_TRANSACTION,
|
||||
TxnState.TRANSACTION_IN_PROGRESS
|
||||
]);
|
||||
|
||||
const COMMITTED_STATES: Set<TxnState> = new Set([
|
||||
TxnState.TRANSACTION_COMMITTED,
|
||||
TxnState.TRANSACTION_COMMITTED_EMPTY,
|
||||
TxnState.TRANSACTION_ABORTED
|
||||
]);
|
||||
|
||||
/**
|
||||
* Configuration options for a transaction.
|
||||
* @public
|
||||
*/
|
||||
export interface TransactionOptions extends CommandOperationOptions {
|
||||
// TODO(NODE-3344): These options use the proper class forms of these settings, it should accept the basic enum values too
|
||||
/** A default read concern for commands in this transaction */
|
||||
readConcern?: ReadConcernLike;
|
||||
/** A default writeConcern for commands in this transaction */
|
||||
writeConcern?: WriteConcern;
|
||||
/** A default read preference for commands in this transaction */
|
||||
readPreference?: ReadPreference;
|
||||
/** Specifies the maximum amount of time to allow a commit action on a transaction to run in milliseconds */
|
||||
maxCommitTimeMS?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A class maintaining state related to a server transaction. Internal Only
|
||||
*/
|
||||
export class Transaction {
|
||||
/** @internal */
|
||||
state: TxnState;
|
||||
options: TransactionOptions;
|
||||
/** @internal */
|
||||
_pinnedServer?: Server;
|
||||
/** @internal */
|
||||
_recoveryToken?: Document;
|
||||
|
||||
/** Create a transaction @internal */
|
||||
constructor(options?: TransactionOptions) {
|
||||
options = options ?? {};
|
||||
this.state = TxnState.NO_TRANSACTION;
|
||||
this.options = {};
|
||||
|
||||
const writeConcern = WriteConcern.fromOptions(options);
|
||||
if (writeConcern) {
|
||||
if (writeConcern.w === 0) {
|
||||
throw new MongoTransactionError('Transactions do not support unacknowledged write concern');
|
||||
}
|
||||
|
||||
this.options.writeConcern = writeConcern;
|
||||
}
|
||||
|
||||
if (options.readConcern) {
|
||||
this.options.readConcern = ReadConcern.fromOptions(options);
|
||||
}
|
||||
|
||||
if (options.readPreference) {
|
||||
this.options.readPreference = ReadPreference.fromOptions(options);
|
||||
}
|
||||
|
||||
if (options.maxCommitTimeMS) {
|
||||
this.options.maxTimeMS = options.maxCommitTimeMS;
|
||||
}
|
||||
|
||||
// TODO: This isn't technically necessary
|
||||
this._pinnedServer = undefined;
|
||||
this._recoveryToken = undefined;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
get server(): Server | undefined {
|
||||
return this._pinnedServer;
|
||||
}
|
||||
|
||||
get recoveryToken(): Document | undefined {
|
||||
return this._recoveryToken;
|
||||
}
|
||||
|
||||
get isPinned(): boolean {
|
||||
return !!this.server;
|
||||
}
|
||||
|
||||
/** @returns Whether the transaction has started */
|
||||
get isStarting(): boolean {
|
||||
return this.state === TxnState.STARTING_TRANSACTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Whether this session is presently in a transaction
|
||||
*/
|
||||
get isActive(): boolean {
|
||||
return ACTIVE_STATES.has(this.state);
|
||||
}
|
||||
|
||||
get isCommitted(): boolean {
|
||||
return COMMITTED_STATES.has(this.state);
|
||||
}
|
||||
/**
|
||||
* Transition the transaction in the state machine
|
||||
* @internal
|
||||
* @param nextState - The new state to transition to
|
||||
*/
|
||||
transition(nextState: TxnState): void {
|
||||
const nextStates = stateMachine[this.state];
|
||||
if (nextStates && nextStates.includes(nextState)) {
|
||||
this.state = nextState;
|
||||
if (
|
||||
this.state === TxnState.NO_TRANSACTION ||
|
||||
this.state === TxnState.STARTING_TRANSACTION ||
|
||||
this.state === TxnState.TRANSACTION_ABORTED
|
||||
) {
|
||||
this.unpinServer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new MongoRuntimeError(
|
||||
`Attempted illegal state transition from [${this.state}] to [${nextState}]`
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
pinServer(server: Server): void {
|
||||
if (this.isActive) {
|
||||
this._pinnedServer = server;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
unpinServer(): void {
|
||||
this._pinnedServer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTransactionCommand(command: Document): boolean {
|
||||
return !!(command.commitTransaction || command.abortTransaction);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user