From 93afa527d6d06b07c8ce4c0172f011cca5589074 Mon Sep 17 00:00:00 2001 From: Sean Milligan Date: Mon, 27 Jul 2026 19:27:54 -0700 Subject: [PATCH] Allow passthrough options on createIndexes --- src/operations/indexes.ts | 45 +++++++++++++----- test/integration/index_management.test.ts | 41 ++++++++++++++++ test/unit/operations/indexes.test.ts | 57 +++++++++++++++++++++++ 3 files changed, 132 insertions(+), 11 deletions(-) diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index f6354dcf1ad..04152f3e231 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -161,6 +161,13 @@ export interface CreateIndexesOptions extends Omit { - 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])) ); } @@ -252,7 +264,8 @@ export class CreateIndexesOperation extends CommandOperation { parent: OperationParent, collectionName: string, indexes: IndexDescription[], - options?: CreateIndexesOptions + options: CreateIndexesOptions | undefined, + allowUnknownIndexOptions: boolean ) { super(parent, options); @@ -265,9 +278,9 @@ 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 indexOptions = resolveIndexDescription(userIndex, allowUnknownIndexOptions); return { - ...validIndexOptions, + ...indexOptions, name, key }; @@ -281,7 +294,15 @@ export class CreateIndexesOperation extends CommandOperation { indexes: IndexDescription[], options?: CreateIndexesOptions ): CreateIndexesOperation { - return new CreateIndexesOperation(parent, collectionName, indexes, options); + // `allowUnknownIndexOptions` passthrough is only supported via `createIndexes`, where each + // index is a user-provided description that does not carry command/driver-level options. + return new CreateIndexesOperation( + parent, + collectionName, + indexes, + options, + options?.allowUnknownIndexOptions ?? false + ); } static fromIndexSpecification( @@ -292,7 +313,9 @@ export class CreateIndexesOperation extends CommandOperation { ): CreateIndexesOperation { const key = constructIndexDescriptionMap(indexSpec); const description: IndexDescription = { ...options, key }; - return new CreateIndexesOperation(parent, collectionName, [description], options); + // The allowlist is always enforced for `createIndex` because command/driver-level options are + // merged into the index description above and must not be passed through to the server. + return new CreateIndexesOperation(parent, collectionName, [description], options, false); } override get commandName() { diff --git a/test/integration/index_management.test.ts b/test/integration/index_management.test.ts index 100cb1c9a5a..d9d04619c37 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/operations/indexes.test.ts b/test/unit/operations/indexes.test.ts index 585da82356a..7cf020a859c 100644 --- a/test/unit/operations/indexes.test.ts +++ b/test/unit/operations/indexes.test.ts @@ -107,6 +107,14 @@ describe('class CreateIndexesOperation', () => { options ); + const makeIndexesOperation = (indexes, options: CreateIndexesOptions = {}) => + CreateIndexesOperation.fromIndexDescriptionArray( + { s: { namespace: ns('a.b') } }, + 'b', + indexes, + options + ); + describe('#constructor()', () => { for (const { description, input, mapData, name } of testCases) { it(`should create fieldHash correctly when input is: ${description}`, () => { @@ -152,4 +160,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'); + }); + }); });