MongoDB Shell (mongosh)

repository·main·Indexed 18 days ago

https://github.com/mongodb-js/mongosh

The MongoDB Shell is a monorepo containing components for the shell across REPL, Browser, and Compass environments. It includes packages such as @mongosh/autocomplete for command completions, @mongosh/async-rewriter2 for implicit awaiting of Promises, and a React-based <Shell /> component for embedding the interface into web applications. The project also provides specialized runtimes like IframeRuntime and ElectronRuntime, as well as CLI authentication, TLS, and OIDC options.

Tokens
48.4K
Snippets
147
Records
213
Agent score
14%

What's inside mongosh

  1. How CLI REPL code evaluation works

    main

    When a user enters a command in the mongosh CLI REPL (e.g., db.coll.find({})) and presses enter, the code undergoes a multi-stage transformation and execution process involving several components:

    1. Async REPL: A customized REPLServer.eval function intercepts the input to manage interrupts and error handling.
    2. Mongosh Node REPL: Acts as the orchestrator, managing the Shell API abstraction and history, then forwarding input to the Shell Evaluator.
    3. Shell Evaluator: Determines if the input is a special shell command (like show dbs) or JavaScript code.
      • Shell Commands: Executed immediately without further transformation.
      • JavaScript Code: Passed to the Async Rewriter.
    4. Async Rewriter: Transforms synchronous-style JavaScript into asynchronous code by automatically inserting await statements for Shell API promises.
    5. Final Evaluation: The rewritten code is evaluated in the Shell API context using the original REPLServer.eval function.
  2. How mongosh handles interrupting asynchronous execution

    main

    In a standard Node.js REPL, CTRL-C only interrupts synchronous code (like while(true) {}). Once code becomes asynchronous (e.g., using await, Promises, or timeouts), the standard interrupt handling fails to stop execution.

    mongosh solves this by:

    1. Wrapping Shell API functions: Every Shell API function is wrapped in a Promise.race. This race pits the actual API call against an interruptPromise. If CTRL-C is pressed, the interruptPromise rejects, causing the API call to fail immediately.
    2. Force-closing connections: When a SIGINT is received, mongosh force-closes all MongoClient instances. This terminates operations currently running on the MongoDB server (requires MongoDB > 4.2).
    3. Preventing error swallowing: To prevent user try-catch blocks from accidentally catching the interrupt signal and continuing execution, the async-rewriter rewrites user code to ensure that MongoshInterruptedError is immediately re-thrown and cannot be caught by standard error handlers.
  3. Understand the mechanism of Shell API interruption

    main

    To ensure asynchronous Shell API calls (like db.collection.find()) are interrupted when the user hits CTRL-C, mongosh uses a pattern where the API call is raced against an interruptor.

    Conceptually, the wrapping logic looks like this:

    const interruptPromise = interruptor.checkInterrupt(); // Throws if CTRL-C happened, or rejects on CTRL-C
    
    try {
      return result = Promise.race([
        interruptPromise.promise(),   // Rejects on CTRL-C
        Collection.find(args)         // The actual Shell API call
      ]);
    } catch {
      // Handle interruption
    } finally {
      interruptPromise.destroy(); // Cleanup to prevent memory leaks
    }
    const interruptPromise = interruptor.checkInterrupt(); // will throw immediately if CTRL-C already happened
                                                           // otherwise the contained promise is rejected on CTRL-C
    
    try {
      return result = Promise.race([
        interruptPromise.promise(),   // this is never resolved but only rejected on CTRL-C!
        Collection.find(args)         // here we call the original function
      ]);
    } catch {
      ... // throw an appropriate error (see below for exception handling details)
    } finally {
      interruptPromise.destroy(); // we make sure to de-register ourselves to not leak memory
    }
  4. How MongoshInterruptedError is handled in user code

    main

    To prevent user-defined try-catch blocks from silently swallowing an interruption, the async-rewriter transforms the code. It ensures that if an error is an instance of MongoshInterruptedError, it is immediately re-thrown, bypassing the user's catch block. It also ensures that finally blocks only execute if the error was actually catchable, preventing side effects from running after an interrupt.

    // Original User Code:
    try {
      return await db.coll.count();
    } catch (e) {
      return -1;
    } finally {
      print('completed');
    }
    
    // Rewritten Code (approximate):
    let isCatchable;
    try {
      return await db.coll.count();
    } catch (e) {
      isCatchable = isCatchableError(e); // checks if e != MongoshInterruptedError
      if (!isCatchable) {
        throw e; // Re-throws the uncatchable interrupt error
      }
      return -1;
    } finally {
      if (isCatchable) {
        print('completed');
      }
    }
  5. How the async-rewriter2 transformation works

    main

    The async-rewriter2 uses a three-step transformation process to enable implicit awaiting without a global symbol table:

    1. IIFE wrapping: The input code is wrapped in an Immediately Invoked Function Expression (IIFE). This ensures that identifiers (like top-level functions) remain accessible in the outer environment while the execution logic is encapsulated.

    2. Uncatchable exceptions: To support shell features like Ctrl+C, the rewriter transforms try/catch/finally blocks to use a special type of exception marked with Symbol.for('@@mongosh.uncatchable'). These exceptions are designed to bypass userland catch blocks so the shell can intercept them.

    3. Async function wrapping:

      • Shorthand arrow functions are converted to statement bodies.
      • Original functions are turned into async functions, but a non-async wrapper is generated to manage execution state. This wrapper forwards synchronous results immediately and handles the transition to asynchronous execution.
      • Expressions are wrapped with checks that look for a specific Symbol property (set by the API). If the property is present, the expression is awaited; otherwise, it is returned as-is.

    Note on limitations: This approach does not currently support situations where implicit async functions cannot be used, such as inside class constructors or synchronous generator functions. It also does not prevent conflicts with top-level variable names (e.g., naming a variable db).

  6. Understand the Shell Evaluator's command detection

    main
    The ShellEvaluator serves as an intermediate layer that distinguishes between standard JavaScript and specialized shell commands. It specifically detects Linux-style commands (e.g., show dbs) and triggers their execution directly, bypassing the JavaScript transformation and async-rewriter pipeline.
  7. Understand the @mongosh/shell-api abstractions

    main

    The @mongosh/shell-api package provides the core, runtime-independent classes and global objects that define the MongoDB Shell experience. It ensures that the same API surface is available regardless of the underlying execution environment (e.g., CLI, Browser, or Electron).

    Key components include:

    Core Classes

    These classes represent the primary data and operational structures in MongoDB:

    • Database: Represents a MongoDB database.
    • Collection: Represents a MongoDB collection.

    Global Objects and APIs

    These are the standard tools available to shell users for interacting with the server and the environment:

    • Database/Cluster Management: db (the current database), rs (replica set commands), and sh (sharding commands).
    • Environment/Output: console for standard output/error and print() for formatted printing to the shell.
  8. Understand mongosh CLI event logging via message bus

    main

    The CLI REPL listens to several events via a message bus. These events are logged to the user's local log file in ~/.mongodb/mongosh/ in NDJSON format using pino. This is useful for telemetry, debugging, and auditing shell activity.

    Available Bus Events:

    • mongosh:connect: Emitted when a connection is established. Carries driverUri.
    • mongosh:error: Emitted when an error is thrown. Carries an Error object.
    • mongosh:rewritten-async-input: Used for debugging the async-rewriter. Carries original and rewritten strings.
    • mongosh:use: Emitted when the use command is called. Carries the db name.
    • mongosh:show: Emitted when a show command is used. Carries the method name.
    • mongosh:it: Emitted when the it command is called.
    • mongosh:api-call: Emitted when an API call is made. Carries details about the method, class, database, collection, and arguments (e.g., pipeline, query, options). Note: To keep logs clean, documents returned by calls like insert() are typically not included in the arguments.
  9. Understand Mongosh error codes and scopes

    main

    Error codes are used to identify specific errors and map them to documentation. They follow a pattern consisting of a scope and a numeric part (e.g., ASYNC-01005 where the scope is ASYNC).

    Recommended Numbering Scheme:

    • Use 5-digit numbers for spacing.
    • 10000 - 89999: Errors that can be solved immediately by the user.
    • 90000 - 99999: Errors caused by internal limitations or violated assumptions.

    To generate an overview of all errors across all packages, run:

    npx ts-node scripts/extract-errors.ts <path to /packages>
  10. Understand the role of the Async Rewriter

    main
    The async-rewriter2 is a core component responsible for improving the developer experience by allowing users to write code in a synchronous style. It automatically transforms user input by inserting await statements where necessary to resolve promises returned by the Shell API. This allows commands like db.coll.find({}) to behave as if they were naturally asynchronous without requiring the user to explicitly type await.
  11. Compile mongosh components

    main

    Use the following commands to compile different parts of the project:

    • All TypeScript: npm run compile
    • CLI and its dependencies: npm run compile-cli
    • Standalone executable: npm run compile-exec

    Compilation Environment Variables:

    • NODE_JS_VERSION: Specify a Node.js version (e.g., 16.15.0 or 16.x).
    • BOXEDNODE_CONFIGURE_ARGS: Node.js configure flags (comma-separated or JSON array, e.g., --shared-openssl,--shared-zlib).
    • BOXEDNODE_MAKE_ARGS: Node.js make args (comma-separated or JSON array, e.g., -j12).

    Packaging Example (Debian): To compile and package a .deb for Debian:

    npm run compile-exec
    npm run evergreen-release package -- --build-variant=deb-x64

    Output is written to the dist/ directory.

    npm run compile
    npm run compile-cli
    npm run compile-exec