From 8a0866f959044a8b8bf8b537f7ad88b17f2a83f3 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Mon, 27 Jul 2026 19:27:54 -0700 Subject: [PATCH 01/23] Allow passthrough options on createIndexes --- src/collection.ts | 64 ++- src/db.ts | 8 +- src/gridfs/upload.ts | 6 + src/index.ts | 3 + src/operations/create_collection.ts | 1 + src/operations/indexes.ts | 296 ++++++++++++- src/utils.ts | 66 +++ .../create_indexes_option_validation.test.ts | 405 ++++++++++++++++++ test/integration/index_management.test.ts | 41 ++ test/unit/collection.test.ts | 117 ++++- test/unit/operations/indexes.test.ts | 58 +++ 11 files changed, 1036 insertions(+), 29 deletions(-) create mode 100644 test/integration/index-management/create_indexes_option_validation.test.ts diff --git a/src/collection.ts b/src/collection.ts index e3a52057363..e6cea762550 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -54,14 +54,17 @@ import { type FindOneAndUpdateOptions } from './operations/find_and_modify'; import { + type CreateIndexesCommandOptions, CreateIndexesOperation, type CreateIndexesOptions, + type CreateIndexOptions, type DropIndexesOptions, DropIndexOperation, type IndexDescription, type IndexDescriptionCompact, type IndexDescriptionInfo, type IndexInformationOptions, + type IndexOptions, type IndexSpecification, type ListIndexesOptions } from './operations/indexes'; @@ -94,6 +97,7 @@ import { DEFAULT_PK_FACTORY, MongoDBCollectionNamespace, normalizeHintField, + resolveCommandOptions, resolveOptions } from './utils'; import { WriteConcern, type WriteConcernOptions } from './write_concern'; @@ -609,7 +613,7 @@ export class Collection { * Creates an index on the db and collection collection. * * @param indexSpec - The field name or index specification to create an index for - * @param options - Optional settings for the command + * @param indexOptions - Optional settings for the command * * @example * ```ts @@ -633,19 +637,51 @@ export class Collection { * await collection.createIndex(['j', ['k', -1], { l: '2d' }]) * ``` */ + createIndex(indexSpec: IndexSpecification, options?: CreateIndexesOptions): Promise; + + /** + * Creates an index on the db and collection collection. + * + * Options for the index itself are given in `indexOptions`, and options for the + * `createIndexes` command are given separately in `commandOptions`. Unknown fields in + * `indexOptions` are passed through to the server for validation rather than being dropped. + * + * @param keys - The field name or index specification to create an index for + * @param indexOptions - Optional settings for the index + * @param commandOptions - Optional settings for the `createIndexes` command + */ + createIndex( + keys: IndexSpecification, + indexOptions?: IndexOptions, + commandOptions?: CreateIndexOptions + ): Promise; + async createIndex( indexSpec: IndexSpecification, - options?: CreateIndexesOptions + indexOptions?: CreateIndexesOptions | IndexOptions, + commandOptions?: CreateIndexOptions ): Promise { - const indexes = await executeOperation( - this.client, - CreateIndexesOperation.fromIndexSpecification( - this, - this.collectionName, - indexSpec, - resolveOptions(this, options) - ) - ); + // When commandOptions is provided the caller has separated the two kinds of options, so the + // index options are forwarded as-is and only the command options inherit from the parent. + // Otherwise indexOptions is both, and it inherits from the parent as it always has. + const operation = + commandOptions == null + ? CreateIndexesOperation.fromIndexSpecification( + this, + this.collectionName, + indexSpec, + /*allowUnknownIndexOptions=*/ false, + resolveOptions(this, indexOptions) // at this point indexOptions is the combined index and command options + ) + : CreateIndexesOperation.fromIndexSpecification( + this, + this.collectionName, + indexSpec, + /*allowUnknownIndexOptions=*/ true, + indexOptions, + resolveCommandOptions(this, commandOptions) + ); + const indexes = await executeOperation(this.client, operation); return indexes[0]; } @@ -683,7 +719,8 @@ export class Collection { */ async createIndexes( indexSpecs: IndexDescription[], - options?: CreateIndexesOptions + options?: CreateIndexesOptions, + commandOptions?: CreateIndexesCommandOptions ): Promise { return await executeOperation( this.client, @@ -691,7 +728,8 @@ export class Collection { this, this.collectionName, indexSpecs, - resolveOptions(this, { ...options, maxTimeMS: undefined }) + resolveOptions(this, { ...options, maxTimeMS: undefined }), + commandOptions ) ); } diff --git a/src/db.ts b/src/db.ts index 0907b6f0a01..1edf1072bfe 100644 --- a/src/db.ts +++ b/src/db.ts @@ -454,7 +454,13 @@ export class Db { ): Promise { const indexes = await executeOperation( this.client, - CreateIndexesOperation.fromIndexSpecification(this, name, indexSpec, options) + CreateIndexesOperation.fromIndexSpecification( + this, + name, + indexSpec, + /*allowUnknownIndexOptions=*/ false, + options ?? {} + ) ); return indexes[0]; } diff --git a/src/gridfs/upload.ts b/src/gridfs/upload.ts index 13359cad4fb..e9464c89762 100644 --- a/src/gridfs/upload.ts +++ b/src/gridfs/upload.ts @@ -272,6 +272,10 @@ async function checkChunksIndex(stream: GridFSBucketWriteStream): Promise remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow( `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms` ); + // TODO(NODE-6893): this is a mixed bag of index options (background, unique) and command + // options (the write concern fields, timeoutMS), which the two parameter overload sorts via + // the index option allowlist. When validateOptions defaults to false, move the command + // options into the third parameter. await stream.chunks.createIndex(index, { ...stream.writeConcern, background: true, @@ -379,6 +383,8 @@ async function checkIndexes(stream: GridFSBucketWriteStream): Promise { `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms` ); + // TODO(NODE-6893): timeoutMS is a command option; move it into the third parameter when + // validateOptions defaults to false. await stream.files.createIndex(index, { background: false, timeoutMS: remainingTimeMS }); } diff --git a/src/index.ts b/src/index.ts index ef151a64424..5b093ba78e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -529,12 +529,15 @@ export type { } from './operations/find_and_modify'; export type { IndexInformationOptions } from './operations/indexes'; export type { + CreateIndexesCommandOptions, CreateIndexesOptions, + CreateIndexOptions, DropIndexesOptions, IndexDescription, IndexDescriptionCompact, IndexDescriptionInfo, IndexDirection, + IndexOptions, IndexSpecification, ListIndexesOptions } from './operations/indexes'; diff --git a/src/operations/create_collection.ts b/src/operations/create_collection.ts index 15733c91176..89731fd1765 100644 --- a/src/operations/create_collection.ts +++ b/src/operations/create_collection.ts @@ -207,6 +207,7 @@ export async function createCollections( db, name, { __safeContent__: 1 }, + /*allowUnknownIndexOptions=*/ false, { session: options.session } ); await executeOperation(db.client, createIndexOp, timeoutContext); diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index d6ae371f702..4106f7e6dd5 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -160,6 +160,207 @@ export interface CreateIndexesOptions extends Omit= 4.4. + */ + hidden?: boolean; + + /** + * Optionally specifies that this index is clustered. This is not a valid option to provide to + * 'createIndexes', but can appear in the options returned for an index via 'listIndexes'. To + * create a clustered index, create a new collection using the 'clusteredIndex' option. + * + * This options is only supported by servers \>= 6.0. + */ + clustered?: boolean; +} + +/** @public */ +export interface CreateIndexOptions { + /** + * 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. + * + * This option is only supported by servers \>= 4.4. Drivers MUST manually raise an error if this option + * is specified when creating an index on a pre 4.4 server. See the Q&A section for the rationale behind this. + * + * @remarks This option is sent only if the caller explicitly provides a value. The default is to not send a value. + * + * @sinceServerVersion 4.4 + */ + commitQuorum?: number | string; + + /** + * The maximum amount of time to allow the index build to take before returning an error. + * + * @remarks This option is sent only if the caller explicitly provides a value. The default is to not send a value. + */ + maxTimeMS?: number; + + /** + * This option MAY be implemented by drivers that need to grant access to underlying namespaces + * for time-series collections. Drivers SHOULD NOT implement this option unless asked to do so. + * + * This option is intended for internal use by MongoDB teams and should be discouraged for + * general application use. It may be changed or removed in any release without notice. + * + * Drivers SHOULD implement this option in a way that discourages customer use, such as: + * - Marking it as deprecated, experimental, or internal in their language's idioms + * - Excluding it from primary documentation + * + * @remarks This option MUST NOT be sent when connected to pre-8.2 servers. + * + * @sinceServerVersion 8.2 + */ + rawData?: boolean; + + /** + * Enables users to specify an arbitrary comment to help trace the operation through + * the database profiler, currentOp and logs. The default is to not send a value. + * + * @see https://www.mongodb.com/docs/manual/reference/command/createIndexes/ + * + * @sinceServerVersion 4.4 + */ + comment?: Document; +} + +/** @public */ +// Maps to `CreateIndexOptions` in +// http://github.com/mongodb/specifications/blob/6f64d0ee3ae49edbdb30eb995f3e29549e8cfa6a/source/index-management/index-management.md#standard-api +// This represents the options for the COMMAND, not the INDEX. +export interface CreateIndexesCommandOptions + extends Pick { + /** ...votingMembers etc. */ + commitQuorum?: number | string; } function isSingleIndexTuple(t: unknown): t is [string, IndexDirection] { @@ -197,19 +398,24 @@ function constructIndexDescriptionMap(indexSpec: IndexSpecification): Map { - const validProvidedOptions = Object.entries(description).filter(([optionName]) => - VALID_INDEX_OPTIONS.has(optionName) + const providedOptions = Object.entries(description).filter( + ([optionName]) => allowUnknownIndexOptions || VALID_INDEX_OPTIONS.has(optionName) ); return Object.fromEntries( // we support the `version` option, but the `createIndexes` command expects it to be the `v` - validProvidedOptions.map(([name, value]) => (name === 'version' ? ['v', value] : [name, value])) + providedOptions.map(([name, value]) => (name === 'version' ? ['v', value] : [name, value])) ); } @@ -251,11 +457,28 @@ export class CreateIndexesOperation extends CommandOperation { parent: OperationParent, collectionName: string, indexes: IndexDescription[], + allowUnknownIndexOptions: boolean, options?: CreateIndexesOptions + ); + + private constructor( + parent: OperationParent, + collectionName: string, + indexes: IndexDescription[], + allowUnknownIndexOptions: boolean, + commandOptions?: CreateIndexOptions + ); + + private constructor( + parent: OperationParent, + collectionName: string, + indexes: IndexDescription[], + allowUnknownIndexOptions: boolean, + commandOptions?: CreateIndexesOptions | CreateIndexOptions ) { - super(parent, options); + super(parent, commandOptions); - this.options = options ?? {}; + this.options = { ...commandOptions }; // collation is set on each index, it should not be defined at the root this.options.collation = undefined; this.collectionName = collectionName; @@ -264,7 +487,13 @@ export class CreateIndexesOperation extends CommandOperation { const key = userIndex.key instanceof Map ? userIndex.key : new Map(Object.entries(userIndex.key)); const name = userIndex.name ?? Array.from(key).flat().join('_'); - const validIndexOptions = resolveIndexDescription(userIndex); + + const validIndexOptions = resolveIndexDescription( + userIndex, + // TODO(seanrmilligan): Add NODE ticket to set to remove allowUnknownIndexOptions with + // a default behavior of true in a future 8.0.0 release + allowUnknownIndexOptions + ); return { ...validIndexOptions, name, @@ -278,20 +507,59 @@ export class CreateIndexesOperation extends CommandOperation { parent: OperationParent, collectionName: string, indexes: IndexDescription[], - options?: CreateIndexesOptions + indexOptions?: CreateIndexesOptions, + commandOptions?: CreateIndexesCommandOptions ): CreateIndexesOperation { - return new CreateIndexesOperation(parent, collectionName, indexes, options); + return new CreateIndexesOperation( + parent, + collectionName, + indexes, + /*allowUnknownIndexOptions=*/ false, + // TODO(seanrmilligan): remove the `?? indexOptions` fallback when the two parameter path is + // deprecated. Once `indexOptions` is index options only it must not reach the command root. + commandOptions ?? indexOptions + ); } static fromIndexSpecification( parent: OperationParent, collectionName: string, indexSpec: IndexSpecification, - options: CreateIndexesOptions = {} + allowUnknownIndexOptions: boolean, + options: CreateIndexesOptions + ): CreateIndexesOperation; + + static fromIndexSpecification( + parent: OperationParent, + collectionName: string, + indexSpec: IndexSpecification, + allowUnknownIndexOptions: boolean, + indexOptions?: IndexOptions, + commandOptions?: CreateIndexOptions + ): CreateIndexesOperation; + + static fromIndexSpecification( + parent: OperationParent, + collectionName: string, + indexSpec: IndexSpecification, + allowUnknownIndexOptions: boolean, + indexOptions?: CreateIndexesOptions | IndexOptions, + commandOptions?: CreateIndexOptions ): CreateIndexesOperation { const key = constructIndexDescriptionMap(indexSpec); - const description: IndexDescription = { ...options, key }; - return new CreateIndexesOperation(parent, collectionName, [description], options); + // If called with overload using `CreateIndexesOptions`, then indexOptions may contain combined index and command options + // These are filtered in CreateIndexesOperation using VALID_INDEX_OPTIONS. + // Otherwise, `IndexOptions` only contains index options (which is the ultimate goal anyway) + const description: IndexDescription = { ...indexOptions, key }; + return new CreateIndexesOperation( + parent, + collectionName, + [description], + allowUnknownIndexOptions, + // TODO(seanrmilligan): remove the `?? indexOptions` fallback when the two parameter path is + // deprecated. Once `indexOptions` is index options only it must not reach the command root. + commandOptions ?? indexOptions + ); } override get commandName() { diff --git a/src/utils.ts b/src/utils.ts index d9a96115fc8..c6585691a0b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -556,6 +556,72 @@ export function resolveOptions( return result; } +/** + * Merge inherited properties from parent into options, prioritizing values from options, + * then values from parent. + * + * Behaves identically to {@link resolveOptions}, but does not require `T` to inherit from + * `CommandOperationOptions`. The command-level fields this resolves (read/write concern, + * read preference, timeoutMS, BSON serialization) are added to the return type, and any + * field `T` declares itself takes precedence -- so an option type that narrows a field + * (ex. `CreateIndexOptions.maxTimeMS`, which is a `bigint`) keeps its own declaration. + * + * @param parent - An optional owning class of the operation being run. ex. Db/Collection/MongoClient. + * @param options - The options passed to the operation method. + * + * @internal + */ +export function resolveCommandOptions( + parent: OperationParent | undefined, + options?: T +): T & Omit { + // `T` is not constrained to `CommandOperationOptions`, but the command-level fields read + // below may still be present on it, so read them through a single widened view. + const commandOptions = options as CommandOperationOptions | undefined; + const resolved: CommandOperationOptions = {}; + + const timeoutMS = commandOptions?.timeoutMS ?? parent?.timeoutMS; + // Users cannot pass a readConcern/writeConcern to operations in a transaction + const session = commandOptions?.session; + + if (!session?.inTransaction()) { + const readConcern = ReadConcern.fromOptions(commandOptions) ?? parent?.readConcern; + if (readConcern) { + resolved.readConcern = readConcern; + } + + let writeConcern = WriteConcern.fromOptions(commandOptions) ?? parent?.writeConcern; + if (writeConcern) { + if (timeoutMS != null) { + writeConcern = WriteConcern.fromOptions({ + writeConcern: { + ...writeConcern, + wtimeout: undefined, + wtimeoutMS: undefined + } + }); + } + resolved.writeConcern = writeConcern; + } + } + + resolved.timeoutMS = timeoutMS; + + const readPreference = ReadPreference.fromOptions(commandOptions) ?? parent?.readPreference; + if (readPreference) { + resolved.readPreference = readPreference; + } + + const isConvenientTransaction = session?.explicit && session?.timeoutContext != null; + if (isConvenientTransaction && commandOptions?.timeoutMS != null) { + throw new MongoInvalidArgumentError( + 'An operation cannot be given a timeoutMS setting when inside a withTransaction call that has a timeoutMS setting' + ); + } + + return Object.assign({}, options, resolveBSONOptions(commandOptions, parent), resolved); +} + export function isSuperset(set: Set | any[], subset: Set | any[]): boolean { set = Array.isArray(set) ? new Set(set) : set; subset = Array.isArray(subset) ? new Set(subset) : subset; diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts new file mode 100644 index 00000000000..fcb055d3490 --- /dev/null +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -0,0 +1,405 @@ +import { expect } from 'chai'; + +import { + type Collection, + type CommandStartedEvent, + type Db, + type Document, + type MongoClient, + MongoServerError +} from '../../mongodb'; + +/** + * The two parameter form filters index options against an allowlist before sending them + * to the server, so options the server supports but the driver has not learned about yet are + * silently dropped. The three parameter form — + * `createIndex(keys, indexOptions, commandOptions)` — turns the filter off: supplying command + * options separately is what opts a caller into passthrough. + * + * `createIndex` and `createIndexes` are separated here because they build their index descriptions + * by different routes. `createIndexes` receives `IndexDescription` objects the user wrote directly, + * so index options and command options never mix. `createIndex` takes a single flat options bag + * that is *both*, and only becomes an index description via a merge in `fromIndexSpecification` — + * which is why turning the allowlist off is far more delicate on that path. + * + * The allowlist is what sorts that mixed bag today: index options survive into the description and + * everything else is dropped. Turning it off removes the only mechanism doing that sorting, so on + * the three parameter overload the separation becomes the caller's responsibility — index options + * in the second parameter, command options in the third. A command option left in the second + * parameter is forwarded to the server verbatim and the server rejects it, which is the intended + * (and loud) outcome rather than something the driver silently repairs. + */ + +/** + * The `key` of an index description is a Map by the time it reaches the wire, so that index key + * ordering is preserved. Convert it back to a plain object so descriptions can be compared with + * `deep.equal`. + */ +function indexesSentBy(event: CommandStartedEvent): Document[] { + return event.command.indexes.map(({ key, ...rest }: Document) => ({ + ...rest, + key: Object.fromEntries(key) + })); +} + +describe('createIndex option validation', function () { + let client: MongoClient; + let db: Db; + let collection: Collection; + let commands: CommandStartedEvent[]; + + /** The `indexes` array as it appeared on the wire for the last createIndexes command. */ + function sentIndexes(): Document[] { + expect(commands).to.have.lengthOf.at.least(1); + return indexesSentBy(commands[commands.length - 1]); + } + + /** The last createIndexes command itself, without its `indexes` array. */ + function sentCommand(): Document { + expect(commands).to.have.lengthOf.at.least(1); + const { indexes: _indexes, ...rest } = commands[commands.length - 1].command; + return rest; + } + + beforeEach(async function () { + client = this.configuration.newClient({}, { monitorCommands: true }); + commands = []; + client.on('commandStarted', ev => { + if (ev.commandName === 'createIndexes') commands.push(ev); + }); + db = client.db('node6893_create_index'); + collection = db.collection('c'); + }); + + afterEach(async function () { + await db.dropDatabase().catch(() => null); + await client.close(); + }); + + // The two parameter overload keeps the historical behaviour: one flat options bag, sorted by the + // allowlist. These tests guard that the filter is still in place and still doing the sorting. + describe('when only index options are given (two parameter form)', function () { + it('sends only the key and a generated name for a bare call', async function () { + await collection.createIndex({ a: 1 }); + + expect(sentIndexes()).to.deep.equal([{ key: { a: 1 }, name: 'a_1' }]); + }); + + it('sends index options and maps version to v', async function () { + await collection.createIndex( + { b: 1 }, + { unique: true, sparse: true, name: 'b_ix', version: 2 } + ); + + expect(sentIndexes()).to.deep.equal([ + { unique: true, sparse: true, name: 'b_ix', v: 2, key: { b: 1 } } + ]); + }); + + it('sends text index options', async function () { + await collection.createIndex( + { c: 'text' }, + { weights: { c: 5 }, default_language: 'english', textIndexVersion: 3 } + ); + + expect(sentIndexes()).to.deep.equal([ + { + weights: { c: 5 }, + default_language: 'english', + textIndexVersion: 3, + name: 'c_text', + key: { c: 'text' } + } + ]); + }); + + it('drops an unknown option from the options bag', async function () { + // @ts-expect-error CreateIndexesOptions is a closed interface + await collection.createIndex({ d: 1 }, { unique: true, notARealOption: true }); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'd_1', key: { d: 1 } }]); + }); + + it('keeps user-supplied command options out of the index description', async function () { + await collection.createIndex( + { e: 1 }, + { unique: true, comment: 'a comment', maxTimeMS: 1000, expireAfterSeconds: 100 } + ); + + expect(sentIndexes()).to.deep.equal([ + { unique: true, expireAfterSeconds: 100, name: 'e_1', key: { e: 1 } } + ]); + expect(sentCommand()).to.have.property('maxTimeMS', 1000); + }); + + it('keeps command options out of the index description for db.createIndex', async function () { + await db.createIndex('c', { f: 1 }, { unique: true, comment: 'a comment' }); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'f_1', key: { f: 1 } }]); + }); + }); + + describe('when command options are given (three parameter form)', function () { + it('does not send driver options the user never supplied', async function () { + await collection.createIndex({ a: 1 }, {}, {}); + + expect(sentIndexes()).to.deep.equal([{ key: { a: 1 }, name: 'a_1' }]); + }); + + it('sends an unknown option to the server', async function () { + const error = await collection + // @ts-expect-error CreateIndexesOptions is a closed interface + .createIndex({ d: 1 }, { notARealOption: true }, {}) + .catch(error => error); + + // the driver forwards the option; the server is what rejects it + expect(sentIndexes()[0]).to.have.property('notARealOption', true); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.message).to.match(/not valid for an index specification/); + }); + + it( + 'creates an index using a server option the driver does not know about', + { metadata: { requires: { mongodb: '>=5.3' } } }, + async function () { + // `prepareUnique` is supported by the server but is not in the driver's allowlist + await collection.createIndex( + { e: 1 }, + // @ts-expect-error CreateIndexesOptions is a closed interface + { prepareUnique: true }, + {} + ); + + expect(sentIndexes()[0]).to.have.property('prepareUnique', true); + const indexes = await collection.listIndexes().toArray(); + expect(indexes.find(index => index.name === 'e_1')).to.have.property('prepareUnique', true); + } + ); + + it('sends index options as normal', async function () { + await collection.createIndex({ f: 1 }, { unique: true, sparse: true, version: 2 }, {}); + + expect(sentIndexes()).to.deep.equal([ + { unique: true, sparse: true, v: 2, name: 'f_1', key: { f: 1 } } + ]); + }); + + describe('and command options are passed in the third parameter', function () { + it('keeps a comment out of the index description', async function () { + await collection.createIndex({ g: 1 }, { unique: true }, { comment: 'a comment' }); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'g_1', key: { g: 1 } }]); + // `comment` is accepted by CommandOperationOptions but createIndexes has never written it + // into its command document, so it does not reach the wire on either overload. This + // asserts only that the third parameter does not leak it into the index description. + expect(sentCommand()).to.not.have.property('comment'); + }); + + it('sends maxTimeMS on the command and not in the index description', async function () { + await collection.createIndex({ h: 1 }, { unique: true }, { maxTimeMS: 1000 }); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'h_1', key: { h: 1 } }]); + expect(sentCommand()).to.have.property('maxTimeMS', 1000); + }); + + it('sends a session on the command and not in the index description', async function () { + const session = client.startSession(); + try { + await collection.createIndex({ i: 1 }, { unique: true }, { session }); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'i_1', key: { i: 1 } }]); + expect(sentCommand()).to.have.property('lsid'); + } finally { + await session.endSession(); + } + }); + + it('sends a writeConcern on the command and not in the index description', async function () { + await collection.createIndex({ j: 1 }, { unique: true }, { writeConcern: { w: 1 } }); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'j_1', key: { j: 1 } }]); + expect(sentCommand()).to.have.property('writeConcern'); + }); + + it('sends index options and command options together', async function () { + await collection.createIndex( + { k: 1 }, + { unique: true, sparse: true, expireAfterSeconds: 100 }, + { maxTimeMS: 1000, writeConcern: { w: 1 } } + ); + + expect(sentIndexes()).to.deep.equal([ + { unique: true, sparse: true, expireAfterSeconds: 100, name: 'k_1', key: { k: 1 } } + ]); + expect(sentCommand()).to.have.property('maxTimeMS', 1000); + expect(sentCommand()).to.have.property('writeConcern'); + }); + }); + + describe('and a command option is left in the index options', function () { + it('forwards a comment to the server, which rejects it', async function () { + const error = await collection + .createIndex({ l: 1 }, { unique: true, comment: 'a comment' }, {}) + .catch(error => error); + + expect(sentIndexes()[0]).to.have.property('comment', 'a comment'); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.message).to.match(/not valid for an index specification/); + }); + + it('forwards maxTimeMS to the server, which rejects it', async function () { + const error = await collection + .createIndex({ m: 1 }, { unique: true, maxTimeMS: 1000 }, {}) + .catch(error => error); + + expect(sentIndexes()[0]).to.have.property('maxTimeMS', 1000); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.message).to.match(/not valid for an index specification/); + }); + }); + }); +}); + +describe('createIndexes option validation', function () { + let client: MongoClient; + let db: Db; + let collection: Collection; + let commands: CommandStartedEvent[]; + + function sentIndexes(): Document[] { + expect(commands).to.have.lengthOf.at.least(1); + return indexesSentBy(commands[commands.length - 1]); + } + + function sentCommand(): Document { + expect(commands).to.have.lengthOf.at.least(1); + const { indexes: _indexes, ...rest } = commands[commands.length - 1].command; + return rest; + } + + beforeEach(async function () { + client = this.configuration.newClient({}, { monitorCommands: true }); + commands = []; + client.on('commandStarted', ev => { + if (ev.commandName === 'createIndexes') commands.push(ev); + }); + db = client.db('node6893_create_indexes'); + collection = db.collection('c'); + }); + + afterEach(async function () { + await db.dropDatabase().catch(() => null); + await client.close(); + }); + + describe('when only index options are given (two parameter form)', function () { + it('sends only the key and a generated name for a bare description', async function () { + await collection.createIndexes([{ key: { a: 1 } }]); + + expect(sentIndexes()).to.deep.equal([{ key: { a: 1 }, name: 'a_1' }]); + }); + + it('sends index options and maps version to v', async function () { + await collection.createIndexes([ + { key: { b: 1 }, name: 'b_ix', unique: true, version: 2 }, + { key: { c: -1 }, hidden: true, expireAfterSeconds: 60 } + ]); + + expect(sentIndexes()).to.deep.equal([ + { name: 'b_ix', unique: true, v: 2, key: { b: 1 } }, + { hidden: true, expireAfterSeconds: 60, name: 'c_-1', key: { c: -1 } } + ]); + }); + + it('drops an unknown option from an index description', async function () { + await collection.createIndexes([ + // @ts-expect-error IndexDescription is a closed interface + { key: { d: 1 }, name: 'd_1', unique: true, notARealOption: true } + ]); + + expect(sentIndexes()).to.deep.equal([{ name: 'd_1', unique: true, key: { d: 1 } }]); + }); + + it('keeps user-supplied command options out of the index description', async function () { + await collection.createIndexes([{ key: { e: 1 } }], { writeConcern: { w: 1 } }); + + expect(sentIndexes()).to.deep.equal([{ key: { e: 1 }, name: 'e_1' }]); + expect(sentCommand()).to.have.property('writeConcern'); + }); + }); + + describe('when command options are given (three parameter form)', function () { + it('does not send driver options the user never supplied', async function () { + await collection.createIndexes([{ key: { a: 1 } }], {}, {}); + + expect(sentIndexes()).to.deep.equal([{ key: { a: 1 }, name: 'a_1' }]); + }); + + it('sends an unknown option to the server', async function () { + const error = await collection + .createIndexes( + // @ts-expect-error IndexDescription is a closed interface + [{ key: { d: 1 }, name: 'd_1', notARealOption: true }], + {}, + {} + ) + .catch(error => error); + + expect(sentIndexes()[0]).to.have.property('notARealOption', true); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.message).to.match(/not valid for an index specification/); + }); + + it( + 'creates an index using a server option the driver does not know about', + { metadata: { requires: { mongodb: '>=5.3' } } }, + async function () { + await collection.createIndexes( + // @ts-expect-error IndexDescription is a closed interface + [{ key: { e: 1 }, name: 'e_1', prepareUnique: true }], + {}, + {} + ); + + expect(sentIndexes()[0]).to.have.property('prepareUnique', true); + const indexes = await collection.listIndexes().toArray(); + expect(indexes.find(index => index.name === 'e_1')).to.have.property('prepareUnique', true); + } + ); + + it('sends index options as normal', async function () { + await collection.createIndexes( + [{ key: { f: 1 }, unique: true, sparse: true, version: 2 }], + {}, + {} + ); + + expect(sentIndexes()).to.deep.equal([ + { unique: true, sparse: true, v: 2, name: 'f_1', key: { f: 1 } } + ]); + }); + + it('keeps user-supplied command options out of the index description', async function () { + await collection.createIndexes( + [{ key: { g: 1 }, unique: true }], + { writeConcern: { w: 1 } }, + {} + ); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'g_1', key: { g: 1 } }]); + expect(sentCommand()).to.have.property('writeConcern'); + }); + + it('keeps a user-supplied session out of the index description', async function () { + const session = client.startSession(); + try { + await collection.createIndexes([{ key: { i: 1 }, unique: true }], { session }, {}); + + expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'i_1', key: { i: 1 } }]); + expect(sentCommand()).to.have.property('lsid'); + } finally { + await session.endSession(); + } + }); + }); +}); diff --git a/test/integration/index_management.test.ts b/test/integration/index_management.test.ts index abbb5e8899c..d86dab492ef 100644 --- a/test/integration/index_management.test.ts +++ b/test/integration/index_management.test.ts @@ -252,6 +252,47 @@ describe('Indexes', function () { }); } ); + + context('when an unknown index option is provided', function () { + context('and allowUnknownIndexOptions is unset (default)', function () { + it('silently drops the unknown option and creates the index', async () => { + const [name] = await collection.createIndexes([ + // @ts-expect-error: intentionally providing an unknown option + { key: { loc: '2dsphere' }, thisOptionDoesNotExist: true } + ]); + expect(started[0].command.indexes[0]).to.not.have.property('thisOptionDoesNotExist'); + const indexes = await collection.listIndexes().toArray(); + expect(indexes.map(i => i.name)).to.include(name); + }); + }); + + context('and allowUnknownIndexOptions is false', function () { + it('silently drops the unknown option and creates the index', async () => { + const [name] = await collection.createIndexes( + // @ts-expect-error: intentionally providing an unknown option + [{ key: { loc: '2dsphere' }, thisOptionDoesNotExist: true }], + { allowUnknownIndexOptions: false } + ); + expect(started[0].command.indexes[0]).to.not.have.property('thisOptionDoesNotExist'); + const indexes = await collection.listIndexes().toArray(); + expect(indexes.map(i => i.name)).to.include(name); + }); + }); + + context('and allowUnknownIndexOptions is true', function () { + it('passes the option through and surfaces the server error', async () => { + const error = await collection + .createIndexes( + // @ts-expect-error: intentionally providing an unknown option + [{ key: { loc: '2dsphere' }, thisOptionDoesNotExist: true }], + { allowUnknownIndexOptions: true } + ) + .catch(error => error); + expect(error).to.be.instanceOf(MongoServerError); + expect(started[0].command.indexes[0]).to.have.property('thisOptionDoesNotExist', true); + }); + }); + }); }); describe('Collection.indexExists()', function () { diff --git a/test/unit/collection.test.ts b/test/unit/collection.test.ts index 91cf4aa4fdb..e41e65f8da2 100644 --- a/test/unit/collection.test.ts +++ b/test/unit/collection.test.ts @@ -1,7 +1,7 @@ import { Long } from 'bson'; import { expect } from 'chai'; -import { isHello, MongoClient } from '../mongodb'; +import { type Collection, type Document, isHello, MongoClient } from '../mongodb'; import { cleanup, createServer, HELLO } from '../tools/mongodb-mock'; describe('Collection', function () { @@ -157,4 +157,119 @@ describe('Collection', function () { await testBulkWrite({ expected: undefined, actual: false, ordered: false }); }); }); + + context('#createIndex', () => { + /** + * Runs `createIndex` against the mock server and returns the `createIndexes` command + * that went over the wire, so both the command root and the index descriptions can be + * asserted on. + */ + async function captureCreateIndexes( + run: (collection: Collection) => Promise + ): Promise { + const client = new MongoClient(`mongodb://${server.uri()}/test`); + let command: Document | undefined; + + server.setMessageHandler(request => { + const doc = request.document; + if (doc.createIndexes) { + command = doc; + request.reply({ ok: 1, createdCollectionAutomatically: false }); + } else if (isHello(doc)) { + request.reply(Object.assign({}, HELLO)); + } else if (doc.endSessions) { + request.reply({ ok: 1 }); + } + }); + + await client.connect(); + try { + await run(client.db('test').collection('test_c')); + } finally { + await client.close(); + } + + expect(command, 'no createIndexes command was sent').to.exist; + return command as Document; + } + + context('command options', () => { + it('sends command options at the command root on the two parameter path', async () => { + const command = await captureCreateIndexes(collection => + collection.createIndex({ a: 1 }, { unique: true, commitQuorum: 2 }) + ); + + expect(command).to.have.property('commitQuorum', 2); + // index options belong on the index description, not the command root + expect(command).to.not.have.property('unique'); + }); + + it('sends command options at the command root on the three parameter path', async () => { + const command = await captureCreateIndexes(collection => + collection.createIndex({ a: 1 }, { unique: true }, { commitQuorum: 2 }) + ); + + expect(command).to.have.property('commitQuorum', 2); + expect(command).to.not.have.property('unique'); + }); + + it('does not write `comment` to the command (createIndexes never has)', async () => { + const command = await captureCreateIndexes(collection => + collection.createIndex({ a: 1 }, { unique: true }, { comment: 'a comment' }) + ); + + // `comment` is accepted by the option types but `buildCommandDocument` only writes + // `commitQuorum`. Documented here so a future change to that is a deliberate one. + expect(command).to.not.have.property('comment'); + }); + }); + + context('index options', () => { + it('keeps known index options on the index description', async () => { + const command = await captureCreateIndexes(collection => + collection.createIndex({ a: 1 }, { unique: true }) + ); + + expect(command.indexes).to.have.lengthOf(1); + expect(command.indexes[0]).to.have.property('unique', true); + expect(command.indexes[0]).to.have.property('name', 'a_1'); + }); + + it('drops unknown index options on the two parameter path', async () => { + const command = await captureCreateIndexes(collection => + // @ts-expect-error: unknown index options are filtered on the legacy path + collection.createIndex({ a: 1 }, { unique: true, notARealIndexOption: true }) + ); + + expect(command.indexes[0]).to.have.property('unique', true); + expect(command.indexes[0]).to.not.have.property('notARealIndexOption'); + }); + + it('keeps command level fields out of the index description', async () => { + const command = await captureCreateIndexes(collection => + collection.createIndex({ a: 1 }, { unique: true, comment: 'a comment', maxTimeMS: 1000 }) + ); + + expect(command.indexes[0]).to.not.have.property('comment'); + expect(command.indexes[0]).to.not.have.property('maxTimeMS'); + expect(command.indexes[0]).to.not.have.property('readConcern'); + expect(command.indexes[0]).to.not.have.property('readPreference'); + expect(command.indexes[0]).to.not.have.property('promoteLongs'); + }); + + it('passes unknown index options through on the three parameter path', async () => { + const command = await captureCreateIndexes(collection => + collection.createIndex( + { a: 1 }, + // @ts-expect-error: unknown index options are passed through to the server + { unique: true, finestIndexedLevel: 15 }, + { commitQuorum: 2 } + ) + ); + + expect(command.indexes[0]).to.have.property('unique', true); + expect(command.indexes[0]).to.have.property('finestIndexedLevel', 15); + }); + }); + }); }); diff --git a/test/unit/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index 585da82356a..c6d7d936ea5 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -104,6 +104,15 @@ describe('class CreateIndexesOperation', () => { { s: { namespace: ns('a.b') } }, 'b', input, + /*allowUnknownIndexOptions=*/ false, + options + ); + + const makeIndexesOperation = (indexes, options: CreateIndexesOptions = {}) => + CreateIndexesOperation.fromIndexDescriptionArray( + { s: { namespace: ns('a.b') } }, + 'b', + indexes, options ); @@ -152,4 +161,53 @@ describe('class CreateIndexesOperation', () => { expect(indexOutput.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); }); }); + + describe('allowUnknownIndexOptions (createIndexes passthrough)', () => { + const indexDescription = () => ({ + key: { a: 1 }, + // @ts-expect-error: Testing that unknown options are passed through when enabled + finestIndexedLevel: 15, + randomOptionThatWillNeverBeAdded: true + }); + + it('drops unknown options when the flag is unset (default behavior)', () => { + const output = makeIndexesOperation([indexDescription()]); + expect(output.indexes[0]).to.not.have.property('finestIndexedLevel'); + expect(output.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); + }); + + it('drops unknown options when the flag is set to false', () => { + const output = makeIndexesOperation([indexDescription()], { + allowUnknownIndexOptions: false + }); + expect(output.indexes[0]).to.not.have.property('finestIndexedLevel'); + expect(output.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); + }); + + it('retains unknown options when the flag is set to true', () => { + const output = makeIndexesOperation([indexDescription()], { + allowUnknownIndexOptions: true + }); + expect(output.indexes[0]).to.have.property('finestIndexedLevel', 15); + expect(output.indexes[0]).to.have.property('randomOptionThatWillNeverBeAdded', true); + }); + + it('still maps `version` to `v` when the flag is set to true', () => { + const output = makeIndexesOperation([{ key: { a: 1 }, version: 1 }], { + allowUnknownIndexOptions: true + }); + expect(output.indexes[0]).to.have.property('v', 1); + expect(output.indexes[0]).to.not.have.property('version'); + }); + + it('does not enable passthrough for createIndex even when the flag is set to true', () => { + const output = makeIndexOperation( + { a: 1 }, + // @ts-expect-error: Testing bad options get filtered + { allowUnknownIndexOptions: true, randomOptionThatWillNeverBeAdded: true } + ); + expect(output.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); + expect(output.indexes[0]).to.not.have.property('allowUnknownIndexOptions'); + }); + }); }); From 991884fec0353a41070e03da1a791e00d80b23ee Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Fri, 11 Sep 2026 06:53:39 -0700 Subject: [PATCH 02/23] Fix: createIndexes / fromIndexDescriptionArray interaction --- src/collection.ts | 19 +- src/index.ts | 1 - src/operations/indexes.ts | 176 +++++++++++++++--- .../crud/abstract_operation.test.ts | 7 +- .../create_indexes_option_validation.test.ts | 17 +- test/integration/index_management.test.ts | 6 +- test/unit/operations/indexes.test.ts | 12 +- 7 files changed, 186 insertions(+), 52 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index e6cea762550..3244e524fd4 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -54,7 +54,6 @@ import { type FindOneAndUpdateOptions } from './operations/find_and_modify'; import { - type CreateIndexesCommandOptions, CreateIndexesOperation, type CreateIndexesOptions, type CreateIndexOptions, @@ -716,11 +715,17 @@ export class Collection { * } * ]); * ``` + * + * @param indexSpecs - An array of index specifications to be created + * @param commandOptions - Optional settings for the `createIndexes` command + * @param allowUnknownIndexOptions - When `true`, index options the driver does not recognise are + * sent to the server instead of being dropped. Defaults to `false`; this will become the only + * behaviour in a future major release. */ async createIndexes( indexSpecs: IndexDescription[], - options?: CreateIndexesOptions, - commandOptions?: CreateIndexesCommandOptions + commandOptions?: CreateIndexesOptions, + allowUnknownIndexOptions = false ): Promise { return await executeOperation( this.client, @@ -728,8 +733,12 @@ export class Collection { this, this.collectionName, indexSpecs, - resolveOptions(this, { ...options, maxTimeMS: undefined }), - commandOptions + // TODO(seanrmilligan): default this to true and remove the parameter in a future major + // release. Index options live on each index description, so nothing on this path + // contaminates them -- but flipping it turns today's silently dropped unknown option into + // a server error. + allowUnknownIndexOptions, + resolveOptions(this, { ...commandOptions, maxTimeMS: undefined }) ) ); } diff --git a/src/index.ts b/src/index.ts index 5b093ba78e6..dd20169bb97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -529,7 +529,6 @@ export type { } from './operations/find_and_modify'; export type { IndexInformationOptions } from './operations/indexes'; export type { - CreateIndexesCommandOptions, CreateIndexesOptions, CreateIndexOptions, DropIndexesOptions, diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 4106f7e6dd5..36df4ee15bb 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -123,44 +123,170 @@ export interface IndexDescription /** @public */ export interface CreateIndexesOptions extends Omit { - /** Creates the index in the background, yielding whenever possible. */ + /** + * Creates the index in the background, yielding whenever possible. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ background?: boolean; - /** Creates an unique index. */ + /** + * Creates an unique index. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ unique?: boolean; - /** Override the autogenerated index name (useful if the resulting name is larger than 128 bytes) */ + /** + * Override the autogenerated index name (useful if the resulting name is larger than 128 bytes) + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ name?: string; - /** Creates a partial index based on the given filter object (MongoDB 3.2 or higher) */ + /** + * Creates a partial index based on the given filter object (MongoDB 3.2 or higher) + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ partialFilterExpression?: Document; - /** Creates a sparse index. */ + /** + * Creates a sparse index. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ sparse?: boolean; - /** Allows you to expire data on indexes applied to a data (MongoDB 2.2 or higher) */ + /** + * Allows you to expire data on indexes applied to a data (MongoDB 2.2 or higher) + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ expireAfterSeconds?: number; - /** Allows users to configure the storage engine on a per-index basis when creating an index. (MongoDB 3.0 or higher) */ + /** + * Allows users to configure the storage engine on a per-index basis when creating an index. (MongoDB 3.0 or higher) + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ storageEngine?: Document; - /** 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. */ + /** + * 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. */ + /** + * Specifies the index version number, either 0 or 1. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ version?: number; + // text indexes + /** + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ weights?: Document; + /** + * Renamed to `defaultLanguage` on {@link IndexOptions}. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ default_language?: string; + /** + * Renamed to `languageOverride` on {@link IndexOptions}. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ language_override?: string; + /** + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ textIndexVersion?: number; + // 2d-sphere indexes + /** + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ '2dsphereIndexVersion'?: number; + // 2d indexes + /** + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ bits?: number; - /** For geospatial indexes set the lower bound for the co-ordinates. */ + /** + * For geospatial indexes set the lower bound for the co-ordinates. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ min?: number; - /** For geospatial indexes set the high bound for the co-ordinates. */ + /** + * For geospatial indexes set the high bound for the co-ordinates. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ max?: number; + // geoHaystack Indexes + /** + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ bucketSize?: number; + // wildcard indexes + /** + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ wildcardProjection?: Document; - /** Specifies that the index should exist on the target collection but should not be used by the query planner when executing operations. */ + /** + * Specifies that the index should exist on the target collection but should not be used by the query planner when executing operations. + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ hidden?: boolean; - /** Collation */ + /** + * Collation + * + * @deprecated Index options will be removed from this type in a future major release. Pass + * them in the `indexOptions` parameter of the three parameter `createIndex` overload, which is + * typed {@link IndexOptions}. + */ collation?: CollationOptions; } @@ -353,16 +479,6 @@ export interface CreateIndexOptions { comment?: Document; } -/** @public */ -// Maps to `CreateIndexOptions` in -// http://github.com/mongodb/specifications/blob/6f64d0ee3ae49edbdb30eb995f3e29549e8cfa6a/source/index-management/index-management.md#standard-api -// This represents the options for the COMMAND, not the INDEX. -export interface CreateIndexesCommandOptions - extends Pick { - /** ...votingMembers etc. */ - commitQuorum?: number | string; -} - function isSingleIndexTuple(t: unknown): t is [string, IndexDirection] { return Array.isArray(t) && t.length === 2 && isIndexDirection(t[1]); } @@ -503,21 +619,23 @@ export class CreateIndexesOperation extends CommandOperation { this.ns = parent.s.namespace; } + /** + * Index options are carried by the individual {@link IndexDescription} entries of `indexes`, + * so the only options bag this factory takes is the command one. + */ static fromIndexDescriptionArray( parent: OperationParent, collectionName: string, indexes: IndexDescription[], - indexOptions?: CreateIndexesOptions, - commandOptions?: CreateIndexesCommandOptions + allowUnknownIndexOptions: boolean, + commandOptions?: CreateIndexesOptions ): CreateIndexesOperation { return new CreateIndexesOperation( parent, collectionName, indexes, - /*allowUnknownIndexOptions=*/ false, - // TODO(seanrmilligan): remove the `?? indexOptions` fallback when the two parameter path is - // deprecated. Once `indexOptions` is index options only it must not reach the command root. - commandOptions ?? indexOptions + allowUnknownIndexOptions, + commandOptions ); } diff --git a/test/integration/crud/abstract_operation.test.ts b/test/integration/crud/abstract_operation.test.ts index 777e2a38fce..dc6a2fd9079 100644 --- a/test/integration/crud/abstract_operation.test.ts +++ b/test/integration/crud/abstract_operation.test.ts @@ -155,7 +155,12 @@ describe('abstract operation', function () { }, { subclassCreator: () => - CreateIndexesOperation.fromIndexDescriptionArray(db, 'bar', [{ key: { a: 1 } }]), + CreateIndexesOperation.fromIndexDescriptionArray( + db, + 'bar', + [{ key: { a: 1 } }], + /*allowUnknownIndexOptions=*/ false + ), subclassType: CreateIndexesOperation, correctCommandName: 'createIndexes' }, diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts index fcb055d3490..8c8c35d61a5 100644 --- a/test/integration/index-management/create_indexes_option_validation.test.ts +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -328,9 +328,9 @@ describe('createIndexes option validation', function () { }); }); - describe('when command options are given (three parameter form)', function () { + describe('when command options are given', function () { it('does not send driver options the user never supplied', async function () { - await collection.createIndexes([{ key: { a: 1 } }], {}, {}); + await collection.createIndexes([{ key: { a: 1 } }], {}); expect(sentIndexes()).to.deep.equal([{ key: { a: 1 }, name: 'a_1' }]); }); @@ -341,7 +341,7 @@ describe('createIndexes option validation', function () { // @ts-expect-error IndexDescription is a closed interface [{ key: { d: 1 }, name: 'd_1', notARealOption: true }], {}, - {} + /*allowUnknownIndexOptions=*/ true ) .catch(error => error); @@ -358,7 +358,7 @@ describe('createIndexes option validation', function () { // @ts-expect-error IndexDescription is a closed interface [{ key: { e: 1 }, name: 'e_1', prepareUnique: true }], {}, - {} + /*allowUnknownIndexOptions=*/ true ); expect(sentIndexes()[0]).to.have.property('prepareUnique', true); @@ -370,7 +370,6 @@ describe('createIndexes option validation', function () { it('sends index options as normal', async function () { await collection.createIndexes( [{ key: { f: 1 }, unique: true, sparse: true, version: 2 }], - {}, {} ); @@ -380,11 +379,7 @@ describe('createIndexes option validation', function () { }); it('keeps user-supplied command options out of the index description', async function () { - await collection.createIndexes( - [{ key: { g: 1 }, unique: true }], - { writeConcern: { w: 1 } }, - {} - ); + await collection.createIndexes([{ key: { g: 1 }, unique: true }], { writeConcern: { w: 1 } }); expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'g_1', key: { g: 1 } }]); expect(sentCommand()).to.have.property('writeConcern'); @@ -393,7 +388,7 @@ describe('createIndexes option validation', function () { it('keeps a user-supplied session out of the index description', async function () { const session = client.startSession(); try { - await collection.createIndexes([{ key: { i: 1 }, unique: true }], { session }, {}); + await collection.createIndexes([{ key: { i: 1 }, unique: true }], { session }); expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'i_1', key: { i: 1 } }]); expect(sentCommand()).to.have.property('lsid'); diff --git a/test/integration/index_management.test.ts b/test/integration/index_management.test.ts index d86dab492ef..f95ced6f706 100644 --- a/test/integration/index_management.test.ts +++ b/test/integration/index_management.test.ts @@ -271,7 +271,8 @@ describe('Indexes', function () { const [name] = await collection.createIndexes( // @ts-expect-error: intentionally providing an unknown option [{ key: { loc: '2dsphere' }, thisOptionDoesNotExist: true }], - { allowUnknownIndexOptions: false } + {}, + /*allowUnknownIndexOptions=*/ false ); expect(started[0].command.indexes[0]).to.not.have.property('thisOptionDoesNotExist'); const indexes = await collection.listIndexes().toArray(); @@ -285,7 +286,8 @@ describe('Indexes', function () { .createIndexes( // @ts-expect-error: intentionally providing an unknown option [{ key: { loc: '2dsphere' }, thisOptionDoesNotExist: true }], - { allowUnknownIndexOptions: true } + {}, + /*allowUnknownIndexOptions=*/ true ) .catch(error => error); expect(error).to.be.instanceOf(MongoServerError); diff --git a/test/unit/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index c6d7d936ea5..0978a511f98 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -108,13 +108,19 @@ describe('class CreateIndexesOperation', () => { options ); - const makeIndexesOperation = (indexes, options: CreateIndexesOptions = {}) => - CreateIndexesOperation.fromIndexDescriptionArray( + const makeIndexesOperation = ( + indexes, + options: CreateIndexesOptions & { allowUnknownIndexOptions?: boolean } = {} + ) => { + const { allowUnknownIndexOptions = false, ...commandOptions } = options; + return CreateIndexesOperation.fromIndexDescriptionArray( { s: { namespace: ns('a.b') } }, 'b', indexes, - options + allowUnknownIndexOptions, + commandOptions ); + }; describe('#constructor()', () => { for (const { description, input, mapData, name } of testCases) { From ab02deb7bef896c52fc6c2e65d61067603830e9b Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Mon, 14 Sep 2026 15:04:01 -0700 Subject: [PATCH 03/23] drop ticket number from test db string name --- .../index-management/create_indexes_option_validation.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts index 8c8c35d61a5..61ad83d3e63 100644 --- a/test/integration/index-management/create_indexes_option_validation.test.ts +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -67,7 +67,7 @@ describe('createIndex option validation', function () { client.on('commandStarted', ev => { if (ev.commandName === 'createIndexes') commands.push(ev); }); - db = client.db('node6893_create_index'); + db = client.db('create_index_option_validation'); collection = db.collection('c'); }); @@ -283,7 +283,7 @@ describe('createIndexes option validation', function () { client.on('commandStarted', ev => { if (ev.commandName === 'createIndexes') commands.push(ev); }); - db = client.db('node6893_create_indexes'); + db = client.db('create_indexes_option_validation'); collection = db.collection('c'); }); From e1ad0226ab4d6943ecad3c5f472e93190d52bb5e Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Mon, 14 Sep 2026 16:07:58 -0700 Subject: [PATCH 04/23] Inherit CSOT and other fields from CommandOperationOptions into CreateIndexOptions --- src/collection.ts | 3 +- src/gridfs/upload.ts | 19 ++++------- src/operations/indexes.ts | 36 +-------------------- src/utils.ts | 66 --------------------------------------- 4 files changed, 8 insertions(+), 116 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 3244e524fd4..99af4e9e127 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -96,7 +96,6 @@ import { DEFAULT_PK_FACTORY, MongoDBCollectionNamespace, normalizeHintField, - resolveCommandOptions, resolveOptions } from './utils'; import { WriteConcern, type WriteConcernOptions } from './write_concern'; @@ -678,7 +677,7 @@ export class Collection { indexSpec, /*allowUnknownIndexOptions=*/ true, indexOptions, - resolveCommandOptions(this, commandOptions) + resolveOptions(this, commandOptions) ); const indexes = await executeOperation(this.client, operation); diff --git a/src/gridfs/upload.ts b/src/gridfs/upload.ts index e9464c89762..c1abac34855 100644 --- a/src/gridfs/upload.ts +++ b/src/gridfs/upload.ts @@ -272,16 +272,11 @@ async function checkChunksIndex(stream: GridFSBucketWriteStream): Promise remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow( `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms` ); - // TODO(NODE-6893): this is a mixed bag of index options (background, unique) and command - // options (the write concern fields, timeoutMS), which the two parameter overload sorts via - // the index option allowlist. When validateOptions defaults to false, move the command - // options into the third parameter. - await stream.chunks.createIndex(index, { - ...stream.writeConcern, - background: true, - unique: true, - timeoutMS: remainingTimeMS - }); + await stream.chunks.createIndex( + index, + { background: true, unique: true }, + { timeoutMS: remainingTimeMS } + ); } } @@ -383,9 +378,7 @@ async function checkIndexes(stream: GridFSBucketWriteStream): Promise { `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms` ); - // TODO(NODE-6893): timeoutMS is a command option; move it into the third parameter when - // validateOptions defaults to false. - await stream.files.createIndex(index, { background: false, timeoutMS: remainingTimeMS }); + await stream.files.createIndex(index, { background: false }, { timeoutMS: remainingTimeMS }); } await checkChunksIndex(stream); diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 36df4ee15bb..12a0478dafa 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -427,7 +427,7 @@ export interface IndexOptions { } /** @public */ -export interface CreateIndexOptions { +export interface CreateIndexOptions extends Omit { /** * 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. @@ -443,40 +443,6 @@ export interface CreateIndexOptions { * @sinceServerVersion 4.4 */ commitQuorum?: number | string; - - /** - * The maximum amount of time to allow the index build to take before returning an error. - * - * @remarks This option is sent only if the caller explicitly provides a value. The default is to not send a value. - */ - maxTimeMS?: number; - - /** - * This option MAY be implemented by drivers that need to grant access to underlying namespaces - * for time-series collections. Drivers SHOULD NOT implement this option unless asked to do so. - * - * This option is intended for internal use by MongoDB teams and should be discouraged for - * general application use. It may be changed or removed in any release without notice. - * - * Drivers SHOULD implement this option in a way that discourages customer use, such as: - * - Marking it as deprecated, experimental, or internal in their language's idioms - * - Excluding it from primary documentation - * - * @remarks This option MUST NOT be sent when connected to pre-8.2 servers. - * - * @sinceServerVersion 8.2 - */ - rawData?: boolean; - - /** - * Enables users to specify an arbitrary comment to help trace the operation through - * the database profiler, currentOp and logs. The default is to not send a value. - * - * @see https://www.mongodb.com/docs/manual/reference/command/createIndexes/ - * - * @sinceServerVersion 4.4 - */ - comment?: Document; } function isSingleIndexTuple(t: unknown): t is [string, IndexDirection] { diff --git a/src/utils.ts b/src/utils.ts index c6585691a0b..d9a96115fc8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -556,72 +556,6 @@ export function resolveOptions( return result; } -/** - * Merge inherited properties from parent into options, prioritizing values from options, - * then values from parent. - * - * Behaves identically to {@link resolveOptions}, but does not require `T` to inherit from - * `CommandOperationOptions`. The command-level fields this resolves (read/write concern, - * read preference, timeoutMS, BSON serialization) are added to the return type, and any - * field `T` declares itself takes precedence -- so an option type that narrows a field - * (ex. `CreateIndexOptions.maxTimeMS`, which is a `bigint`) keeps its own declaration. - * - * @param parent - An optional owning class of the operation being run. ex. Db/Collection/MongoClient. - * @param options - The options passed to the operation method. - * - * @internal - */ -export function resolveCommandOptions( - parent: OperationParent | undefined, - options?: T -): T & Omit { - // `T` is not constrained to `CommandOperationOptions`, but the command-level fields read - // below may still be present on it, so read them through a single widened view. - const commandOptions = options as CommandOperationOptions | undefined; - const resolved: CommandOperationOptions = {}; - - const timeoutMS = commandOptions?.timeoutMS ?? parent?.timeoutMS; - // Users cannot pass a readConcern/writeConcern to operations in a transaction - const session = commandOptions?.session; - - if (!session?.inTransaction()) { - const readConcern = ReadConcern.fromOptions(commandOptions) ?? parent?.readConcern; - if (readConcern) { - resolved.readConcern = readConcern; - } - - let writeConcern = WriteConcern.fromOptions(commandOptions) ?? parent?.writeConcern; - if (writeConcern) { - if (timeoutMS != null) { - writeConcern = WriteConcern.fromOptions({ - writeConcern: { - ...writeConcern, - wtimeout: undefined, - wtimeoutMS: undefined - } - }); - } - resolved.writeConcern = writeConcern; - } - } - - resolved.timeoutMS = timeoutMS; - - const readPreference = ReadPreference.fromOptions(commandOptions) ?? parent?.readPreference; - if (readPreference) { - resolved.readPreference = readPreference; - } - - const isConvenientTransaction = session?.explicit && session?.timeoutContext != null; - if (isConvenientTransaction && commandOptions?.timeoutMS != null) { - throw new MongoInvalidArgumentError( - 'An operation cannot be given a timeoutMS setting when inside a withTransaction call that has a timeoutMS setting' - ); - } - - return Object.assign({}, options, resolveBSONOptions(commandOptions, parent), resolved); -} - export function isSuperset(set: Set | any[], subset: Set | any[]): boolean { set = Array.isArray(set) ? new Set(set) : set; subset = Array.isArray(subset) ? new Set(subset) : subset; From 4a4310c6ec2f9f06ee87972b206c6e0dcdafb181 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Mon, 14 Sep 2026 17:12:08 -0700 Subject: [PATCH 05/23] support public api spec / wire property naming differences with a map --- src/operations/indexes.ts | 15 ++++++++++++--- test/unit/operations/indexes.test.ts | 11 +++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 12a0478dafa..7065700b04e 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -479,9 +479,19 @@ function constructIndexDescriptionMap(indexSpec: IndexSpecification): Map (name === 'version' ? ['v', value] : [name, value])) + providedOptions.map(([name, value]) => [INDEX_OPTION_RENAMES.get(name) ?? name, value]) ); } diff --git a/test/unit/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index 0978a511f98..466f6f32f4e 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -198,6 +198,17 @@ describe('class CreateIndexesOperation', () => { expect(output.indexes[0]).to.have.property('randomOptionThatWillNeverBeAdded', true); }); + it('renames the text index language options the server expects in snake_case', () => { + const output = makeIndexesOperation( + [{ key: { a: 'text' }, defaultLanguage: 'spanish', languageOverride: 'lang' }], + { allowUnknownIndexOptions: true } + ); + expect(output.indexes[0]).to.have.property('default_language', 'spanish'); + expect(output.indexes[0]).to.have.property('language_override', 'lang'); + expect(output.indexes[0]).to.not.have.property('defaultLanguage'); + expect(output.indexes[0]).to.not.have.property('languageOverride'); + }); + it('still maps `version` to `v` when the flag is set to true', () => { const output = makeIndexesOperation([{ key: { a: 1 }, version: 1 }], { allowUnknownIndexOptions: true From 214cd1a1d5ee254127b1f5d39e6b74f19ce05852 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Tue, 15 Sep 2026 06:47:17 -0700 Subject: [PATCH 06/23] prepareUnique requires 6.0+ --- .../create_indexes_option_validation.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts index 61ad83d3e63..33d7a1e0135 100644 --- a/test/integration/index-management/create_indexes_option_validation.test.ts +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -160,12 +160,14 @@ describe('createIndex option validation', function () { it( 'creates an index using a server option the driver does not know about', - { metadata: { requires: { mongodb: '>=5.3' } } }, + // `prepareUnique` was introduced in server 6.0; on older servers it is not a valid index + // option and the server rejects the command, so this test cannot run there. + { metadata: { requires: { mongodb: '>=6.0' } } }, async function () { // `prepareUnique` is supported by the server but is not in the driver's allowlist await collection.createIndex( { e: 1 }, - // @ts-expect-error CreateIndexesOptions is a closed interface + // @ts-expect-error IndexOptions is a closed interface { prepareUnique: true }, {} ); @@ -352,7 +354,9 @@ describe('createIndexes option validation', function () { it( 'creates an index using a server option the driver does not know about', - { metadata: { requires: { mongodb: '>=5.3' } } }, + // `prepareUnique` was introduced in server 6.0; on older servers it is not a valid index + // option and the server rejects the command, so this test cannot run there. + { metadata: { requires: { mongodb: '>=6.0' } } }, async function () { await collection.createIndexes( // @ts-expect-error IndexDescription is a closed interface From 4c1630b644cb168c05f3564d280d54da88c06748 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Tue, 15 Sep 2026 07:30:47 -0700 Subject: [PATCH 07/23] remove metadata --- .../index-management/create_indexes_option_validation.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts index 33d7a1e0135..6f4b8a50151 100644 --- a/test/integration/index-management/create_indexes_option_validation.test.ts +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -162,7 +162,7 @@ describe('createIndex option validation', function () { 'creates an index using a server option the driver does not know about', // `prepareUnique` was introduced in server 6.0; on older servers it is not a valid index // option and the server rejects the command, so this test cannot run there. - { metadata: { requires: { mongodb: '>=6.0' } } }, + { requires: { mongodb: '>=6.0' } }, async function () { // `prepareUnique` is supported by the server but is not in the driver's allowlist await collection.createIndex( @@ -356,7 +356,7 @@ describe('createIndexes option validation', function () { 'creates an index using a server option the driver does not know about', // `prepareUnique` was introduced in server 6.0; on older servers it is not a valid index // option and the server rejects the command, so this test cannot run there. - { metadata: { requires: { mongodb: '>=6.0' } } }, + { requires: { mongodb: '>=6.0' } }, async function () { await collection.createIndexes( // @ts-expect-error IndexDescription is a closed interface From 4d84948f00d9c2ce39c6367526088e4073c6b76f Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Thu, 17 Sep 2026 07:42:47 -0700 Subject: [PATCH 08/23] Remove 'key' in resolveIndexDescription, even when allowUnknownIndexOptions is true --- src/operations/indexes.ts | 10 ++++++++-- test/unit/operations/indexes.test.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 7065700b04e..9463a72149e 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -93,7 +93,11 @@ export interface IndexInformationOptions extends ListIndexesOptions { full?: boolean; } -/** @public */ +/** + * @public + * + * Loosely aligns with `IndexModel` from the spec. + * */ export interface IndexDescription extends Pick< CreateIndexesOptions, @@ -502,7 +506,9 @@ function resolveIndexDescription( allowUnknownIndexOptions: boolean ): Omit { const providedOptions = Object.entries(description).filter( - ([optionName]) => allowUnknownIndexOptions || VALID_INDEX_OPTIONS.has(optionName) + ([optionName]) => + // Ensure `key` is removed, even when `allowUnknownIndexOptions` is true + optionName !== 'key' && (allowUnknownIndexOptions || VALID_INDEX_OPTIONS.has(optionName)) ); return Object.fromEntries( diff --git a/test/unit/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index 466f6f32f4e..b05ac4aa830 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -198,6 +198,22 @@ describe('class CreateIndexesOperation', () => { expect(output.indexes[0]).to.have.property('randomOptionThatWillNeverBeAdded', true); }); + it('rebuilds `key` as a Map even when unknown options are passed through', () => { + const output = makeIndexesOperation([{ key: { a: 1, b: -1 } }], { + allowUnknownIndexOptions: true + }); + + // `key` must not survive the option filter: the operation rebuilds it as a Map so that + // index key ordering is preserved, and re-adds it after the filtered options. + expect(output.indexes[0].key).to.be.instanceOf(Map); + expect(output.indexes[0].key).to.deep.equal( + new Map([ + ['a', 1], + ['b', -1] + ]) + ); + }); + it('renames the text index language options the server expects in snake_case', () => { const output = makeIndexesOperation( [{ key: { a: 'text' }, defaultLanguage: 'spanish', languageOverride: 'lang' }], From 44b38b13e36ffc75a8d19b6e5781671403965750 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Wed, 23 Sep 2026 12:30:59 -0700 Subject: [PATCH 09/23] Update TODO with ticket number --- src/collection.ts | 1 + src/operations/indexes.ts | 28 +++++----------------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 99af4e9e127..7397776d706 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -724,6 +724,7 @@ export class Collection { async createIndexes( indexSpecs: IndexDescription[], commandOptions?: CreateIndexesOptions, + // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions = false ): Promise { return await executeOperation( diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 9463a72149e..c3099addbdd 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -503,6 +503,7 @@ const INDEX_OPTION_RENAMES = new Map([ */ function resolveIndexDescription( description: IndexDescription, + // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions: boolean ): Omit { const providedOptions = Object.entries(description).filter( @@ -554,24 +555,9 @@ export class CreateIndexesOperation extends CommandOperation { parent: OperationParent, collectionName: string, indexes: IndexDescription[], + // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions: boolean, - options?: CreateIndexesOptions - ); - - private constructor( - parent: OperationParent, - collectionName: string, - indexes: IndexDescription[], - allowUnknownIndexOptions: boolean, - commandOptions?: CreateIndexOptions - ); - - private constructor( - parent: OperationParent, - collectionName: string, - indexes: IndexDescription[], - allowUnknownIndexOptions: boolean, - commandOptions?: CreateIndexesOptions | CreateIndexOptions + commandOptions: CreateIndexesOptions | CreateIndexOptions | undefined ) { super(parent, commandOptions); @@ -585,12 +571,7 @@ export class CreateIndexesOperation extends CommandOperation { userIndex.key instanceof Map ? userIndex.key : new Map(Object.entries(userIndex.key)); const name = userIndex.name ?? Array.from(key).flat().join('_'); - const validIndexOptions = resolveIndexDescription( - userIndex, - // TODO(seanrmilligan): Add NODE ticket to set to remove allowUnknownIndexOptions with - // a default behavior of true in a future 8.0.0 release - allowUnknownIndexOptions - ); + const validIndexOptions = resolveIndexDescription(userIndex, allowUnknownIndexOptions); return { ...validIndexOptions, name, @@ -608,6 +589,7 @@ export class CreateIndexesOperation extends CommandOperation { parent: OperationParent, collectionName: string, indexes: IndexDescription[], + // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions: boolean, commandOptions?: CreateIndexesOptions ): CreateIndexesOperation { From 44c62ee71af00962343d787a354ac74b50b7b383 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Wed, 23 Sep 2026 13:22:51 -0700 Subject: [PATCH 10/23] remove optionality from CreateIndexesOperation options/commandOptions parameter --- src/operations/indexes.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index c3099addbdd..e7abcd74c5a 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -551,13 +551,29 @@ export class CreateIndexesOperation extends CommandOperation { collectionName: string; indexes: ReadonlyArray; + private constructor( + parent: OperationParent, + collectionName: string, + indexes: IndexDescription[], + allowUnknownIndexOptions: boolean, + options?: CreateIndexesOptions + ); + + private constructor( + parent: OperationParent, + collectionName: string, + indexes: IndexDescription[], + allowUnknownIndexOptions: boolean, + commandOptions?: CreateIndexOptions + ); + private constructor( parent: OperationParent, collectionName: string, indexes: IndexDescription[], // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions: boolean, - commandOptions: CreateIndexesOptions | CreateIndexOptions | undefined + commandOptions?: CreateIndexesOptions | CreateIndexOptions ) { super(parent, commandOptions); From f560c71ba6fde9edb1b3ba7b1f9adb3e698559d0 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Wed, 23 Sep 2026 13:53:42 -0700 Subject: [PATCH 11/23] deprecate option for removal in 8.x --- src/collection.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 7397776d706..a0fd7a646c9 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -717,8 +717,10 @@ export class Collection { * * @param indexSpecs - An array of index specifications to be created * @param commandOptions - Optional settings for the `createIndexes` command - * @param allowUnknownIndexOptions - When `true`, index options the driver does not recognise are - * sent to the server instead of being dropped. Defaults to `false`; this will become the only + * @deprecated Used to opt into "pass through" behavior, where options will be validated by the server rather than the driver. + * In a future release, this will be removed and the default behavior will change from false to true. + * @param allowUnknownIndexOptions - When `true`, index options the driver does not recognise + * are sent to the server instead of being dropped. Defaults to `false`; this will become the only * behaviour in a future major release. */ async createIndexes( From e0ec4247505cb95d0386e1c44e0d2f436258cfa2 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Thu, 24 Sep 2026 06:18:13 -0700 Subject: [PATCH 12/23] Extend Document to allow unknown options without a @ts-expect-error override --- src/operations/indexes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index e7abcd74c5a..7cff70123f2 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -297,7 +297,7 @@ export interface CreateIndexesOptions extends Omit Date: Thu, 24 Sep 2026 06:24:43 -0700 Subject: [PATCH 13/23] Add 2dsphere options to IndexOptions interface --- src/operations/indexes.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 7cff70123f2..2cd57dd4028 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -373,6 +373,16 @@ export interface IndexOptions extends Document { */ '2dsphereIndexVersion'?: number; + /** + * Optionally specifies the finest S2 cell level indexed by a 2dsphere index. + */ + finestIndexedLevel?: number; + + /** + * Optionally specifies the coarsest S2 cell level indexed by a 2dsphere index. + */ + coarsestIndexedLevel?: number; + /** * Optionally specifies the precision of the stored geo hash in the 2d index, from 1 to 32. */ From 56d06d965d3ab6522adae9c6fe76b6735f80b7d6 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Thu, 24 Sep 2026 06:36:55 -0700 Subject: [PATCH 14/23] remove ts-expect-error --- .../create_indexes_option_validation.test.ts | 9 +-------- test/integration/index_management.test.ts | 1 - test/unit/collection.test.ts | 2 -- test/unit/operations/indexes.test.ts | 1 - 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts index 6f4b8a50151..4481d1bb65f 100644 --- a/test/integration/index-management/create_indexes_option_validation.test.ts +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -114,7 +114,6 @@ describe('createIndex option validation', function () { }); it('drops an unknown option from the options bag', async function () { - // @ts-expect-error CreateIndexesOptions is a closed interface await collection.createIndex({ d: 1 }, { unique: true, notARealOption: true }); expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'd_1', key: { d: 1 } }]); @@ -148,7 +147,6 @@ describe('createIndex option validation', function () { it('sends an unknown option to the server', async function () { const error = await collection - // @ts-expect-error CreateIndexesOptions is a closed interface .createIndex({ d: 1 }, { notARealOption: true }, {}) .catch(error => error); @@ -165,12 +163,7 @@ describe('createIndex option validation', function () { { requires: { mongodb: '>=6.0' } }, async function () { // `prepareUnique` is supported by the server but is not in the driver's allowlist - await collection.createIndex( - { e: 1 }, - // @ts-expect-error IndexOptions is a closed interface - { prepareUnique: true }, - {} - ); + await collection.createIndex({ e: 1 }, { prepareUnique: true }, {}); expect(sentIndexes()[0]).to.have.property('prepareUnique', true); const indexes = await collection.listIndexes().toArray(); diff --git a/test/integration/index_management.test.ts b/test/integration/index_management.test.ts index f95ced6f706..08dbfd7a942 100644 --- a/test/integration/index_management.test.ts +++ b/test/integration/index_management.test.ts @@ -807,7 +807,6 @@ describe('Indexes', function () { it( 'should run command with commitQuorum if specified on collection.createIndex', commitQuorumTest((db, collection) => - // @ts-expect-error revaluate this? collection.createIndex('a', { writeConcern: { w: 'majority' }, commitQuorum: 0 }) ) ); diff --git a/test/unit/collection.test.ts b/test/unit/collection.test.ts index e41e65f8da2..363d0834def 100644 --- a/test/unit/collection.test.ts +++ b/test/unit/collection.test.ts @@ -237,7 +237,6 @@ describe('Collection', function () { it('drops unknown index options on the two parameter path', async () => { const command = await captureCreateIndexes(collection => - // @ts-expect-error: unknown index options are filtered on the legacy path collection.createIndex({ a: 1 }, { unique: true, notARealIndexOption: true }) ); @@ -261,7 +260,6 @@ describe('Collection', function () { const command = await captureCreateIndexes(collection => collection.createIndex( { a: 1 }, - // @ts-expect-error: unknown index options are passed through to the server { unique: true, finestIndexedLevel: 15 }, { commitQuorum: 2 } ) diff --git a/test/unit/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index b05ac4aa830..c3b929f6168 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -171,7 +171,6 @@ describe('class CreateIndexesOperation', () => { describe('allowUnknownIndexOptions (createIndexes passthrough)', () => { const indexDescription = () => ({ key: { a: 1 }, - // @ts-expect-error: Testing that unknown options are passed through when enabled finestIndexedLevel: 15, randomOptionThatWillNeverBeAdded: true }); From 2712b41b5e097f191f5598c432e0035bd67b7985 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Thu, 24 Sep 2026 06:46:04 -0700 Subject: [PATCH 15/23] Add deprecated comment to two-parameter overload of createIndex --- src/collection.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/collection.ts b/src/collection.ts index a0fd7a646c9..a0365d20dd4 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -634,6 +634,8 @@ export class Collection { * // Equivalent to { j: 1, k: -1, l: 2d } * await collection.createIndex(['j', ['k', -1], { l: '2d' }]) * ``` + * @deprecated Use the three parameter overload, which separates index options from + * command options. This overload will be removed in a future major release. */ createIndex(indexSpec: IndexSpecification, options?: CreateIndexesOptions): Promise; From b3279ed2fefe451653bb2c80341602e334eef9fa Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Thu, 24 Sep 2026 07:04:30 -0700 Subject: [PATCH 16/23] change how I deprecated the allowUnknownIndexOptions parameter on createIndexes --- src/collection.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index a0365d20dd4..bd462e89a23 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -695,7 +695,7 @@ export class Collection { * Index specifications are defined {@link https://www.mongodb.com/docs/manual/reference/command/createIndexes/| here}. * * @param indexSpecs - An array of index specifications to be created - * @param options - Optional settings for the command + * @param commandOptions - Optional settings for the `createIndexes` command * * @example * ```ts @@ -716,15 +716,31 @@ export class Collection { * } * ]); * ``` + */ + createIndexes( + indexSpecs: IndexDescription[], + commandOptions?: CreateIndexesOptions + ): Promise; + + /** + * Creates multiple indexes in the collection, opting into "pass through" behavior for index + * options the driver does not recognise. * * @param indexSpecs - An array of index specifications to be created * @param commandOptions - Optional settings for the `createIndexes` command - * @deprecated Used to opt into "pass through" behavior, where options will be validated by the server rather than the driver. - * In a future release, this will be removed and the default behavior will change from false to true. - * @param allowUnknownIndexOptions - When `true`, index options the driver does not recognise - * are sent to the server instead of being dropped. Defaults to `false`; this will become the only - * behaviour in a future major release. + * @param allowUnknownIndexOptions - When `true`, index options the driver does not recognise are + * sent to the server instead of being dropped, for the server to validate. + * + * @deprecated Used to opt into "pass through" behavior, where options will be validated by the + * server rather than the driver. In a future release this overload will be removed and the + * default behavior will change from `false` to `true`. */ + createIndexes( + indexSpecs: IndexDescription[], + commandOptions: CreateIndexesOptions | undefined, + allowUnknownIndexOptions: boolean + ): Promise; + async createIndexes( indexSpecs: IndexDescription[], commandOptions?: CreateIndexesOptions, From 7339d8a17e653429aaad0b654d688b8cacdb9ec7 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Thu, 24 Sep 2026 07:17:05 -0700 Subject: [PATCH 17/23] type tests --- test/types/index_options.test-d.ts | 68 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/test/types/index_options.test-d.ts b/test/types/index_options.test-d.ts index e540f9654ab..95460b76a21 100644 --- a/test/types/index_options.test-d.ts +++ b/test/types/index_options.test-d.ts @@ -1,6 +1,11 @@ -import { expectAssignable, expectNotAssignable } from 'tsd'; +import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotDeprecated } from 'tsd'; -import type { IndexDescription } from '../mongodb'; +import type { + CreateIndexesOptions, + CreateIndexOptions, + IndexDescription, + IndexOptions +} from '../mongodb'; // test that all valid index options are allowed in IndexDescription expectAssignable({ key: {}, background: true }); @@ -24,3 +29,62 @@ expectAssignable({ key: {}, collation: { locale: 'en' } }); expectAssignable({ key: {}, wildcardProjection: {} }); expectAssignable({ key: {}, hidden: true }); expectNotAssignable({ key: {}, invalidOption: 2400 }); + +// `IndexOptions` holds options for the index itself. It uses the names the index management +// specification defines, which differ from the field names the `createIndexes` command expects for +// three of them (`version`, `defaultLanguage`, `languageOverride`). +expectAssignable({ background: true }); +expectAssignable({ expireAfterSeconds: 2400 }); +expectAssignable({ name: 'index_1' }); +expectAssignable({ sparse: true }); +expectAssignable({ storageEngine: {} }); +expectAssignable({ unique: true }); +expectAssignable({ version: 1 }); +expectAssignable({ defaultLanguage: 'english' }); +expectAssignable({ languageOverride: 'language' }); +expectAssignable({ textIndexVersion: 2 }); +expectAssignable({ weights: {} }); +expectAssignable({ '2dsphereIndexVersion': 2 }); +expectAssignable({ bits: 1 }); +expectAssignable({ max: 1.1 }); +expectAssignable({ min: 9.9 }); +expectAssignable({ bucketSize: 100 }); +expectAssignable({ partialFilterExpression: {} }); +expectAssignable({ collation: { locale: 'en' } }); +expectAssignable({ wildcardProjection: {} }); +expectAssignable({ hidden: true }); +expectAssignable({ clustered: true }); + +// 2dsphere cell levels are supported by the server but are not in the specification. +expectAssignable({ finestIndexedLevel: 15 }); +expectAssignable({ coarsestIndexedLevel: 3 }); + +// `IndexOptions` is intentionally open so that index options the driver has not learned about yet +// can be supplied and passed through to the server for validation. +expectAssignable({ anOptionTheDriverDoesNotKnowAbout: true }); + +// `CreateIndexOptions` holds options for the `createIndexes` command, not for the index. The +// command level options it does not declare itself are inherited from `CommandOperationOptions`. +expectAssignable({ commitQuorum: 2 }); +expectAssignable({ commitQuorum: 'votingMembers' }); +expectAssignable({ maxTimeMS: 1000 }); +expectAssignable({ comment: 'a comment' }); +expectAssignable({ writeConcern: { w: 1 } }); +expectAssignable({ timeoutMS: 1000 }); + +// `collation` belongs on the index description, not at the command root, so it is omitted. +expectNotAssignable({ collation: { locale: 'en' } }); +// index options do not belong in the command options bag +expectNotAssignable({ unique: true }); + +// The legacy interface combines index and command options and stays closed, so a misspelled option is +// still caught at compile time on the two parameter overload. +expectNotAssignable({ anOptionTheDriverDoesNotKnowAbout: true }); + +// Index options on the legacy interface are deprecated in favour of `IndexOptions`; `commitQuorum` is a +// command option and is not. +declare const legacyOptions: CreateIndexesOptions; +expectDeprecated(legacyOptions.unique); +expectDeprecated(legacyOptions.default_language); +expectDeprecated(legacyOptions.collation); +expectNotDeprecated(legacyOptions.commitQuorum); From 293bdc294a0c27c12c836825e84532a50fe0ad56 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Fri, 25 Sep 2026 14:06:46 -0700 Subject: [PATCH 18/23] more TODO -> ticket number updates --- src/collection.ts | 5 +---- src/operations/indexes.ts | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index bd462e89a23..f84c8e67f15 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -753,10 +753,7 @@ export class Collection { this, this.collectionName, indexSpecs, - // TODO(seanrmilligan): default this to true and remove the parameter in a future major - // release. Index options live on each index description, so nothing on this path - // contaminates them -- but flipping it turns today's silently dropped unknown option into - // a server error. + // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions, resolveOptions(this, { ...commandOptions, maxTimeMS: undefined }) ) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 2cd57dd4028..79f8440f1bd 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -663,8 +663,8 @@ export class CreateIndexesOperation extends CommandOperation { collectionName, [description], allowUnknownIndexOptions, - // TODO(seanrmilligan): remove the `?? indexOptions` fallback when the two parameter path is - // deprecated. Once `indexOptions` is index options only it must not reach the command root. + // TODO(NODE-7868): Remove the `?? indexOptions` fallback along with the two parameter path. + // Once `indexOptions` is index options only it must not reach the command root. commandOptions ?? indexOptions ); } From 585c7fdaf2101d9b361e545ba53ed177765e11dc Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Wed, 30 Sep 2026 13:16:06 -0700 Subject: [PATCH 19/23] refactor(NODE-6893): require explicit command options on CreateIndexesOperation Collapse the private constructor to a single signature and make the trailing options parameters of the constructor and both static factories mandatory but nullable (`| undefined`). Callers must now pass `undefined` explicitly, so "forgot to pass command options" is a compile error rather than being indistinguishable from "there are no command options". Drops the redundant constructor overloads and the third `fromIndexSpecification` signature, and updates every call site (including tests) to pass `undefined` where no command options apply. --- src/collection.ts | 3 +- src/db.ts | 3 +- src/operations/create_collection.ts | 3 +- src/operations/indexes.ts | 36 ++++--------------- .../crud/abstract_operation.test.ts | 3 +- test/unit/operations/indexes.test.ts | 3 +- 6 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index f84c8e67f15..9866314a4f9 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -671,7 +671,8 @@ export class Collection { this.collectionName, indexSpec, /*allowUnknownIndexOptions=*/ false, - resolveOptions(this, indexOptions) // at this point indexOptions is the combined index and command options + resolveOptions(this, indexOptions), // at this point indexOptions is the combined index and command options + undefined ) : CreateIndexesOperation.fromIndexSpecification( this, diff --git a/src/db.ts b/src/db.ts index 1edf1072bfe..bb8165cd0e5 100644 --- a/src/db.ts +++ b/src/db.ts @@ -459,7 +459,8 @@ export class Db { name, indexSpec, /*allowUnknownIndexOptions=*/ false, - options ?? {} + options ?? {}, + undefined ) ); return indexes[0]; diff --git a/src/operations/create_collection.ts b/src/operations/create_collection.ts index 89731fd1765..f62f80b8255 100644 --- a/src/operations/create_collection.ts +++ b/src/operations/create_collection.ts @@ -208,7 +208,8 @@ export async function createCollections( name, { __safeContent__: 1 }, /*allowUnknownIndexOptions=*/ false, - { session: options.session } + { session: options.session }, + undefined ); await executeOperation(db.client, createIndexOp, timeoutContext); } diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 79f8440f1bd..a0b8cac9bf1 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -561,29 +561,13 @@ export class CreateIndexesOperation extends CommandOperation { collectionName: string; indexes: ReadonlyArray; - private constructor( - parent: OperationParent, - collectionName: string, - indexes: IndexDescription[], - allowUnknownIndexOptions: boolean, - options?: CreateIndexesOptions - ); - - private constructor( - parent: OperationParent, - collectionName: string, - indexes: IndexDescription[], - allowUnknownIndexOptions: boolean, - commandOptions?: CreateIndexOptions - ); - private constructor( parent: OperationParent, collectionName: string, indexes: IndexDescription[], // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions: boolean, - commandOptions?: CreateIndexesOptions | CreateIndexOptions + commandOptions: CreateIndexesOptions | CreateIndexOptions | undefined ) { super(parent, commandOptions); @@ -617,7 +601,7 @@ export class CreateIndexesOperation extends CommandOperation { indexes: IndexDescription[], // TODO(NODE-7868): Remove allowUnknownIndexOptions with a default behavior of true in a future major version release allowUnknownIndexOptions: boolean, - commandOptions?: CreateIndexesOptions + commandOptions: CreateIndexesOptions | undefined ): CreateIndexesOperation { return new CreateIndexesOperation( parent, @@ -633,16 +617,8 @@ export class CreateIndexesOperation extends CommandOperation { collectionName: string, indexSpec: IndexSpecification, allowUnknownIndexOptions: boolean, - options: CreateIndexesOptions - ): CreateIndexesOperation; - - static fromIndexSpecification( - parent: OperationParent, - collectionName: string, - indexSpec: IndexSpecification, - allowUnknownIndexOptions: boolean, - indexOptions?: IndexOptions, - commandOptions?: CreateIndexOptions + indexOptions: IndexOptions | undefined, + commandOptions: CreateIndexOptions | undefined ): CreateIndexesOperation; static fromIndexSpecification( @@ -650,8 +626,8 @@ export class CreateIndexesOperation extends CommandOperation { collectionName: string, indexSpec: IndexSpecification, allowUnknownIndexOptions: boolean, - indexOptions?: CreateIndexesOptions | IndexOptions, - commandOptions?: CreateIndexOptions + indexOptions: CreateIndexesOptions | IndexOptions | undefined, + commandOptions: CreateIndexOptions | undefined ): CreateIndexesOperation { const key = constructIndexDescriptionMap(indexSpec); // If called with overload using `CreateIndexesOptions`, then indexOptions may contain combined index and command options diff --git a/test/integration/crud/abstract_operation.test.ts b/test/integration/crud/abstract_operation.test.ts index dc6a2fd9079..0beec2ff315 100644 --- a/test/integration/crud/abstract_operation.test.ts +++ b/test/integration/crud/abstract_operation.test.ts @@ -159,7 +159,8 @@ describe('abstract operation', function () { db, 'bar', [{ key: { a: 1 } }], - /*allowUnknownIndexOptions=*/ false + /*allowUnknownIndexOptions=*/ false, + undefined ), subclassType: CreateIndexesOperation, correctCommandName: 'createIndexes' diff --git a/test/unit/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index c3b929f6168..38fa341feaa 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -105,7 +105,8 @@ describe('class CreateIndexesOperation', () => { 'b', input, /*allowUnknownIndexOptions=*/ false, - options + options, + undefined ); const makeIndexesOperation = ( From f9fac6fced7d50f1e9b6233c161bbc37437a194a Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Wed, 30 Sep 2026 13:36:55 -0700 Subject: [PATCH 20/23] Remove clustered, finestIndexedLevel, coarsestIndexedLevel --- src/operations/indexes.ts | 19 ------------------- test/types/index_options.test-d.ts | 4 ++-- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index a0b8cac9bf1..6b7edd599b3 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -373,16 +373,6 @@ export interface IndexOptions extends Document { */ '2dsphereIndexVersion'?: number; - /** - * Optionally specifies the finest S2 cell level indexed by a 2dsphere index. - */ - finestIndexedLevel?: number; - - /** - * Optionally specifies the coarsest S2 cell level indexed by a 2dsphere index. - */ - coarsestIndexedLevel?: number; - /** * Optionally specifies the precision of the stored geo hash in the 2d index, from 1 to 32. */ @@ -429,15 +419,6 @@ export interface IndexOptions extends Document { * This option is only supported by servers \>= 4.4. */ hidden?: boolean; - - /** - * Optionally specifies that this index is clustered. This is not a valid option to provide to - * 'createIndexes', but can appear in the options returned for an index via 'listIndexes'. To - * create a clustered index, create a new collection using the 'clusteredIndex' option. - * - * This options is only supported by servers \>= 6.0. - */ - clustered?: boolean; } /** @public */ diff --git a/test/types/index_options.test-d.ts b/test/types/index_options.test-d.ts index 95460b76a21..6ac9b838517 100644 --- a/test/types/index_options.test-d.ts +++ b/test/types/index_options.test-d.ts @@ -53,9 +53,9 @@ expectAssignable({ partialFilterExpression: {} }); expectAssignable({ collation: { locale: 'en' } }); expectAssignable({ wildcardProjection: {} }); expectAssignable({ hidden: true }); -expectAssignable({ clustered: true }); -// 2dsphere cell levels are supported by the server but are not in the specification. +// 2dsphere cell levels are supported by the server but are not modelled by the driver; they are +// accepted because `IndexOptions` is open, and passed through to the server for validation. expectAssignable({ finestIndexedLevel: 15 }); expectAssignable({ coarsestIndexedLevel: 3 }); From 04ea05d02b819d96e943e28a771662a28eb724f6 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Wed, 30 Sep 2026 14:04:00 -0700 Subject: [PATCH 21/23] refactor(NODE-6893): require both arguments on the three parameter createIndex overload Make `indexOptions` and `commandOptions` on the three parameter `createIndex` overload mandatory (still nullable). With both optional, a two argument call that failed the legacy overload on an unknown option fell through to this overload, since `IndexOptions` accepts arbitrary keys. That silently dropped the compile error, hid the deprecation warning, and matched a pass-through signature for a call that takes the legacy path at runtime. Two argument calls can now only match the legacy overload, so the unknown option error is reported again. Restore the `@ts-expect-error` on the two tests that deliberately pass an unknown option on the legacy path. --- src/collection.ts | 8 ++++---- .../create_indexes_option_validation.test.ts | 1 + test/unit/collection.test.ts | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 9866314a4f9..28892b36c49 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -647,13 +647,13 @@ export class Collection { * `indexOptions` are passed through to the server for validation rather than being dropped. * * @param keys - The field name or index specification to create an index for - * @param indexOptions - Optional settings for the index - * @param commandOptions - Optional settings for the `createIndexes` command + * @param indexOptions - Settings for the index, or `undefined` if there are none + * @param commandOptions - Settings for the `createIndexes` command, or `undefined` if there are none */ createIndex( keys: IndexSpecification, - indexOptions?: IndexOptions, - commandOptions?: CreateIndexOptions + indexOptions: IndexOptions | undefined, + commandOptions: CreateIndexOptions | undefined ): Promise; async createIndex( diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts index 4481d1bb65f..b2887717b4f 100644 --- a/test/integration/index-management/create_indexes_option_validation.test.ts +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -114,6 +114,7 @@ describe('createIndex option validation', function () { }); it('drops an unknown option from the options bag', async function () { + // @ts-expect-error: the legacy options type is closed; the unknown option is dropped at runtime await collection.createIndex({ d: 1 }, { unique: true, notARealOption: true }); expect(sentIndexes()).to.deep.equal([{ unique: true, name: 'd_1', key: { d: 1 } }]); diff --git a/test/unit/collection.test.ts b/test/unit/collection.test.ts index 363d0834def..953e988db1a 100644 --- a/test/unit/collection.test.ts +++ b/test/unit/collection.test.ts @@ -237,6 +237,7 @@ describe('Collection', function () { it('drops unknown index options on the two parameter path', async () => { const command = await captureCreateIndexes(collection => + // @ts-expect-error: the legacy options type is closed; the unknown option is dropped at runtime collection.createIndex({ a: 1 }, { unique: true, notARealIndexOption: true }) ); From 8a5596f77512e209cb1b46bea4da3621f27c6481 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Wed, 30 Sep 2026 14:55:38 -0700 Subject: [PATCH 22/23] refactor(NODE-6893): remove background from IndexOptions `IndexOptions` is new in this change, and `background` has been ignored by the server since 4.2 while the driver's minimum supported server is 4.4. Rather than add the field only to deprecate and remove it later, leave it out. GridFS no longer sends `background` when creating its indexes, which has no effect on the server. The released `CreateIndexesOptions.background` is kept for the two parameter path and remains deprecated until v8. --- src/gridfs/upload.ts | 8 ++------ src/operations/indexes.ts | 10 ---------- test/types/index_options.test-d.ts | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/gridfs/upload.ts b/src/gridfs/upload.ts index c1abac34855..c0f29307fa7 100644 --- a/src/gridfs/upload.ts +++ b/src/gridfs/upload.ts @@ -272,11 +272,7 @@ async function checkChunksIndex(stream: GridFSBucketWriteStream): Promise remainingTimeMS = stream.timeoutContext?.getRemainingTimeMSOrThrow( `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms` ); - await stream.chunks.createIndex( - index, - { background: true, unique: true }, - { timeoutMS: remainingTimeMS } - ); + await stream.chunks.createIndex(index, { unique: true }, { timeoutMS: remainingTimeMS }); } } @@ -378,7 +374,7 @@ async function checkIndexes(stream: GridFSBucketWriteStream): Promise { `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms` ); - await stream.files.createIndex(index, { background: false }, { timeoutMS: remainingTimeMS }); + await stream.files.createIndex(index, {}, { timeoutMS: remainingTimeMS }); } await checkChunksIndex(stream); diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index 6b7edd599b3..c9a3659a3bc 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -298,16 +298,6 @@ export interface CreateIndexesOptions extends Omit({ key: {}, invalidOption: 2400 }); // `IndexOptions` holds options for the index itself. It uses the names the index management // specification defines, which differ from the field names the `createIndexes` command expects for // three of them (`version`, `defaultLanguage`, `languageOverride`). -expectAssignable({ background: true }); expectAssignable({ expireAfterSeconds: 2400 }); expectAssignable({ name: 'index_1' }); expectAssignable({ sparse: true }); From 1a43ad0d141cbb0dd3bd5124bcb77aab2b43ef28 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Thu, 1 Oct 2026 13:45:48 -0700 Subject: [PATCH 23/23] pr feedback: testing --- src/collection.ts | 4 +- src/db.ts | 2 +- src/operations/create_collection.ts | 2 +- .../crud/abstract_operation.test.ts | 2 +- .../create_indexes_option_validation.test.ts | 115 +++++++++--------- test/integration/index_management.test.ts | 18 +-- test/unit/operations/indexes.test.ts | 110 +++++++++-------- 7 files changed, 121 insertions(+), 132 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 28892b36c49..16617c0b715 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -670,7 +670,7 @@ export class Collection { this, this.collectionName, indexSpec, - /*allowUnknownIndexOptions=*/ false, + false, resolveOptions(this, indexOptions), // at this point indexOptions is the combined index and command options undefined ) @@ -678,7 +678,7 @@ export class Collection { this, this.collectionName, indexSpec, - /*allowUnknownIndexOptions=*/ true, + true, indexOptions, resolveOptions(this, commandOptions) ); diff --git a/src/db.ts b/src/db.ts index bb8165cd0e5..7aa21ac0888 100644 --- a/src/db.ts +++ b/src/db.ts @@ -458,7 +458,7 @@ export class Db { this, name, indexSpec, - /*allowUnknownIndexOptions=*/ false, + false, options ?? {}, undefined ) diff --git a/src/operations/create_collection.ts b/src/operations/create_collection.ts index f62f80b8255..63764d3dbb8 100644 --- a/src/operations/create_collection.ts +++ b/src/operations/create_collection.ts @@ -207,7 +207,7 @@ export async function createCollections( db, name, { __safeContent__: 1 }, - /*allowUnknownIndexOptions=*/ false, + false, { session: options.session }, undefined ); diff --git a/test/integration/crud/abstract_operation.test.ts b/test/integration/crud/abstract_operation.test.ts index 0beec2ff315..82b8f5e03e6 100644 --- a/test/integration/crud/abstract_operation.test.ts +++ b/test/integration/crud/abstract_operation.test.ts @@ -159,7 +159,7 @@ describe('abstract operation', function () { db, 'bar', [{ key: { a: 1 } }], - /*allowUnknownIndexOptions=*/ false, + false, undefined ), subclassType: CreateIndexesOperation, diff --git a/test/integration/index-management/create_indexes_option_validation.test.ts b/test/integration/index-management/create_indexes_option_validation.test.ts index b2887717b4f..41f0280779b 100644 --- a/test/integration/index-management/create_indexes_option_validation.test.ts +++ b/test/integration/index-management/create_indexes_option_validation.test.ts @@ -140,47 +140,68 @@ describe('createIndex option validation', function () { }); describe('when command options are given (three parameter form)', function () { - it('does not send driver options the user never supplied', async function () { - await collection.createIndex({ a: 1 }, {}, {}); - - expect(sentIndexes()).to.deep.equal([{ key: { a: 1 }, name: 'a_1' }]); - }); + describe('and the command options are an empty object', function () { + it('fails to create an index using an index option which the server does not recognize', async function () { + const error = await collection + .createIndex({ d: 1 }, { notARealOption: true }, {}) + .catch(error => error); - it('sends an unknown option to the server', async function () { - const error = await collection - .createIndex({ d: 1 }, { notARealOption: true }, {}) - .catch(error => error); + // the driver forwards the option; the server is what rejects it + expect(sentIndexes()[0]).to.have.property('notARealOption', true); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.message).to.match(/not valid for an index specification/); + }); - // the driver forwards the option; the server is what rejects it - expect(sentIndexes()[0]).to.have.property('notARealOption', true); - expect(error).to.be.instanceOf(MongoServerError); - expect(error.message).to.match(/not valid for an index specification/); - }); + it( + 'creates an index using an index option the driver does not know about', + // `prepareUnique` was introduced in server 6.0; on older servers it is not a valid index + // option and the server rejects the command, so this test cannot run there. + { requires: { mongodb: '>=6.0' } }, + async function () { + // `prepareUnique` is supported by the server but is not in the driver's allowlist + await collection.createIndex({ e: 1 }, { prepareUnique: true }, {}); + + expect(sentIndexes()[0]).to.have.property('prepareUnique', true); + const indexes = await collection.listIndexes().toArray(); + expect(indexes.find(index => index.name === 'e_1')).to.have.property( + 'prepareUnique', + true + ); + } + ); - it( - 'creates an index using a server option the driver does not know about', - // `prepareUnique` was introduced in server 6.0; on older servers it is not a valid index - // option and the server rejects the command, so this test cannot run there. - { requires: { mongodb: '>=6.0' } }, - async function () { - // `prepareUnique` is supported by the server but is not in the driver's allowlist - await collection.createIndex({ e: 1 }, { prepareUnique: true }, {}); + it('sends index options as normal', async function () { + await collection.createIndex({ f: 1 }, { unique: true, sparse: true, version: 2 }, {}); - expect(sentIndexes()[0]).to.have.property('prepareUnique', true); - const indexes = await collection.listIndexes().toArray(); - expect(indexes.find(index => index.name === 'e_1')).to.have.property('prepareUnique', true); - } - ); - - it('sends index options as normal', async function () { - await collection.createIndex({ f: 1 }, { unique: true, sparse: true, version: 2 }, {}); + expect(sentIndexes()).to.deep.equal([ + { unique: true, sparse: true, v: 2, name: 'f_1', key: { f: 1 } } + ]); + }); - expect(sentIndexes()).to.deep.equal([ - { unique: true, sparse: true, v: 2, name: 'f_1', key: { f: 1 } } - ]); + describe('and a command option is left in the index options', function () { + it('forwards a comment to the server, which rejects it', async function () { + const error = await collection + .createIndex({ l: 1 }, { unique: true, comment: 'a comment' }, {}) + .catch(error => error); + + expect(sentIndexes()[0]).to.have.property('comment', 'a comment'); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.message).to.match(/not valid for an index specification/); + }); + + it('forwards maxTimeMS to the server, which rejects it', async function () { + const error = await collection + .createIndex({ m: 1 }, { unique: true, maxTimeMS: 1000 }, {}) + .catch(error => error); + + expect(sentIndexes()[0]).to.have.property('maxTimeMS', 1000); + expect(error).to.be.instanceOf(MongoServerError); + expect(error.message).to.match(/not valid for an index specification/); + }); + }); }); - describe('and command options are passed in the third parameter', function () { + describe('and the command options have values', function () { it('keeps a comment out of the index description', async function () { await collection.createIndex({ g: 1 }, { unique: true }, { comment: 'a comment' }); @@ -231,28 +252,6 @@ describe('createIndex option validation', function () { expect(sentCommand()).to.have.property('writeConcern'); }); }); - - describe('and a command option is left in the index options', function () { - it('forwards a comment to the server, which rejects it', async function () { - const error = await collection - .createIndex({ l: 1 }, { unique: true, comment: 'a comment' }, {}) - .catch(error => error); - - expect(sentIndexes()[0]).to.have.property('comment', 'a comment'); - expect(error).to.be.instanceOf(MongoServerError); - expect(error.message).to.match(/not valid for an index specification/); - }); - - it('forwards maxTimeMS to the server, which rejects it', async function () { - const error = await collection - .createIndex({ m: 1 }, { unique: true, maxTimeMS: 1000 }, {}) - .catch(error => error); - - expect(sentIndexes()[0]).to.have.property('maxTimeMS', 1000); - expect(error).to.be.instanceOf(MongoServerError); - expect(error.message).to.match(/not valid for an index specification/); - }); - }); }); }); @@ -337,7 +336,7 @@ describe('createIndexes option validation', function () { // @ts-expect-error IndexDescription is a closed interface [{ key: { d: 1 }, name: 'd_1', notARealOption: true }], {}, - /*allowUnknownIndexOptions=*/ true + true ) .catch(error => error); @@ -356,7 +355,7 @@ describe('createIndexes option validation', function () { // @ts-expect-error IndexDescription is a closed interface [{ key: { e: 1 }, name: 'e_1', prepareUnique: true }], {}, - /*allowUnknownIndexOptions=*/ true + true ); expect(sentIndexes()[0]).to.have.property('prepareUnique', true); diff --git a/test/integration/index_management.test.ts b/test/integration/index_management.test.ts index 08dbfd7a942..4bad76589f9 100644 --- a/test/integration/index_management.test.ts +++ b/test/integration/index_management.test.ts @@ -254,7 +254,7 @@ describe('Indexes', function () { ); context('when an unknown index option is provided', function () { - context('and allowUnknownIndexOptions is unset (default)', function () { + context('and allowUnknownIndexOptions is false (the default when unset)', function () { it('silently drops the unknown option and creates the index', async () => { const [name] = await collection.createIndexes([ // @ts-expect-error: intentionally providing an unknown option @@ -266,20 +266,6 @@ describe('Indexes', function () { }); }); - context('and allowUnknownIndexOptions is false', function () { - it('silently drops the unknown option and creates the index', async () => { - const [name] = await collection.createIndexes( - // @ts-expect-error: intentionally providing an unknown option - [{ key: { loc: '2dsphere' }, thisOptionDoesNotExist: true }], - {}, - /*allowUnknownIndexOptions=*/ false - ); - expect(started[0].command.indexes[0]).to.not.have.property('thisOptionDoesNotExist'); - const indexes = await collection.listIndexes().toArray(); - expect(indexes.map(i => i.name)).to.include(name); - }); - }); - context('and allowUnknownIndexOptions is true', function () { it('passes the option through and surfaces the server error', async () => { const error = await collection @@ -287,7 +273,7 @@ describe('Indexes', function () { // @ts-expect-error: intentionally providing an unknown option [{ key: { loc: '2dsphere' }, thisOptionDoesNotExist: true }], {}, - /*allowUnknownIndexOptions=*/ true + true ) .catch(error => error); expect(error).to.be.instanceOf(MongoServerError); diff --git a/test/unit/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index 38fa341feaa..db845294f11 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -104,7 +104,7 @@ describe('class CreateIndexesOperation', () => { { s: { namespace: ns('a.b') } }, 'b', input, - /*allowUnknownIndexOptions=*/ false, + false, options, undefined ); @@ -169,70 +169,74 @@ describe('class CreateIndexesOperation', () => { }); }); - describe('allowUnknownIndexOptions (createIndexes passthrough)', () => { - const indexDescription = () => ({ - key: { a: 1 }, - finestIndexedLevel: 15, - randomOptionThatWillNeverBeAdded: true - }); - - it('drops unknown options when the flag is unset (default behavior)', () => { - const output = makeIndexesOperation([indexDescription()]); - expect(output.indexes[0]).to.not.have.property('finestIndexedLevel'); - expect(output.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); - }); + describe('CreateIndexesOperation.fromIndexDescriptionArray', () => { + describe('allowUnknownIndexOptions (createIndexes passthrough)', () => { + const indexDescription = () => ({ + key: { a: 1 }, + finestIndexedLevel: 15, + randomOptionThatWillNeverBeAdded: true + }); - it('drops unknown options when the flag is set to false', () => { - const output = makeIndexesOperation([indexDescription()], { - allowUnknownIndexOptions: false + it('drops unknown options when the allowUnknownIndexOptions is false (default behavior when unset)', () => { + const output = makeIndexesOperation([indexDescription()]); + expect(output.indexes[0]).to.not.have.property('finestIndexedLevel'); + expect(output.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); }); - expect(output.indexes[0]).to.not.have.property('finestIndexedLevel'); - expect(output.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); - }); - it('retains unknown options when the flag is set to true', () => { - const output = makeIndexesOperation([indexDescription()], { - allowUnknownIndexOptions: true + it('drops unknown options when allowUnknownIndexOptions is set to false', () => { + const output = makeIndexesOperation([indexDescription()], { + allowUnknownIndexOptions: false + }); + expect(output.indexes[0]).to.not.have.property('finestIndexedLevel'); + expect(output.indexes[0]).to.not.have.property('randomOptionThatWillNeverBeAdded'); }); - expect(output.indexes[0]).to.have.property('finestIndexedLevel', 15); - expect(output.indexes[0]).to.have.property('randomOptionThatWillNeverBeAdded', true); - }); - it('rebuilds `key` as a Map even when unknown options are passed through', () => { - const output = makeIndexesOperation([{ key: { a: 1, b: -1 } }], { - allowUnknownIndexOptions: true + it('retains unknown options when allowUnknownIndexOptions is set to true', () => { + const output = makeIndexesOperation([indexDescription()], { + allowUnknownIndexOptions: true + }); + expect(output.indexes[0]).to.have.property('finestIndexedLevel', 15); + expect(output.indexes[0]).to.have.property('randomOptionThatWillNeverBeAdded', true); }); - // `key` must not survive the option filter: the operation rebuilds it as a Map so that - // index key ordering is preserved, and re-adds it after the filtered options. - expect(output.indexes[0].key).to.be.instanceOf(Map); - expect(output.indexes[0].key).to.deep.equal( - new Map([ - ['a', 1], - ['b', -1] - ]) - ); - }); + it('rebuilds `key` as a Map even when unknown options are passed through', () => { + const output = makeIndexesOperation([{ key: { a: 1, b: -1 } }], { + allowUnknownIndexOptions: true + }); + + // `key` must not survive the option filter: the operation rebuilds it as a Map so that + // index key ordering is preserved, and re-adds it after the filtered options. + expect(output.indexes[0].key).to.be.instanceOf(Map); + expect(output.indexes[0].key).to.deep.equal( + new Map([ + ['a', 1], + ['b', -1] + ]) + ); + }); - it('renames the text index language options the server expects in snake_case', () => { - const output = makeIndexesOperation( - [{ key: { a: 'text' }, defaultLanguage: 'spanish', languageOverride: 'lang' }], - { allowUnknownIndexOptions: true } - ); - expect(output.indexes[0]).to.have.property('default_language', 'spanish'); - expect(output.indexes[0]).to.have.property('language_override', 'lang'); - expect(output.indexes[0]).to.not.have.property('defaultLanguage'); - expect(output.indexes[0]).to.not.have.property('languageOverride'); - }); + it('renames the text index language option names to ones the server expects', () => { + const output = makeIndexesOperation( + [{ key: { a: 'text' }, defaultLanguage: 'spanish', languageOverride: 'lang' }], + { allowUnknownIndexOptions: true } + ); + expect(output.indexes[0]).to.have.property('default_language', 'spanish'); + expect(output.indexes[0]).to.have.property('language_override', 'lang'); + expect(output.indexes[0]).to.not.have.property('defaultLanguage'); + expect(output.indexes[0]).to.not.have.property('languageOverride'); + }); - it('still maps `version` to `v` when the flag is set to true', () => { - const output = makeIndexesOperation([{ key: { a: 1 }, version: 1 }], { - allowUnknownIndexOptions: true + it('still maps `version` to `v` when allowUnknownIndexOptions is set to true', () => { + const output = makeIndexesOperation([{ key: { a: 1 }, version: 1 }], { + allowUnknownIndexOptions: true + }); + expect(output.indexes[0]).to.have.property('v', 1); + expect(output.indexes[0]).to.not.have.property('version'); }); - expect(output.indexes[0]).to.have.property('v', 1); - expect(output.indexes[0]).to.not.have.property('version'); }); + }); + describe('CreateIndexesOperation.fromIndexSpecification', () => { it('does not enable passthrough for createIndex even when the flag is set to true', () => { const output = makeIndexOperation( { a: 1 },