UNPKG

@13w/miri

Version:

MongoDB patch manager

289 lines (203 loc) 9.87 kB
# Miri A MongoDB migration and patch manager with SSH tunneling support. Manages database schema changes through versioned migration scripts, initial setup scripts, and index definitions. Published as `@13w/miri` on npm. ## Requirements - Node.js >= 18 ## Installation ```bash npm install -g @13w/miri ``` ## Quick Start 1. Create a `.mirirc` file in your project root (see [Configuration](#configuration)) 2. Create a `migrations/` directory with your migration scripts (see [Migration Structure](#migration-structure)) 3. Run `miri status` to see pending migrations 4. Run `miri sync` to apply all pending migrations ## Configuration Miri reads a `.mirirc` JSON file from the current working directory. All settings can also be overridden via CLI flags. ### Minimal `.mirirc` ```json { "db": "mongodb://127.0.0.1:27017/MyDatabase", "migrations": "migrations" } ``` ### Full `.mirirc` with environments ```json { "db": "mongodb://127.0.0.1:27017/MyDatabase", "migrations": "migrations", "sshUser": "ubuntu", "environments": { "dev": { "sshProfile": "my-dev-bastion" }, "staging": { "sshProfile": "my-staging-bastion" }, "production": { "sshHost": "bastion.example.com", "sshUser": "deploy", "sshKey": "~/.ssh/prod_key" } } } ``` ### Configuration Options | Key | CLI Flag | Default | Description | |-----|----------|---------|-------------| | `db` | `-d, --db <uri>` | `mongodb://localhost:27017/test` | MongoDB connection URI. The hostname and port are used as the SSH tunnel destination when tunneling is active. | | `migrations` | `-m, --migrations <folder>` | `./migrations` | Path to the migrations directory, relative to the working directory. | | `directConnection` | `--no-direct-connection` | `true` | Appends `directConnection=true` to the MongoDB URI. Disable this when connecting to a replica set where you want the driver to discover other members. | | `sshProfile` | `--ssh-profile <profile>` | — | Name of an SSH host entry in `~/.ssh/config`. When set, miri reads `Hostname`, `Port`, `User`, and `IdentityFile` from the matching SSH config block. Individual SSH fields from CLI flags take precedence over what the profile provides. | | `sshHost` | `--ssh-host <host>` | — | SSH bastion/jump host address. Setting this (or `sshProfile`) activates SSH tunneling. | | `sshPort` | `--ssh-port <port>` | `22` | SSH port on the bastion host. | | `sshUser` | `--ssh-user <user>` | — | SSH username. Can be set at the top level of `.mirirc` as a default for all environments. | | `sshKey` | `--ssh-key <path>` | — | Path to the SSH private key. Supports `~/` expansion. If a `.pub` file is given, miri will look for the corresponding private key. | | `sshAskPass` | — | `false` | When `true` and the private key is encrypted, miri prompts for the passphrase interactively. | ### Environment Selection Use `-e <name>` to select a named environment: ```bash miri -e dev status # Uses the "dev" environment miri -e production sync # Uses the "production" environment ``` **Resolution order** (last wins): 1. Top-level `.mirirc` fields (`db`, `migrations`, `sshUser`, etc.) 2. Fields from the selected `environments.<name>` block 3. CLI flags If no `-e` flag is provided, miri looks for an environment named `"default"`. If that doesn't exist, the top-level settings are used directly. ### SSH Tunneling When an SSH host is configured (via `sshProfile` or `sshHost`), miri creates a local SSH tunnel to the MongoDB host before connecting. The tunnel forwards a random local port to the `hostname:port` extracted from the `db` URI. **Authentication priority:** 1. **SSH Agent** — If `SSH_AUTH_SOCK` is set and the key is loaded in the agent, the agent is used automatically. 2. **Private key file** — If a key file is configured and not already in the agent, it is read from disk. 3. **Passphrase prompt** — If the private key is encrypted (passphrase-protected), miri prompts for the passphrase on stdin. ## CLI Reference ``` miri [options] [command] ``` ### Global Options | Flag | Description | |------|-------------| | `-V, --version` | Output the version number | | `-e, --env <environment>` | Environment name from `.mirirc` (default: `"default"`) | | `-m, --migrations <folder>` | Folder with migrations | | `-d, --db <mongo-uri>` | MongoDB connection URI | | `--no-direct-connection` | Disable `directConnection` on the MongoDB URI | | `--ssh-profile <profile>` | Connect via SSH using an `~/.ssh/config` profile | | `--ssh-host <host>` | SSH proxy host | | `--ssh-port <port>` | SSH proxy port | | `--ssh-user <user>` | SSH proxy user | | `--ssh-key <path>` | SSH proxy identity key | ### Commands #### `miri status [--all]` Displays the status of all versioned patches. Use `--all` to include init patches. Statuses: - **Ok** — Applied and unchanged - **New** — Exists locally but not yet applied - **Updated** — Applied, but `test` or `down` functions have changed (safe to re-sync) - **Changed** — Applied, but the `up` function has changed (requires revert + reapply) - **Degraded** — Applied, but `test()` returns > 0 (the migration's postcondition is no longer met) - **Removed** — In the database but no longer exists locally #### `miri sync [--degraded] [--all]` Applies all pending migrations in order: init scripts, then indexes, then versioned patches. - `--degraded` — Also re-apply patches whose status is Degraded - `--all` — Re-apply all patches regardless of status #### `miri init apply [patch] [--no-exec] [--force]` Runs init scripts from `migrations/init/`. Optionally target a single patch by name. - `--no-exec` — Mark the patch as applied without executing it - `--force` — Re-apply even if already recorded as done #### `miri init remove <patch> [--no-exec]` Removes an init patch record from the database. - `--no-exec` — Remove the record without executing the script #### `miri init status` Displays the status of init patches only. #### `miri indexes status [collection] [-q, --quiet]` Shows the diff between local index definitions and the indexes currently in MongoDB. - `--quiet` — Only show changes (hide indexes that are already applied) #### `miri indexes sync [collection]` Creates new indexes and drops removed indexes. Optionally target a single collection. #### `miri patch diff` Displays the diff between local versioned patches and what's applied in the database. #### `miri patch sync [--remote] [--degraded] [--all]` Apply versioned patches. - `--remote` — Remote only - `--degraded` — Re-apply degraded patches - `--all` — Re-apply all patches #### `miri patch apply <group> <patch> [--no-exec]` Apply a single specific patch by group and name. #### `miri patch remove <group> <patch> [--no-exec]` Revert and remove a single patch. Runs the `down()` function then deletes the record. ## Migration Structure ``` migrations/ ├── init/ # One-time setup scripts │ ├── 01-create-collections.js │ └── 02-seed-data.js ├── indexes/ # Index definitions (JSON) │ ├── users.json │ └── goods.json ├── version-1/ # Versioned patch group │ ├── 01-02-2023-add-full-name.js │ └── 04-05-2023-add-user-age.js └── version-2/ # Another patch group └── 05-08-2023-add-price.js ``` ### Init Scripts Simple scripts executed in a `mongosh` context. They run once and are tracked by name. ```javascript db.createCollection('users'); db.createCollection('goods'); ``` ### Versioned Patches Each patch must export three functions: ```javascript // test: returns the count of documents that still need migrating // When this returns 0, the migration is considered fully applied export const test = () => db.users.countDocuments({ fullName: { $exists: false } }); // up: applies the migration export const up = () => db.users.updateMany( { fullName: { $exists: false } }, [{ $set: { fullName: { $concat: ['$firstName', ' ', '$lastName'] } } }] ); // down: reverts the migration export const down = () => db.users.updateMany({}, { $unset: { fullName: 1 } }); ``` **Execution flow during sync:** 1. `test()` is called to check if migration is needed 2. If the patch was previously applied but has changed, `down()` is called first to revert 3. `up()` is called to apply the migration 4. The patch content (hashes of test/up/down) is stored in the `migrations` collection ### Index Definitions JSON files in `migrations/indexes/`, named after the target collection. Each file contains an array of index specifications: ```json [ { "name": 1 }, [{ "email": 1 }, { "unique": true }] ] ``` - A plain object defines the index key (e.g., `{ "name": 1 }`) - An array of `[keySpec, options]` lets you pass index options like `unique`, `sparse`, etc. ### Environment Variables Migration scripts can access environment variables prefixed with `MIRI_`. Inside scripts, they're available on the `__env` object with the prefix stripped: ```bash MIRI_ADMIN_EMAIL=admin@example.com miri sync ``` ```javascript // In a migration script: export const up = () => db.users.updateOne( { role: 'admin' }, { $set: { email: __env.ADMIN_EMAIL } } ); ``` ## How Miri Tracks State Miri stores migration state in a `migrations` collection in the target database. Each applied patch is recorded with: - `group` — The subdirectory name (e.g., `version-1`, `init`) - `name` — The filename - `content` — SHA-256 hashes and base64-encoded bodies of `test`, `up`, and `down` functions This allows miri to detect when a migration script has been modified since it was last applied and report the appropriate status (Updated, Changed). ## License MIT