feat(completion): complete mongo dropIndex and hint indexes

This commit is contained in:
Loc Nguyen authored and GitHub committed 2026-10-02 09:17:27 +08:00
1 parent 0f81dcdf40
commit 80bd37ebaa
6 files changed
+296 -7

No files matched your search

@@ -60,6 +60,7 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}, configureMetad
listRedisCompletionKeys: vi.fn(async () => ["user:1"]),
listMongoCompletionCollections: vi.fn(async () => ["users"]),
listMongoCompletionFields: vi.fn(async () => []),
listMongoCompletionIndexes: vi.fn(async () => []),
listElasticsearchCompletionIndices: vi.fn(async () => ["users"]),
listElasticsearchCompletionFields: vi.fn(async () => []),
};
@@ -275,6 +276,21 @@ describe("QueryEditor completion provider ownership", () => {
expect(await accept("db.users.find({ na })", 18)).toEqual({ doc: "db.users.find({ name: })", cursor: 22 });
});
it("completes MongoDB index names in dropIndex and replaces closing quote", async () => {
const doc = 'db.users.dropIndex("")';
const cursor = doc.indexOf('""') + 1;
const { store, provide, currentView } = createHarness({ databaseType: "mongodb", modelValue: doc });
store.listMongoCompletionIndexes.mockResolvedValue([{ name: "email_1", keyPattern: "{ email: 1 }" }]);
currentView.dispatch({ selection: { anchor: cursor } });
const result = await provide();
expect(store.listMongoCompletionIndexes).toHaveBeenCalledWith("connection", "demo", "users");
const option = result?.options.find((candidate) => candidate.displayLabel === "email_1" || candidate.label === "email_1");
expect(option).toBeDefined();
expect(option?.detail).toBe("{ email: 1 }");
(option!.apply as (view: EditorView, completion: unknown, from: number, to: number) => void)(currentView, option, result!.from, cursor);
expect(currentView.state.doc.toString()).toBe('db.users.dropIndex("email_1")');
});
it.each(["redis", "mongodb", "mysql"] as const)("does not query %s metadata without a connection", async (databaseType) => {
const { store, provide } = createHarness({ databaseType, connectionId: undefined });
expect(await provide()).toBeNull();
@@ -20,7 +20,17 @@ import {
import { originForSqlCompletionProvider, originForTypedSqlCompletionStart, shouldAllowSqlCompletionTrigger, type SqlCompletionTriggerFacts, type SqlCompletionTriggerOrigin } from "@/lib/sql/sqlCompletionTriggerPolicy";
import { driverProfileHasCompletionCandidates } from "@/lib/database/driverProfileExtensions";
import { buildElasticsearchCompletionItemsFromContext, elasticsearchCompletionNeedsFields, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionField } from "@/lib/elasticsearch/elasticsearchCompletion";
import { buildMongoCompletionItemsFromContext, foldMongoKeySeparator, getMongoCompletionContext, getMongoCompletionResultValidFor, mongoCompletionNeedsCollections, mongoCompletionNeedsDatabases, mongoCompletionNeedsFields, shouldAutoOpenMongoCompletion } from "@/lib/mongo/mongoCompletion";
import {
buildMongoCompletionItemsFromContext,
foldMongoKeySeparator,
getMongoCompletionContext,
getMongoCompletionResultValidFor,
mongoCompletionNeedsCollections,
mongoCompletionNeedsDatabases,
mongoCompletionNeedsFields,
mongoCompletionNeedsIndexes,
shouldAutoOpenMongoCompletion,
} from "@/lib/mongo/mongoCompletion";
import { buildSoqlCompletionItems, getSoqlCompletionContext, getSoqlCompletionResultValidFor, resolveSoqlFieldCandidates, resolveSoqlValueField, shouldAutoOpenSoqlCompletion, soqlCompletionNeedsObjects, type SoqlCompletionField, type SoqlCompletionObject } from "@/lib/soql/soqlCompletion";
import {
buildSqlServerUseDatabaseCompletionItems,
@@ -572,6 +582,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
let databases: string[] = [];
let collections: string[] = [];
let fields: Awaited<ReturnType<typeof connectionStore.listMongoCompletionFields>> = [];
let indexes: Awaited<ReturnType<typeof connectionStore.listMongoCompletionIndexes>> = [];
// `use` and `getSiblingDB` name a database, which does not depend on the tab having one selected.
if (mongoCompletionNeedsDatabases(completionContext.mode)) {
@@ -598,12 +609,22 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
}
}
const indexDatabase = completionContext.database ?? props.database;
if (indexDatabase && mongoCompletionNeedsIndexes(completionContext.mode) && completionContext.collection) {
try {
indexes = await connectionStore.listMongoCompletionIndexes(props.connectionId, indexDatabase, completionContext.collection);
} catch {
indexes = [];
}
}
if (epoch !== completionEpoch) return null;
const items = buildMongoCompletionItemsFromContext(completionContext, {
databases,
collections,
fields,
indexes,
});
if (items.length === 0) return null;
return {
+61 -2
View File
@@ -47,6 +47,7 @@ export type MongoCompletionMode =
| "pullCondition"
| "fieldPath"
| "fieldRef"
| "indexName"
| "value"
| "valueWrapper"
| "queryOperator"
@@ -74,6 +75,11 @@ export interface MongoCompletionField {
type?: string;
}
export interface MongoCompletionIndex {
name: string;
keyPattern?: string;
}
export interface MongoCompletionItem {
label: string;
type: "column" | "function" | "keyword" | "snippet" | "table";
@@ -117,6 +123,7 @@ export interface MongoCompletionInput {
databases?: string[];
collections?: string[];
fields?: MongoCompletionField[];
indexes?: Array<string | MongoCompletionIndex>;
}
const COLLECTION_METHODS = [
@@ -242,7 +249,7 @@ const ROOT_SNIPPET_BOOST: Record<(typeof ROOT_SNIPPETS)[number]["label"], number
};
/** Role of each positional argument, by collection helper. Drives cursor classification. */
type MongoArgRole = "filter" | "update" | "replacement" | "document" | "documents" | "operations" | "pipeline" | "projection" | "keys" | "sortKeys" | "fieldName" | "options" | "collation" | "verbosity" | "name";
type MongoArgRole = "filter" | "update" | "replacement" | "document" | "documents" | "operations" | "pipeline" | "projection" | "keys" | "sortKeys" | "fieldName" | "indexName" | "options" | "collation" | "verbosity" | "name";
const METHOD_ARG_ROLES: Record<string, readonly MongoArgRole[]> = {
find: ["filter", "projection"],
@@ -267,6 +274,8 @@ const METHOD_ARG_ROLES: Record<string, readonly MongoArgRole[]> = {
sort: ["sortKeys"],
collation: ["collation"],
explain: ["verbosity"],
dropIndex: ["indexName"],
dropIndexes: ["indexName"],
// Database-level helpers whose argument is a document.
createCollection: ["name", "options"],
runCommand: ["options"],
@@ -417,7 +426,7 @@ export function getMongoCompletionContext(text: string, cursor: number): MongoCo
const call = findInnermostMongoCall(beforeCursor);
// Top-level snippets belong at the start of a command. Inside an argument list — of a method
// this engine does not model (`limit(`, `drop(`, `dropIndex("`, `runCommand({`, …) or after a
// this engine does not model (`limit(`, `drop(`, `renameCollection(`, `runCommand({`, …) or after a
// `use` — they are noise: `db.collection.find` is not something you can type there.
if (!call) return isInsideCallArguments(beforeCursor) || isAfterUseKeyword(beforeCursor) || isAfterShowKeyword(beforeCursor) ? at("none") : at("root");
@@ -489,6 +498,9 @@ export function buildMongoCompletionItemsFromContext(context: MongoCompletionCon
case "fieldRef":
items = fieldRefItems(prefix, fields);
break;
case "indexName":
items = indexNameItems(prefix, input.indexes ?? [], context.method);
break;
case "value":
// Shell constructors first; the extended JSON spellings need their own braces here.
items = [...specItems(VALUE_SNIPPETS, prefix, "value", 100), ...specItems(BRACED_EXTENDED_JSON_VALUES, prefix, "extended JSON value", 90)];
@@ -561,6 +573,11 @@ export function mongoCompletionNeedsCollections(mode: MongoCompletionMode): bool
return mode === "collection" || mode === "collectionOrMethod" || mode === "collectionRef";
}
/** Modes whose items are built from the target collection's indexes. */
export function mongoCompletionNeedsIndexes(mode: MongoCompletionMode): boolean {
return mode === "indexName";
}
/** Modes whose items are built from the connection's database names. */
export function mongoCompletionNeedsDatabases(mode: MongoCompletionMode): boolean {
return mode === "database";
@@ -712,6 +729,20 @@ export function inferMongoCompletionFields(documents: unknown[]): MongoCompletio
return [...typeByPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, types]) => ({ name, type: [...types].sort().join(" | ") }));
}
/** Formats MongoDB index keys into a key pattern string, e.g. `{ name: 1, age: -1 }`. */
export function formatMongoIndexKeyPattern(keys?: readonly { field: string; direction: string | number }[] | null): string | undefined {
if (!keys || keys.length === 0) return undefined;
const parts = keys.map((key) => {
let dir: string | number = key.direction;
if (dir === "1" || dir === 1) dir = 1;
else if (dir === "-1" || dir === -1) dir = -1;
else if (typeof dir === "string" && /^-?\d+$/.test(dir)) dir = Number(dir);
else if (typeof dir === "string") dir = JSON.stringify(dir);
return `${key.field}: ${dir}`;
});
return `{ ${parts.join(", ")} }`;
}
/* ------------------------------------------------------------------ *
* Cursor classification
* ------------------------------------------------------------------ */
@@ -863,6 +894,13 @@ function classifyCursorInCall(method: string, scan: MongoCallScan): MongoCursorC
// A bare string argument naming a field, e.g. distinct("category").
case "fieldName":
return { mode: scan.stack.length === 0 ? "fieldPath" : "none" };
case "indexName":
if (method === "dropIndexes") {
const isBare = scan.stack.length === 0;
const isInArray = scan.stack.length === 1 && scan.stack[0].kind === "array";
return isBare || isInArray ? { mode: "indexName", method } : { mode: "none" };
}
return scan.stack.length === 0 ? { mode: "indexName", method } : { mode: "none" };
case "pipeline":
return classifyPipeline(scan);
case "operations":
@@ -1100,6 +1138,7 @@ function classifyMethodOptions(method: string, scan: MongoCallScan): MongoCursor
if (depth === 0) {
if (scan.inValue) {
if (method === "aggregate" && scan.valueKey === "hint") return { mode: "indexName", method };
const enumKey = OPTION_VALUE_ENUMS[scan.valueKey ?? ""];
if (enumKey) return { mode: "enumValue", enumKey, method };
if (OPTION_COLLECTION_KEYS[method]?.has(scan.valueKey ?? "")) return { mode: "collectionRef", method };
@@ -1507,6 +1546,26 @@ function collectionRefItems(prefix: string, collections: string[]): MongoComplet
});
}
function indexNameItems(prefix: string, indexes: Array<string | MongoCompletionIndex>, method?: string): MongoCompletionItem[] {
const normalizedPrefix = normalizeMongoKeyPrefix(prefix);
const normalized = indexes.map((item) => (typeof item === "string" ? { name: item } : item));
const excludeProtected = method === "dropIndex" || method === "dropIndexes";
const candidates = excludeProtected ? normalized.filter((idx) => idx.name !== "_id_") : normalized;
return dedupeAndSort(
candidates
.filter((idx) => matchesFuzzyPrefix(idx.name, normalizedPrefix))
.slice(0, 100)
.map((idx) => ({
label: idx.name,
type: "keyword" as const,
detail: idx.keyPattern || undefined,
apply: quoteMongoString(idx.name, prefix),
boost: startsWithPrefix(idx.name, normalizedPrefix) ? 120 : 90,
})),
);
}
function methodItems(prefix: string): MongoCompletionItem[] {
return dedupeAndSort(
COLLECTION_METHODS.filter((method) => matchesFuzzyPrefix(method.label, prefix)).map((method) => ({
@@ -1239,6 +1239,57 @@ describe("connectionStore completion assistant", () => {
expect(getColumns.mock.calls.map((call) => call[3])).toEqual(["users", "orders", "users"]);
});
it("caches Mongo completion indexes and invalidates them via invalidateCompletionTableCache and invalidateCompletionCache", async () => {
const mongoListIndexSpecs = vi.fn().mockResolvedValue([
{
name: "email_1",
keys: [{ field: "email", direction: "1" }],
is_unique: true,
is_primary: false,
is_sparse: false,
expire_after_seconds: null,
partial_filter_expression: null,
background: false,
bucket_size: null,
hidden: false,
properties_complete: true,
extra_options: null,
},
]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
mongoListIndexSpecs,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [
{
...postgresConnection(),
id: "mongo-1",
name: "MongoDB",
db_type: "mongodb",
},
];
store.connectedIds.add("mongo-1");
const first = await store.listMongoCompletionIndexes("mongo-1", "app", "users");
expect(first).toEqual([{ name: "email_1", keyPattern: "{ email: 1 }" }]);
const second = await store.listMongoCompletionIndexes("mongo-1", "app", "users");
expect(second).toEqual([{ name: "email_1", keyPattern: "{ email: 1 }" }]);
expect(mongoListIndexSpecs).toHaveBeenCalledTimes(1);
expect(store.invalidateCompletionTableCache("mongo-1", "app", "users")).toBeGreaterThan(0);
await store.listMongoCompletionIndexes("mongo-1", "app", "users");
expect(mongoListIndexSpecs).toHaveBeenCalledTimes(2);
store.invalidateCompletionCache("mongo-1", "app");
await store.listMongoCompletionIndexes("mongo-1", "app", "users");
expect(mongoListIndexSpecs).toHaveBeenCalledTimes(3);
});
it("does not let an invalidated column request overwrite fresh metadata", async () => {
const staleColumns = deferred<unknown[]>();
const freshColumns = deferred<unknown[]>();
+26 -3
View File
@@ -166,7 +166,7 @@ import { useSettingsStore } from "@/stores/settingsStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { decorateDatabaseSavedSqlTreeNodes, indexSavedSqlFilesByDatabase, stripDatabaseSavedSqlTreeNodes, withDatabaseSavedSqlRoot } from "@/lib/savedSql/savedSqlDatabaseTree";
import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/database/sqlServerLinkedServers";
import { inferMongoCompletionFields, type MongoCompletionField } from "@/lib/mongo/mongoCompletion";
import { formatMongoIndexKeyPattern, inferMongoCompletionFields, type MongoCompletionField, type MongoCompletionIndex } from "@/lib/mongo/mongoCompletion";
import type { SoqlCompletionField, SoqlCompletionObject } from "@/lib/soql/soqlCompletion";
import type { SalesforceCurrentUser } from "@/types/salesforce";
import { flattenElasticsearchMappingFields, type ElasticsearchCompletionField } from "@/lib/elasticsearch/elasticsearchCompletion";
@@ -537,6 +537,7 @@ export const useConnectionStore = defineStore("connection", () => {
const redisCommandDocsCacheGeneration = new Map<string, number>();
const mongoCompletionCollectionsCache = ref<Record<string, string[]>>({});
const mongoCompletionFieldsCache = ref<Record<string, MongoCompletionField[]>>({});
const mongoCompletionIndexesCache = ref<Record<string, MongoCompletionIndex[]>>({});
const soqlCompletionObjectsCache = ref<Record<string, SoqlCompletionObject[]>>({});
const soqlCompletionFieldsCache = ref<Record<string, SoqlCompletionField[]>>({});
// One entry per connection: the authenticated Salesforce user never changes for
@@ -3940,6 +3941,9 @@ export const useConnectionStore = defineStore("connection", () => {
for (const key of Object.keys(mongoCompletionFieldsCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete mongoCompletionFieldsCache.value[key];
}
for (const key of Object.keys(mongoCompletionIndexesCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete mongoCompletionIndexesCache.value[key];
}
for (const key of Object.keys(soqlCompletionObjectsCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete soqlCompletionObjectsCache.value[key];
}
@@ -7901,7 +7905,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (!normalizedKey.startsWith(prefix)) return false;
const tableToken = `:${tableName.toLowerCase()}`;
const tableOffset = normalizedKey.lastIndexOf(tableToken);
if (tableOffset < prefix.length) return false;
if (tableOffset < prefix.length - 1) return false;
const trailing = normalizedKey.slice(tableOffset + tableToken.length);
if (trailing && !trailing.startsWith(":")) return false;
const normalizedSchema = schema?.trim().toLowerCase();
@@ -7919,7 +7923,7 @@ export const useConnectionStore = defineStore("connection", () => {
bumpCompletionCacheRevision(connectionId, database);
const matches = (key: string) => completionTableCacheKeyMatches(key, connectionId, database, tableName, schema, catalog);
let removed = 0;
for (const cache of [completionColumnsCache.value, completionForeignKeysCache.value]) {
for (const cache of [completionColumnsCache.value, completionForeignKeysCache.value, mongoCompletionFieldsCache.value, mongoCompletionIndexesCache.value]) {
for (const key of Object.keys(cache)) {
if (!matches(key)) continue;
delete cache[key];
@@ -8772,6 +8776,24 @@ export const useConnectionStore = defineStore("connection", () => {
});
}
async function listMongoCompletionIndexes(connectionId: string, database: string, collection: string): Promise<MongoCompletionIndex[]> {
if (!database || !collection) return [];
const cacheKey = `${connectionId}:${database}:${collection}`;
const cached = mongoCompletionIndexesCache.value[cacheKey];
if (cached) return cached;
return withCompletionInFlight(`${cacheKey}:mongo-indexes`, async () => {
await ensureConnected(connectionId);
const specs = (await api.mongoListIndexSpecs(connectionId, database, collection)) ?? [];
const indexes = specs.map((spec) => ({
name: spec.name,
keyPattern: formatMongoIndexKeyPattern(spec.keys),
}));
mongoCompletionIndexesCache.value[cacheKey] = indexes;
evictOldestCacheEntries(mongoCompletionIndexesCache.value, COMPLETION_CACHE_MAX);
return indexes;
});
}
// Map a Salesforce describe column to a SOQL completion field. The backend packs
// relationshipName / referenceTo / label into ColumnInfo.extra (JSON) and active
// picklist values into enum_values; parse defensively since extra may be absent.
@@ -10330,6 +10352,7 @@ export const useConnectionStore = defineStore("connection", () => {
listRedisCompletionCommandDocs,
listMongoCompletionCollections,
listMongoCompletionFields,
listMongoCompletionIndexes,
listSoqlCompletionObjects,
listSoqlCompletionFields,
loadSalesforceCurrentUser,
+120 -1
View File
@@ -1,6 +1,16 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { buildMongoCompletionItems, buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, getMongoDocumentQueryCompletionContext, inferMongoCompletionFields, shouldAutoOpenMongoCompletion } from "../../apps/desktop/src/lib/mongo/mongoCompletion.ts";
import {
buildMongoCompletionItems,
buildMongoCompletionItemsFromContext,
formatMongoIndexKeyPattern,
getMongoCompletionContext,
getMongoCompletionResultValidFor,
getMongoDocumentQueryCompletionContext,
inferMongoCompletionFields,
mongoCompletionNeedsIndexes,
shouldAutoOpenMongoCompletion,
} from "../../apps/desktop/src/lib/mongo/mongoCompletion.ts";
import {
ACCUMULATORS,
BULK_WRITE_OPERATION_FIELDS,
@@ -984,6 +994,115 @@ test("completes both arguments of distinct", () => {
assert.ok(labels('db.users.distinct("name", { age: { $g', { fields }).includes("$gte"));
});
test("completes index names in dropIndex, dropIndexes and aggregate hint", () => {
const sampleIndexes = [
{ name: "_id_", keyPattern: "{ _id: 1 }" },
{ name: "email_1", keyPattern: "{ email: 1 }" },
{ name: "name_age_idx", keyPattern: "{ name: 1, age: -1 }" },
];
// Needs-helper correctly identifies indexName mode.
assert.equal(mongoCompletionNeedsIndexes("indexName"), true);
assert.equal(mongoCompletionNeedsIndexes("collection"), false);
assert.equal(mongoCompletionNeedsIndexes("none"), false);
// dropIndex excludes _id_ and completes index names of the collection.
const dropIndexText = 'db.users.dropIndex("';
const dropIndexCtx = getMongoCompletionContext(dropIndexText, dropIndexText.length);
assert.equal(dropIndexCtx.mode, "indexName");
assert.equal(dropIndexCtx.collection, "users");
assert.equal(dropIndexCtx.prefix, '"');
const dropIndexItems = buildMongoCompletionItems(dropIndexText, dropIndexText.length, { indexes: sampleIndexes });
assert.deepEqual(
dropIndexItems.map((item) => item.label),
["email_1", "name_age_idx"],
);
assert.equal(
dropIndexItems.find((i) => i.label === "_id_"),
undefined,
);
assert.equal(dropIndexItems.find((i) => i.label === "name_age_idx")?.detail, "{ name: 1, age: -1 }");
assert.equal(dropIndexItems[0]?.apply, '"email_1"');
// dropIndex handles string-only index inputs as well.
const stringIndexItems = buildMongoCompletionItems(dropIndexText, dropIndexText.length, { indexes: ["_id_", "username_1"] });
assert.deepEqual(
stringIndexItems.map((item) => item.label),
["username_1"],
);
// dropIndex replaces existing closing quote when completing an emptied quote pair.
const dropIndexEmpty = 'db.users.dropIndex("")';
const dropIndexEmptyCursor = dropIndexEmpty.indexOf('""') + 1;
const dropIndexEmptyContext = getMongoCompletionContext(dropIndexEmpty, dropIndexEmptyCursor);
const dropIndexEmptyItem = buildMongoCompletionItems(dropIndexEmpty, dropIndexEmptyCursor, { indexes: sampleIndexes }).find((i) => i.label === "email_1");
assert.equal(dropIndexEmptyContext.replaceClosingQuote, '"');
assert.equal(dropIndexEmptyItem?.replaceClosingQuote, '"');
assert.equal(dropIndexEmpty.slice(0, dropIndexEmptyContext.from) + dropIndexEmptyItem?.apply + dropIndexEmpty.slice(dropIndexEmptyCursor + 1), 'db.users.dropIndex("email_1")');
// dropIndexes (bare string argument) excludes _id_.
const dropIndexesBareText = 'db.users.dropIndexes("';
const dropIndexesBareCtx = getMongoCompletionContext(dropIndexesBareText, dropIndexesBareText.length);
assert.equal(dropIndexesBareCtx.mode, "indexName");
assert.equal(dropIndexesBareCtx.collection, "users");
const dropIndexesBareItems = buildMongoCompletionItems(dropIndexesBareText, dropIndexesBareText.length, { indexes: sampleIndexes });
assert.deepEqual(
dropIndexesBareItems.map((item) => item.label),
["email_1", "name_age_idx"],
);
// dropIndexes (array of string index names) excludes _id_.
const dropIndexesArrayText = 'db.users.dropIndexes(["';
const dropIndexesArrayCtx = getMongoCompletionContext(dropIndexesArrayText, dropIndexesArrayText.length);
assert.equal(dropIndexesArrayCtx.mode, "indexName");
assert.equal(dropIndexesArrayCtx.collection, "users");
const dropIndexesArrayItems = buildMongoCompletionItems(dropIndexesArrayText, dropIndexesArrayText.length, { indexes: sampleIndexes });
assert.deepEqual(
dropIndexesArrayItems.map((item) => item.label),
["email_1", "name_age_idx"],
);
// Subsequent items in dropIndexes array also complete index names.
const dropIndexesNextArrayText = 'db.users.dropIndexes(["email_1", "';
const dropIndexesNextArrayCtx = getMongoCompletionContext(dropIndexesNextArrayText, dropIndexesNextArrayText.length);
assert.equal(dropIndexesNextArrayCtx.mode, "indexName");
assert.equal(dropIndexesNextArrayCtx.collection, "users");
const dropIndexesNextItems = buildMongoCompletionItems(dropIndexesNextArrayText, dropIndexesNextArrayText.length, { indexes: sampleIndexes });
assert.deepEqual(
dropIndexesNextItems.map((item) => item.label),
["email_1", "name_age_idx"],
);
// aggregate({ hint: "..." }) includes _id_ (hinting _id_ is allowed in MongoDB).
const aggHintText = 'db.users.aggregate([], { hint: "';
const aggHintCtx = getMongoCompletionContext(aggHintText, aggHintText.length);
assert.equal(aggHintCtx.mode, "indexName");
assert.equal(aggHintCtx.collection, "users");
const aggHintItems = buildMongoCompletionItems(aggHintText, aggHintText.length, { indexes: sampleIndexes });
assert.deepEqual(
aggHintItems.map((item) => item.label),
["_id_", "email_1", "name_age_idx"],
);
assert.equal(aggHintItems.find((i) => i.label === "_id_")?.detail, "{ _id: 1 }");
// find().hint() is NOT in find chain and does not offer index completion.
const findHintText = 'db.users.find({}).hint("';
assert.notEqual(getMongoCompletionContext(findHintText, findHintText.length).mode, "indexName");
// Key pattern formatter formats directions properly.
assert.equal(
formatMongoIndexKeyPattern([
{ field: "name", direction: "1" },
{ field: "age", direction: "-1" },
]),
"{ name: 1, age: -1 }",
);
assert.equal(formatMongoIndexKeyPattern([{ field: "loc", direction: "2dsphere" }]), '{ loc: "2dsphere" }');
assert.equal(formatMongoIndexKeyPattern([]), undefined);
assert.equal(formatMongoIndexKeyPattern(null), undefined);
});
test("completes database names after use and inside getSiblingDB", () => {
const databases = ["orders", "order_archive", "admin", "local"];