Skip to content

Commit 0754bf9

Browse files
feat(NODE-3517): improve index spec handling and type definitions (#3315)
Co-authored-by: Neal Beeken <[email protected]>
1 parent 8ecbabc commit 0754bf9

File tree

13 files changed

+581
-341
lines changed

13 files changed

+581
-341
lines changed

src/cmap/connection.ts

+22
Original file line numberDiff line numberDiff line change
@@ -594,11 +594,33 @@ export class CryptoConnection extends Connection {
594594
return;
595595
}
596596

597+
// Save sort or indexKeys based on the command being run
598+
// the encrypt API serializes our JS objects to BSON to pass to the native code layer
599+
// and then deserializes the encrypted result, the protocol level components
600+
// of the command (ex. sort) are then converted to JS objects potentially losing
601+
// import key order information. These fields are never encrypted so we can save the values
602+
// from before the encryption and replace them after encryption has been performed
603+
const sort: Map<string, number> | null = cmd.find || cmd.findAndModify ? cmd.sort : null;
604+
const indexKeys: Map<string, number>[] | null = cmd.createIndexes
605+
? cmd.indexes.map((index: { key: Map<string, number> }) => index.key)
606+
: null;
607+
597608
autoEncrypter.encrypt(ns.toString(), cmd, options, (err, encrypted) => {
598609
if (err || encrypted == null) {
599610
callback(err, null);
600611
return;
601612
}
613+
614+
// Replace the saved values
615+
if (sort != null && (cmd.find || cmd.findAndModify)) {
616+
encrypted.sort = sort;
617+
}
618+
if (indexKeys != null && cmd.createIndexes) {
619+
for (const [offset, index] of indexKeys.entries()) {
620+
encrypted.indexes[offset].key = index;
621+
}
622+
}
623+
602624
super.command(ns, encrypted, options, (err, response) => {
603625
if (err || response == null) {
604626
callback(err, response);

src/operations/indexes.ts

+52-52
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { OneOrMore } from '../mongo_types';
66
import { ReadPreference } from '../read_preference';
77
import type { Server } from '../sdam/server';
88
import type { ClientSession } from '../sessions';
9-
import { Callback, maxWireVersion, MongoDBNamespace, parseIndexOptions } from '../utils';
9+
import { Callback, isObject, maxWireVersion, MongoDBNamespace } from '../utils';
1010
import {
1111
CollationOptions,
1212
CommandOperation,
@@ -51,14 +51,17 @@ const VALID_INDEX_OPTIONS = new Set([
5151

5252
/** @public */
5353
export type IndexDirection = -1 | 1 | '2d' | '2dsphere' | 'text' | 'geoHaystack' | number;
54-
54+
function isIndexDirection(x: unknown): x is IndexDirection {
55+
return (
56+
typeof x === 'number' || x === '2d' || x === '2dsphere' || x === 'text' || x === 'geoHaystack'
57+
);
58+
}
5559
/** @public */
5660
export type IndexSpecification = OneOrMore<
5761
| string
5862
| [string, IndexDirection]
5963
| { [key: string]: IndexDirection }
60-
| [string, IndexDirection][]
61-
| { [key: string]: IndexDirection }[]
64+
| Map<string, IndexDirection>
6265
>;
6366

6467
/** @public */
@@ -86,7 +89,7 @@ export interface IndexDescription
8689
> {
8790
collation?: CollationOptions;
8891
name?: string;
89-
key: Document;
92+
key: { [key: string]: IndexDirection } | Map<string, IndexDirection>;
9093
}
9194

9295
/** @public */
@@ -130,23 +133,37 @@ export interface CreateIndexesOptions extends CommandOperationOptions {
130133
hidden?: boolean;
131134
}
132135

133-
function makeIndexSpec(indexSpec: IndexSpecification, options: any): IndexDescription {
134-
const indexParameters = parseIndexOptions(indexSpec);
135-
136-
// Generate the index name
137-
const name = typeof options.name === 'string' ? options.name : indexParameters.name;
138-
139-
// Set up the index
140-
const finalIndexSpec: Document = { name, key: indexParameters.fieldHash };
136+
function isSingleIndexTuple(t: unknown): t is [string, IndexDirection] {
137+
return Array.isArray(t) && t.length === 2 && isIndexDirection(t[1]);
138+
}
141139

142-
// merge valid index options into the index spec
143-
for (const optionName in options) {
144-
if (VALID_INDEX_OPTIONS.has(optionName)) {
145-
finalIndexSpec[optionName] = options[optionName];
140+
function makeIndexSpec(
141+
indexSpec: IndexSpecification,
142+
options?: CreateIndexesOptions
143+
): IndexDescription {
144+
const key: Map<string, IndexDirection> = new Map();
145+
146+
const indexSpecs =
147+
!Array.isArray(indexSpec) || isSingleIndexTuple(indexSpec) ? [indexSpec] : indexSpec;
148+
149+
// Iterate through array and handle different types
150+
for (const spec of indexSpecs) {
151+
if (typeof spec === 'string') {
152+
key.set(spec, 1);
153+
} else if (Array.isArray(spec)) {
154+
key.set(spec[0], spec[1] ?? 1);
155+
} else if (spec instanceof Map) {
156+
for (const [property, value] of spec) {
157+
key.set(property, value);
158+
}
159+
} else if (isObject(spec)) {
160+
for (const [property, value] of Object.entries(spec)) {
161+
key.set(property, value);
162+
}
146163
}
147164
}
148165

149-
return finalIndexSpec as IndexDescription;
166+
return { ...options, key };
150167
}
151168

152169
/** @internal */
@@ -183,7 +200,7 @@ export class CreateIndexesOperation<
183200
> extends CommandOperation<T> {
184201
override options: CreateIndexesOptions;
185202
collectionName: string;
186-
indexes: IndexDescription[];
203+
indexes: ReadonlyArray<Omit<IndexDescription, 'key'> & { key: Map<string, IndexDirection> }>;
187204

188205
constructor(
189206
parent: OperationParent,
@@ -195,8 +212,22 @@ export class CreateIndexesOperation<
195212

196213
this.options = options ?? {};
197214
this.collectionName = collectionName;
198-
199-
this.indexes = indexes;
215+
this.indexes = indexes.map(userIndex => {
216+
// Ensure the key is a Map to preserve index key ordering
217+
const key =
218+
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+
);
225+
return {
226+
...validIndexOptions,
227+
name,
228+
key
229+
};
230+
});
200231
}
201232

202233
override execute(
@@ -209,31 +240,6 @@ export class CreateIndexesOperation<
209240

210241
const serverWireVersion = maxWireVersion(server);
211242

212-
// Ensure we generate the correct name if the parameter is not set
213-
for (let i = 0; i < indexes.length; i++) {
214-
// Did the user pass in a collation, check if our write server supports it
215-
if (indexes[i].collation && serverWireVersion < 5) {
216-
callback(
217-
new MongoCompatibilityError(
218-
`Server ${server.name}, which reports wire version ${serverWireVersion}, ` +
219-
'does not support collation'
220-
)
221-
);
222-
return;
223-
}
224-
225-
if (indexes[i].name == null) {
226-
const keys = [];
227-
228-
for (const name in indexes[i].key) {
229-
keys.push(`${name}_${indexes[i].key[name]}`);
230-
}
231-
232-
// Set the name
233-
indexes[i].name = keys.join('_');
234-
}
235-
}
236-
237243
const cmd: Document = { createIndexes: this.collectionName, indexes };
238244

239245
if (options.commitQuorum != null) {
@@ -271,12 +277,6 @@ export class CreateIndexOperation extends CreateIndexesOperation<string> {
271277
indexSpec: IndexSpecification,
272278
options?: CreateIndexesOptions
273279
) {
274-
// createIndex can be called with a variety of styles:
275-
// coll.createIndex('a');
276-
// coll.createIndex({ a: 1 });
277-
// coll.createIndex([['a', 1]]);
278-
// createIndexes is always called with an array of index spec objects
279-
280280
super(parent, collectionName, [makeIndexSpec(indexSpec, options)], options);
281281
}
282282
override execute(

src/utils.ts

-58
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
import type { Explain } from './explain';
2424
import type { MongoClient } from './mongo_client';
2525
import type { CommandOperationOptions, OperationParent } from './operations/command';
26-
import type { IndexDirection, IndexSpecification } from './operations/indexes';
2726
import type { Hint, OperationOptions } from './operations/operation';
2827
import { PromiseProvider } from './promise_provider';
2928
import { ReadConcern } from './read_concern';
@@ -104,63 +103,6 @@ export function normalizeHintField(hint?: Hint): Hint | undefined {
104103
return finalHint;
105104
}
106105

107-
interface IndexOptions {
108-
name: string;
109-
keys?: string[];
110-
fieldHash: Document;
111-
}
112-
113-
/**
114-
* Create an index specifier based on
115-
* @internal
116-
*/
117-
export function parseIndexOptions(indexSpec: IndexSpecification): IndexOptions {
118-
const fieldHash: { [key: string]: IndexDirection } = {};
119-
const indexes = [];
120-
let keys;
121-
122-
// Get all the fields accordingly
123-
if ('string' === typeof indexSpec) {
124-
// 'type'
125-
indexes.push(indexSpec + '_' + 1);
126-
fieldHash[indexSpec] = 1;
127-
} else if (Array.isArray(indexSpec)) {
128-
indexSpec.forEach((f: any) => {
129-
if ('string' === typeof f) {
130-
// [{location:'2d'}, 'type']
131-
indexes.push(f + '_' + 1);
132-
fieldHash[f] = 1;
133-
} else if (Array.isArray(f)) {
134-
// [['location', '2d'],['type', 1]]
135-
indexes.push(f[0] + '_' + (f[1] || 1));
136-
fieldHash[f[0]] = f[1] || 1;
137-
} else if (isObject(f)) {
138-
// [{location:'2d'}, {type:1}]
139-
keys = Object.keys(f);
140-
keys.forEach(k => {
141-
indexes.push(k + '_' + (f as AnyOptions)[k]);
142-
fieldHash[k] = (f as AnyOptions)[k];
143-
});
144-
} else {
145-
// undefined (ignore)
146-
}
147-
});
148-
} else if (isObject(indexSpec)) {
149-
// {location:'2d', type:1}
150-
keys = Object.keys(indexSpec);
151-
Object.entries(indexSpec).forEach(([key, value]) => {
152-
indexes.push(key + '_' + value);
153-
fieldHash[key] = value;
154-
});
155-
}
156-
157-
return {
158-
name: indexes.join('_'),
159-
keys: keys,
160-
fieldHash: fieldHash
161-
};
162-
}
163-
164106
const TO_STRING = (object: unknown) => Object.prototype.toString.call(object);
165107
/**
166108
* Checks if arg is an Object:

0 commit comments

Comments
 (0)