@skybloxsystems/ticket-bot
Version:
526 lines (453 loc) • 15.5 kB
text/typescript
import { indexInformation, IndexInformationOptions } from './common_functions';
import { AbstractOperation, Aspect, defineAspects } from './operation';
import { MONGODB_ERROR_CODES, MongoServerError, MongoCompatibilityError } from '../error';
import {
maxWireVersion,
parseIndexOptions,
MongoDBNamespace,
Callback,
getTopology
} from '../utils';
import {
CommandOperation,
CommandOperationOptions,
OperationParent,
CollationOptions
} from './command';
import { ReadPreference } from '../read_preference';
import type { Server } from '../sdam/server';
import type { Document } from '../bson';
import type { Collection } from '../collection';
import type { Db } from '../db';
import { AbstractCursor } from '../cursor/abstract_cursor';
import type { ClientSession } from '../sessions';
import { executeOperation, ExecutionResult } from './execute_operation';
import type { OneOrMore } from '../mongo_types';
const LIST_INDEXES_WIRE_VERSION = 3;
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;
/** @public */
export type IndexSpecification = OneOrMore<
| string
| [string, IndexDirection]
| { [key: string]: IndexDirection }
| [string, IndexDirection][]
| { [key: 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: Document;
}
/** @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 makeIndexSpec(indexSpec: IndexSpecification, options: any): IndexDescription {
const indexParameters = parseIndexOptions(indexSpec);
// Generate the index name
const name = typeof options.name === 'string' ? options.name : indexParameters.name;
// Set up the index
const finalIndexSpec: Document = { name, key: indexParameters.fieldHash };
// merge valid index options into the index spec
for (const optionName in options) {
if (VALID_INDEX_OPTIONS.has(optionName)) {
finalIndexSpec[optionName] = options[optionName];
}
}
return finalIndexSpec as IndexDescription;
}
/** @internal */
export class IndexesOperation extends AbstractOperation<Document[]> {
options: IndexInformationOptions;
collection: Collection;
constructor(collection: Collection, options: IndexInformationOptions) {
super(options);
this.options = options;
this.collection = collection;
}
execute(server: Server, session: ClientSession, 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> {
options: CreateIndexesOptions;
collectionName: string;
indexes: IndexDescription[];
constructor(
parent: OperationParent,
collectionName: string,
indexes: IndexDescription[],
options?: CreateIndexesOptions
) {
super(parent, options);
this.options = options ?? {};
this.collectionName = collectionName;
this.indexes = indexes;
}
execute(server: Server, session: ClientSession, callback: Callback<T>): void {
const options = this.options;
const indexes = this.indexes;
const serverWireVersion = maxWireVersion(server);
// Ensure we generate the correct name if the parameter is not set
for (let i = 0; i < indexes.length; i++) {
// Did the user pass in a collation, check if our write server supports it
if (indexes[i].collation && serverWireVersion < 5) {
callback(
new MongoCompatibilityError(
`Server ${server.name}, which reports wire version ${serverWireVersion}, ` +
'does not support collation'
)
);
return;
}
if (indexes[i].name == null) {
const keys = [];
for (const name in indexes[i].key) {
keys.push(`${name}_${indexes[i].key[name]}`);
}
// Set the name
indexes[i].name = keys.join('_');
}
}
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
) {
// createIndex can be called with a variety of styles:
// coll.createIndex('a');
// coll.createIndex({ a: 1 });
// coll.createIndex([['a', 1]]);
// createIndexes is always called with an array of index spec objects
super(parent, collectionName, [makeIndexSpec(indexSpec, options)], options);
}
execute(server: Server, session: ClientSession, 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;
collectionName: string;
constructor(
db: Db,
collectionName: string,
indexSpec: IndexSpecification,
options?: CreateIndexesOptions
) {
super(db, collectionName, indexSpec, options);
this.readPreference = ReadPreference.primary;
this.db = db;
this.collectionName = collectionName;
}
execute(server: Server, session: ClientSession, 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> {
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;
}
execute(server: Server, session: ClientSession, 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);
}
execute(server: Server, session: ClientSession, 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> {
options: ListIndexesOptions;
collectionNamespace: MongoDBNamespace;
constructor(collection: Collection, options?: ListIndexesOptions) {
super(collection, options);
this.options = options ?? {};
this.collectionNamespace = collection.s.namespace;
}
execute(server: Server, session: ClientSession, callback: Callback<Document>): void {
const serverWireVersion = maxWireVersion(server);
if (serverWireVersion < LIST_INDEXES_WIRE_VERSION) {
const systemIndexesNS = this.collectionNamespace.withCollection('system.indexes');
const collectionNS = this.collectionNamespace.toString();
server.query(
systemIndexesNS,
{ query: { ns: collectionNS } },
{ ...this.options, readPreference: this.readPreference },
callback
);
return;
}
const cursor = this.options.batchSize ? { batchSize: this.options.batchSize } : {};
super.executeCommand(
server,
session,
{ listIndexes: this.collectionNamespace.collection, cursor },
callback
);
}
}
/** @public */
export class ListIndexesCursor extends AbstractCursor {
parent: Collection;
options?: ListIndexesOptions;
constructor(collection: Collection, options?: ListIndexesOptions) {
super(getTopology(collection), collection.s.namespace, options);
this.parent = collection;
this.options = options;
}
clone(): ListIndexesCursor {
return new ListIndexesCursor(this.parent, {
...this.options,
...this.cursorOptions
});
}
/** @internal */
_initialize(session: ClientSession | undefined, callback: Callback<ExecutionResult>): void {
const operation = new ListIndexesOperation(this.parent, {
...this.cursorOptions,
...this.options,
session
});
executeOperation(getTopology(this.parent), operation, (err, response) => {
if (err || response == null) return callback(err);
// TODO: NODE-2882
callback(undefined, { server: operation.server, session, response });
});
}
}
/** @internal */
export class IndexExistsOperation extends AbstractOperation<boolean> {
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;
}
execute(server: Server, session: ClientSession, 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> {
options: IndexInformationOptions;
db: Db;
name: string;
constructor(db: Db, name: string, options?: IndexInformationOptions) {
super(options);
this.options = options ?? {};
this.db = db;
this.name = name;
}
execute(server: Server, session: ClientSession, 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]);