quosdolorem
Version:
The MongoDB Shell
239 lines (210 loc) • 9.48 kB
text/typescript
/* eslint complexity: 0, camelcase: 0, no-nested-ternary: 0 */
import { signatures as shellSignatures, Topologies, TypeSignature } from '@mongosh/shell-api';
import semver from 'semver';
import {
CONVERSION_OPERATORS,
EXPRESSION_OPERATORS,
STAGE_OPERATORS,
QUERY_OPERATORS,
ACCUMULATORS,
BSON_TYPES,
ATLAS,
ADL,
ON_PREM
} from 'mongodb-ace-autocompleter';
type TypeSignatureAttributes = { [key: string]: TypeSignature };
export interface AutocompleteParameters {
topology: () => Topologies;
connectionInfo: () => undefined | {
is_atlas: boolean;
is_data_lake: boolean;
server_version: string;
},
getCollectionCompletionsForCurrentDb: (collName: string) => string[] | Promise<string[]>;
getDatabaseCompletions: (dbName: string) => string[] | Promise<string[]>;
}
export const BASE_COMPLETIONS = EXPRESSION_OPERATORS.concat(
CONVERSION_OPERATORS.concat(BSON_TYPES.concat(STAGE_OPERATORS)
));
export const MATCH_COMPLETIONS = QUERY_OPERATORS.concat(BSON_TYPES);
/**
* The project stage operator.
*/
const PROJECT = '$project';
/**
* The group stage operator.
*/
const GROUP = '$group';
/**
* Return complete suggestions given currently typed line
*
* @param {AutocompleteParameters} params - Relevant information about the current connection.
* @param {string} line - Current user input.
*
* @returns {array} Matching Completions, Current User Input.
*/
async function completer(params: AutocompleteParameters, line: string): Promise<[string[], string, 'exclusive'] | [string[], string]> {
const SHELL_COMPLETIONS = shellSignatures.ShellApi.attributes as TypeSignatureAttributes;
const COLL_COMPLETIONS = shellSignatures.Collection.attributes as TypeSignatureAttributes;
const DB_COMPLETIONS = shellSignatures.Database.attributes as TypeSignatureAttributes;
const AGG_CURSOR_COMPLETIONS = shellSignatures.AggregationCursor.attributes as TypeSignatureAttributes;
const COLL_CURSOR_COMPLETIONS = shellSignatures.Cursor.attributes as TypeSignatureAttributes;
const RS_COMPLETIONS = shellSignatures.ReplicaSet.attributes as TypeSignatureAttributes;
const CONFIG_COMPLETIONS = shellSignatures.ShellConfig.attributes as TypeSignatureAttributes;
const SHARD_COMPLETE = shellSignatures.Shard.attributes as TypeSignatureAttributes;
const splitLineWhitespace = line.split(' ');
const command = splitLineWhitespace[0];
if (SHELL_COMPLETIONS[command]?.isDirectShellCommand) {
// If we encounter a direct shell commmand, we know that we want completions
// specific to that command, so we set the 'exclusive' flag on the result.
// If the shell API provides us with a completer, use it.
const completer = SHELL_COMPLETIONS[command].shellCommandCompleter;
if (completer) {
if (splitLineWhitespace.length === 1) {
splitLineWhitespace.push(''); // Treat e.g. 'show' like 'show '.
}
const hits = await completer(params, splitLineWhitespace) || [];
// Adjust to full input, because `completer` only completed the last item
// in the line, e.g. ['profile'] -> ['show profile']
const fullLineHits = hits.map(hit => [...splitLineWhitespace.slice(0, -1), hit].join(' '));
return [fullLineHits, line, 'exclusive'];
}
return [[line], line, 'exclusive'];
}
// keep initial line param intact to always return in return statement
// check for contents of line with:
const splitLine = line.split('.');
const firstLineEl = splitLine[0];
const elToComplete = splitLine[splitLine.length - 1];
if (splitLine.length <= 1) {
const hits = filterShellAPI(params, SHELL_COMPLETIONS, elToComplete);
return [hits.length ? hits : [], line];
} else if (firstLineEl.match(/\bdb\b/) && splitLine.length === 2) {
// We're seeing something like 'db.foo' and expand that to all methods on
// db which start with 'foo' and all collections on the current db that
// start with 'foo'.
const hits = filterShellAPI(params, DB_COMPLETIONS, elToComplete, splitLine);
const colls = await params.getCollectionCompletionsForCurrentDb(elToComplete.trim());
hits.push(...colls.map(coll => `${splitLine[0]}.${coll}`));
return [hits.length ? hits : [], line];
} else if (firstLineEl.match(/\bdb\b/) && splitLine.length > 2) {
if (!splitLine[1].match(/^\s*\w+\s*$/) && !splitLine[1].match(/\bgetCollection\b/)) {
// The collection name contains something that is not whitespace or an
// alphanumeric character. This could be a function call, for example.
// In any case, we can't currently provide reasonable autocompletion
// suggestions for this.
return [[], line];
}
if (splitLine.length > 3) {
// We're seeing something like db.coll.find().xyz or db.coll.aggregate().xyz
if (splitLine[2].match(/\baggregate\b/)) {
// aggregation cursor completions
const hits = filterShellAPI(
params, AGG_CURSOR_COMPLETIONS, elToComplete, splitLine);
return [hits.length ? hits : [], line];
} else if (splitLine[2].match(/\bfind\b/)) {
// collection cursor completions
const hits = filterShellAPI(
params, COLL_CURSOR_COMPLETIONS, elToComplete, splitLine);
return [hits.length ? hits : [], line];
}
// This is something else, and we currently don't know what this is.
return [[], line];
}
// complete aggregation and collection queries/stages
if (splitLine[2].includes('([') || splitLine[2].includes('({')) {
let expressions;
if (splitLine[2].match(/\baggregate\b/)) {
// aggregation needs extra accumulators to autocomplete properly
expressions = BASE_COMPLETIONS.concat(getStageAccumulators(
params, elToComplete));
} else {
// collection querying just needs MATCH COMPLETIONS
expressions = MATCH_COMPLETIONS;
}
// split on {, as a stage/query will always follow an open curly brace
const splitQuery = line.split('{');
const prefix = splitQuery.pop()?.trim();
const command: string = prefix ? line.split(prefix).shift() as string : line;
const hits = filterQueries(params, expressions, prefix || '', command);
return [hits.length ? hits : [], line];
}
const hits = filterShellAPI(
params, COLL_COMPLETIONS, elToComplete, splitLine);
return [hits.length ? hits : [], line];
} else if (firstLineEl.match(/\bsh\b/) && splitLine.length === 2) {
const hits = filterShellAPI(
params, SHARD_COMPLETE, elToComplete, splitLine);
return [hits.length ? hits : [], line];
} else if (firstLineEl.match(/\brs\b/) && splitLine.length === 2) {
const hits = filterShellAPI(
params, RS_COMPLETIONS, elToComplete, splitLine);
return [hits.length ? hits : [], line];
} else if (firstLineEl.match(/\bconfig\b/) && splitLine.length === 2) {
const hits = filterShellAPI(
params, CONFIG_COMPLETIONS, elToComplete, splitLine);
return [hits.length ? hits : [], line];
}
return [[], line];
}
function isAcceptable(
params: AutocompleteParameters,
entry: { version?: string; projectVersion?: string; env?: string[]; },
versionKey: 'version' | 'projectVersion') {
const connectionInfo = params.connectionInfo();
const isAcceptableVersion =
!entry[versionKey] ||
!connectionInfo ||
semver.gte(connectionInfo.server_version, entry[versionKey] as string);
const isAcceptableEnvironment =
!entry.env ||
!connectionInfo ||
(connectionInfo.is_data_lake ? entry.env.includes(ADL) :
connectionInfo.is_atlas ? entry.env.includes(ATLAS) :
entry.env.includes(ON_PREM));
return isAcceptableVersion && isAcceptableEnvironment;
}
// stage completions based on current stage string.
function getStageAccumulators(params: AutocompleteParameters, stage: string): typeof ACCUMULATORS {
if (stage !== '') return [];
if (stage.includes(PROJECT)) {
return ACCUMULATORS.filter((acc: any) => {
return isAcceptable(params, acc, 'projectVersion');
});
} else if (stage.includes(GROUP)) {
return ACCUMULATORS;
}
}
function filterQueries(params: AutocompleteParameters, completions: any, prefix: string, split: string): string[] {
const hits: any[] = completions.filter((e: any) => {
return e.name && e.name.startsWith(prefix) && isAcceptable(params, e, 'version');
});
return hits.map(h => `${split}${h.name}`);
}
function filterShellAPI(
params: AutocompleteParameters,
completions: { [key: string]: TypeSignature },
prefix: string,
split?: string[]): string[] {
const hits: string[] = Object.keys(completions).filter((c: string) => {
if (!c.startsWith(prefix)) return false;
if (completions[c].deprecated) return false;
const serverVersion = params.connectionInfo()?.server_version;
if (!serverVersion) return true;
const acceptableVersions = completions[c].serverVersions;
const isAcceptableVersion =
!acceptableVersions ||
(semver.gte(serverVersion, acceptableVersions[0]) &&
semver.lte(serverVersion, acceptableVersions[1]));
const acceptableTopologies = completions[c].topologies;
const isAcceptableTopology =
!acceptableTopologies ||
acceptableTopologies.includes(params.topology());
return isAcceptableVersion && isAcceptableTopology;
});
if (split) {
return hits.map(h => `${split.slice(0, -1).join('.')}.${h}`);
}
return hits;
}
export default completer;