Skip to content

Commit 1879a04

Browse files
fix(NODE-5971): attach v to createIndexes command when version is specified (#4043)
1 parent abf8bdf commit 1879a04

File tree

2 files changed

+123
-21
lines changed

2 files changed

+123
-21
lines changed

src/operations/indexes.ts

+46-20
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,11 @@ function isSingleIndexTuple(t: unknown): t is [string, IndexDirection] {
167167
return Array.isArray(t) && t.length === 2 && isIndexDirection(t[1]);
168168
}
169169

170-
function makeIndexSpec(
171-
indexSpec: IndexSpecification,
172-
options?: CreateIndexesOptions
173-
): IndexDescription {
170+
/**
171+
* Converts an `IndexSpecification`, which can be specified in multiple formats, into a
172+
* valid `key` for the createIndexes command.
173+
*/
174+
function constructIndexDescriptionMap(indexSpec: IndexSpecification): Map<string, IndexDirection> {
174175
const key: Map<string, IndexDirection> = new Map();
175176

176177
const indexSpecs =
@@ -193,14 +194,46 @@ function makeIndexSpec(
193194
}
194195
}
195196

196-
return { ...options, key };
197+
return key;
197198
}
198199

200+
/**
201+
* Receives an index description and returns a modified index description which has had invalid options removed
202+
* from the description and has mapped the `version` option to the `v` option.
203+
*/
204+
function resolveIndexDescription(
205+
description: IndexDescription
206+
): Omit<ResolvedIndexDescription, 'key'> {
207+
const validProvidedOptions = Object.entries(description).filter(([optionName]) =>
208+
VALID_INDEX_OPTIONS.has(optionName)
209+
);
210+
211+
return Object.fromEntries(
212+
// we support the `version` option, but the `createIndexes` command expects it to be the `v`
213+
validProvidedOptions.map(([name, value]) => (name === 'version' ? ['v', value] : [name, value]))
214+
);
215+
}
216+
217+
/**
218+
* @internal
219+
*
220+
* Internally, the driver represents index description keys with `Map`s to preserve key ordering.
221+
* We don't require users to specify maps, so we transform user provided descriptions into
222+
* "resolved" by converting the `key` into a JS `Map`, if it isn't already a map.
223+
*
224+
* Additionally, we support the `version` option, but the `createIndexes` command uses the field `v`
225+
* to specify the index version so we map the value of `version` to `v`, if provided.
226+
*/
227+
type ResolvedIndexDescription = Omit<IndexDescription, 'key' | 'version'> & {
228+
key: Map<string, IndexDirection>;
229+
v?: IndexDescription['version'];
230+
};
231+
199232
/** @internal */
200233
export class CreateIndexesOperation extends CommandOperation<string[]> {
201234
override options: CreateIndexesOptions;
202235
collectionName: string;
203-
indexes: ReadonlyArray<Omit<IndexDescription, 'key'> & { key: Map<string, IndexDirection> }>;
236+
indexes: ReadonlyArray<ResolvedIndexDescription>;
204237

205238
private constructor(
206239
parent: OperationParent,
@@ -212,16 +245,12 @@ export class CreateIndexesOperation extends CommandOperation<string[]> {
212245

213246
this.options = options ?? {};
214247
this.collectionName = collectionName;
215-
this.indexes = indexes.map(userIndex => {
248+
this.indexes = indexes.map((userIndex: IndexDescription): ResolvedIndexDescription => {
216249
// Ensure the key is a Map to preserve index key ordering
217250
const key =
218251
userIndex.key instanceof Map ? userIndex.key : new Map(Object.entries(userIndex.key));
219-
const name = userIndex.name != null ? userIndex.name : Array.from(key).flat().join('_');
220-
const validIndexOptions = Object.fromEntries(
221-
Object.entries({ ...userIndex }).filter(([optionName]) =>
222-
VALID_INDEX_OPTIONS.has(optionName)
223-
)
224-
);
252+
const name = userIndex.name ?? Array.from(key).flat().join('_');
253+
const validIndexOptions = resolveIndexDescription(userIndex);
225254
return {
226255
...validIndexOptions,
227256
name,
@@ -243,14 +272,11 @@ export class CreateIndexesOperation extends CommandOperation<string[]> {
243272
parent: OperationParent,
244273
collectionName: string,
245274
indexSpec: IndexSpecification,
246-
options?: CreateIndexesOptions
275+
options: CreateIndexesOptions = {}
247276
): CreateIndexesOperation {
248-
return new CreateIndexesOperation(
249-
parent,
250-
collectionName,
251-
[makeIndexSpec(indexSpec, options)],
252-
options
253-
);
277+
const key = constructIndexDescriptionMap(indexSpec);
278+
const description: IndexDescription = { ...options, key };
279+
return new CreateIndexesOperation(parent, collectionName, [description], options);
254280
}
255281

256282
override get commandName() {

test/integration/index_management.test.ts

+77-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
type MongoClient,
99
MongoServerError
1010
} from '../mongodb';
11-
import { assert as test, setupDatabase } from './shared';
11+
import { assert as test, filterForCommands, setupDatabase } from './shared';
1212

1313
describe('Indexes', function () {
1414
let client: MongoClient;
@@ -160,6 +160,82 @@ describe('Indexes', function () {
160160
});
161161
});
162162

163+
describe('Collection.createIndex()', function () {
164+
const started: CommandStartedEvent[] = [];
165+
beforeEach(() => {
166+
started.length = 0;
167+
client.on('commandStarted', filterForCommands('createIndexes', started));
168+
});
169+
170+
context('when version is not specified as an option', function () {
171+
it('does not attach `v` to the command', async () => {
172+
await collection.createIndex({ age: 1 });
173+
const { command } = started[0];
174+
expect(command).to.exist;
175+
expect(command.indexes[0]).not.to.have.property('v');
176+
});
177+
});
178+
179+
context('when version is specified as an option', function () {
180+
it('attaches `v` to the command with the value of `version`', async () => {
181+
await collection.createIndex({ age: 1 }, { version: 1 });
182+
const { command } = started[0];
183+
expect(command).to.exist;
184+
expect(command.indexes[0]).to.have.property('v', 1);
185+
});
186+
});
187+
});
188+
189+
describe('Collection.createIndexes()', function () {
190+
const started: CommandStartedEvent[] = [];
191+
beforeEach(() => {
192+
started.length = 0;
193+
client.on('commandStarted', filterForCommands('createIndexes', started));
194+
});
195+
196+
context('when version is not specified as an option', function () {
197+
it('does not attach `v` to the command', async () => {
198+
await collection.createIndexes([{ key: { age: 1 } }]);
199+
const { command } = started[0];
200+
expect(command).to.exist;
201+
expect(command.indexes[0]).not.to.have.property('v');
202+
});
203+
});
204+
205+
context('when version is specified as an option', function () {
206+
it('does not attach `v` to the command', async () => {
207+
await collection.createIndexes([{ key: { age: 1 } }], { version: 1 });
208+
const { command } = started[0];
209+
expect(command).to.exist;
210+
expect(command.indexes[0]).not.to.have.property('v', 1);
211+
});
212+
});
213+
214+
context('when version is provided in the index description and the options', function () {
215+
it('the value in the description takes precedence', async () => {
216+
await collection.createIndexes([{ key: { age: 1 }, version: 1 }], { version: 0 });
217+
const { command } = started[0];
218+
expect(command).to.exist;
219+
expect(command.indexes[0]).to.have.property('v', 1);
220+
});
221+
});
222+
223+
context(
224+
'when version is provided in some of the index descriptions and the options',
225+
function () {
226+
it('does not specify a version from the `version` provided in the options', async () => {
227+
await collection.createIndexes([{ key: { age: 1 }, version: 1 }, { key: { date: 1 } }], {
228+
version: 0
229+
});
230+
const { command } = started[0];
231+
expect(command).to.exist;
232+
expect(command.indexes[0]).to.have.property('v', 1);
233+
expect(command.indexes[1]).not.to.have.property('v');
234+
});
235+
}
236+
);
237+
});
238+
163239
describe('Collection.indexExists()', function () {
164240
beforeEach(() => collection.createIndex({ age: 1 }));
165241
afterEach(() => collection.dropIndexes());

0 commit comments

Comments
 (0)