something is working
This commit is contained in:
+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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user