@13w/miri 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -1,53 +1,288 @@
1
- #### Migration manager
2
- ### Migrations folder structure
3
-
4
- * migrations/
5
- * init/
6
- * 01-create-collections.js
7
- ```javascript
8
- db.createCollection('users');
9
- db.createCollection('goods');
10
- ```
11
- * 02-create-default-users.js
12
- ```javascript
13
- db.users.insertOne({firstName: 'foo', lastName: 'zoo'});
14
- db.users.insertOne({firstName: 'baz', lastName: 'poo'});
15
- ```
16
- * 03-create-default-goods.js
17
- ```javascript
18
- db.goods.insertOne({name: 'lemon'});
19
- db.goods.insertOne({name: 'orange'});
20
- ```
21
- * indexes/
22
- * users.json
23
- ```json
24
- [
25
- {name: 1}
26
- ]
27
- ```
28
- * goods.json
29
- ```json
30
- [
31
- [{name: 1}, { unique: true }]
32
- ]
33
- ```
34
- * version-1/
35
- * 01-02-2023-add-full-name.js
36
- ```javascript
37
- export const test = () => db.users.countDocuments({ fullName: { $exists: false } });
38
- export const up = () => db.users.updateMany({ fullName: { $exists: false } }, [{ $set: { fullName: { $concat: ['$firstName', ' ', '$lastName'] } } }])
39
- export const down = () => db.users.updateMany({}, {$unset: { fullName: 1 }})
40
- ```
41
- * 04-05-2023-add-user-age.js
42
- ```javascript
43
- export const test = () => db.users.countDocuments({ age: { $exists: false } });
44
- export const up => () => db.users.updateMany({ age: { $exists: false } }, {$set: { age: 135 }})
45
- export const down = () => db.users.updateMany({}, {$unset: { age: 1 }})
46
- ```
47
- * version-2/
48
- * 05-08-2023-add-price-to-goods.js
49
- ```javascript
50
- export const test = () => db.goods.countDocuments({ price: { $exists: false } });
51
- export const up => () => db.goods.updateMany({ price: { $exists: false } }, { $set: {price: 12.24} })
52
- export const down = () => db.goods.updateMany({}, {$unset: { price: 1 }})
53
- ```
1
+ # Miri
2
+
3
+ A MongoDB migration and patch manager with SSH tunneling support. Manages database schema changes through versioned migration scripts, initial setup scripts, and index definitions.
4
+
5
+ Published as `@13w/miri` on npm.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js >= 18
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install -g @13w/miri
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ 1. Create a `.mirirc` file in your project root (see [Configuration](#configuration))
20
+ 2. Create a `migrations/` directory with your migration scripts (see [Migration Structure](#migration-structure))
21
+ 3. Run `miri status` to see pending migrations
22
+ 4. Run `miri sync` to apply all pending migrations
23
+
24
+ ## Configuration
25
+
26
+ Miri reads a `.mirirc` JSON file from the current working directory. All settings can also be overridden via CLI flags.
27
+
28
+ ### Minimal `.mirirc`
29
+
30
+ ```json
31
+ {
32
+ "db": "mongodb://127.0.0.1:27017/MyDatabase",
33
+ "migrations": "migrations"
34
+ }
35
+ ```
36
+
37
+ ### Full `.mirirc` with environments
38
+
39
+ ```json
40
+ {
41
+ "db": "mongodb://127.0.0.1:27017/MyDatabase",
42
+ "migrations": "migrations",
43
+ "sshUser": "ubuntu",
44
+
45
+ "environments": {
46
+ "dev": {
47
+ "sshProfile": "my-dev-bastion"
48
+ },
49
+ "staging": {
50
+ "sshProfile": "my-staging-bastion"
51
+ },
52
+ "production": {
53
+ "sshHost": "bastion.example.com",
54
+ "sshUser": "deploy",
55
+ "sshKey": "~/.ssh/prod_key"
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### Configuration Options
62
+
63
+ | Key | CLI Flag | Default | Description |
64
+ |-----|----------|---------|-------------|
65
+ | `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. |
66
+ | `migrations` | `-m, --migrations <folder>` | `./migrations` | Path to the migrations directory, relative to the working directory. |
67
+ | `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. |
68
+ | `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. |
69
+ | `sshHost` | `--ssh-host <host>` | — | SSH bastion/jump host address. Setting this (or `sshProfile`) activates SSH tunneling. |
70
+ | `sshPort` | `--ssh-port <port>` | `22` | SSH port on the bastion host. |
71
+ | `sshUser` | `--ssh-user <user>` | — | SSH username. Can be set at the top level of `.mirirc` as a default for all environments. |
72
+ | `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. |
73
+ | `sshAskPass` | — | `false` | When `true` and the private key is encrypted, miri prompts for the passphrase interactively. |
74
+
75
+ ### Environment Selection
76
+
77
+ Use `-e <name>` to select a named environment:
78
+
79
+ ```bash
80
+ miri -e dev status # Uses the "dev" environment
81
+ miri -e production sync # Uses the "production" environment
82
+ ```
83
+
84
+ **Resolution order** (last wins):
85
+
86
+ 1. Top-level `.mirirc` fields (`db`, `migrations`, `sshUser`, etc.)
87
+ 2. Fields from the selected `environments.<name>` block
88
+ 3. CLI flags
89
+
90
+ 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.
91
+
92
+ ### SSH Tunneling
93
+
94
+ 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.
95
+
96
+ **Authentication priority:**
97
+
98
+ 1. **SSH Agent** — If `SSH_AUTH_SOCK` is set and the key is loaded in the agent, the agent is used automatically.
99
+ 2. **Private key file** — If a key file is configured and not already in the agent, it is read from disk.
100
+ 3. **Passphrase prompt** — If the private key is encrypted (passphrase-protected), miri prompts for the passphrase on stdin.
101
+
102
+ ## CLI Reference
103
+
104
+ ```
105
+ miri [options] [command]
106
+ ```
107
+
108
+ ### Global Options
109
+
110
+ | Flag | Description |
111
+ |------|-------------|
112
+ | `-V, --version` | Output the version number |
113
+ | `-e, --env <environment>` | Environment name from `.mirirc` (default: `"default"`) |
114
+ | `-m, --migrations <folder>` | Folder with migrations |
115
+ | `-d, --db <mongo-uri>` | MongoDB connection URI |
116
+ | `--no-direct-connection` | Disable `directConnection` on the MongoDB URI |
117
+ | `--ssh-profile <profile>` | Connect via SSH using an `~/.ssh/config` profile |
118
+ | `--ssh-host <host>` | SSH proxy host |
119
+ | `--ssh-port <port>` | SSH proxy port |
120
+ | `--ssh-user <user>` | SSH proxy user |
121
+ | `--ssh-key <path>` | SSH proxy identity key |
122
+
123
+ ### Commands
124
+
125
+ #### `miri status [--all]`
126
+
127
+ Displays the status of all versioned patches. Use `--all` to include init patches.
128
+
129
+ Statuses:
130
+ - **Ok** — Applied and unchanged
131
+ - **New** — Exists locally but not yet applied
132
+ - **Updated** — Applied, but `test` or `down` functions have changed (safe to re-sync)
133
+ - **Changed** — Applied, but the `up` function has changed (requires revert + reapply)
134
+ - **Degraded** — Applied, but `test()` returns > 0 (the migration's postcondition is no longer met)
135
+ - **Removed** — In the database but no longer exists locally
136
+
137
+ #### `miri sync [--degraded] [--all]`
138
+
139
+ Applies all pending migrations in order: init scripts, then indexes, then versioned patches.
140
+
141
+ - `--degraded` — Also re-apply patches whose status is Degraded
142
+ - `--all` — Re-apply all patches regardless of status
143
+
144
+ #### `miri init apply [patch] [--no-exec] [--force]`
145
+
146
+ Runs init scripts from `migrations/init/`. Optionally target a single patch by name.
147
+
148
+ - `--no-exec` — Mark the patch as applied without executing it
149
+ - `--force` — Re-apply even if already recorded as done
150
+
151
+ #### `miri init remove <patch> [--no-exec]`
152
+
153
+ Removes an init patch record from the database.
154
+
155
+ - `--no-exec` — Remove the record without executing the script
156
+
157
+ #### `miri init status`
158
+
159
+ Displays the status of init patches only.
160
+
161
+ #### `miri indexes status [collection] [-q, --quiet]`
162
+
163
+ Shows the diff between local index definitions and the indexes currently in MongoDB.
164
+
165
+ - `--quiet` — Only show changes (hide indexes that are already applied)
166
+
167
+ #### `miri indexes sync [collection]`
168
+
169
+ Creates new indexes and drops removed indexes. Optionally target a single collection.
170
+
171
+ #### `miri patch diff`
172
+
173
+ Displays the diff between local versioned patches and what's applied in the database.
174
+
175
+ #### `miri patch sync [--remote] [--degraded] [--all]`
176
+
177
+ Apply versioned patches.
178
+
179
+ - `--remote` — Remote only
180
+ - `--degraded` — Re-apply degraded patches
181
+ - `--all` — Re-apply all patches
182
+
183
+ #### `miri patch apply <group> <patch> [--no-exec]`
184
+
185
+ Apply a single specific patch by group and name.
186
+
187
+ #### `miri patch remove <group> <patch> [--no-exec]`
188
+
189
+ Revert and remove a single patch. Runs the `down()` function then deletes the record.
190
+
191
+ ## Migration Structure
192
+
193
+ ```
194
+ migrations/
195
+ ├── init/ # One-time setup scripts
196
+ │ ├── 01-create-collections.js
197
+ │ └── 02-seed-data.js
198
+ ├── indexes/ # Index definitions (JSON)
199
+ │ ├── users.json
200
+ │ └── goods.json
201
+ ├── version-1/ # Versioned patch group
202
+ │ ├── 01-02-2023-add-full-name.js
203
+ │ └── 04-05-2023-add-user-age.js
204
+ └── version-2/ # Another patch group
205
+ └── 05-08-2023-add-price.js
206
+ ```
207
+
208
+ ### Init Scripts
209
+
210
+ Simple scripts executed in a `mongosh` context. They run once and are tracked by name.
211
+
212
+ ```javascript
213
+ db.createCollection('users');
214
+ db.createCollection('goods');
215
+ ```
216
+
217
+ ### Versioned Patches
218
+
219
+ Each patch must export three functions:
220
+
221
+ ```javascript
222
+ // test: returns the count of documents that still need migrating
223
+ // When this returns 0, the migration is considered fully applied
224
+ export const test = () =>
225
+ db.users.countDocuments({ fullName: { $exists: false } });
226
+
227
+ // up: applies the migration
228
+ export const up = () =>
229
+ db.users.updateMany(
230
+ { fullName: { $exists: false } },
231
+ [{ $set: { fullName: { $concat: ['$firstName', ' ', '$lastName'] } } }]
232
+ );
233
+
234
+ // down: reverts the migration
235
+ export const down = () =>
236
+ db.users.updateMany({}, { $unset: { fullName: 1 } });
237
+ ```
238
+
239
+ **Execution flow during sync:**
240
+ 1. `test()` is called to check if migration is needed
241
+ 2. If the patch was previously applied but has changed, `down()` is called first to revert
242
+ 3. `up()` is called to apply the migration
243
+ 4. The patch content (hashes of test/up/down) is stored in the `migrations` collection
244
+
245
+ ### Index Definitions
246
+
247
+ JSON files in `migrations/indexes/`, named after the target collection. Each file contains an array of index specifications:
248
+
249
+ ```json
250
+ [
251
+ { "name": 1 },
252
+ [{ "email": 1 }, { "unique": true }]
253
+ ]
254
+ ```
255
+
256
+ - A plain object defines the index key (e.g., `{ "name": 1 }`)
257
+ - An array of `[keySpec, options]` lets you pass index options like `unique`, `sparse`, etc.
258
+
259
+ ### Environment Variables
260
+
261
+ Migration scripts can access environment variables prefixed with `MIRI_`. Inside scripts, they're available on the `__env` object with the prefix stripped:
262
+
263
+ ```bash
264
+ MIRI_ADMIN_EMAIL=admin@example.com miri sync
265
+ ```
266
+
267
+ ```javascript
268
+ // In a migration script:
269
+ export const up = () =>
270
+ db.users.updateOne(
271
+ { role: 'admin' },
272
+ { $set: { email: __env.ADMIN_EMAIL } }
273
+ );
274
+ ```
275
+
276
+ ## How Miri Tracks State
277
+
278
+ Miri stores migration state in a `migrations` collection in the target database. Each applied patch is recorded with:
279
+
280
+ - `group` — The subdirectory name (e.g., `version-1`, `init`)
281
+ - `name` — The filename
282
+ - `content` — SHA-256 hashes and base64-encoded bodies of `test`, `up`, and `down` functions
283
+
284
+ This allows miri to detect when a migration script has been modified since it was last applied and report the appropriate status (Updated, Changed).
285
+
286
+ ## License
287
+
288
+ MIT
package/Readme.orig.md ADDED
@@ -0,0 +1,54 @@
1
+ #### Migration manager
2
+ ### Migrations folder structure
3
+
4
+ * migrations/
5
+ * init/
6
+ * 01-create-collections.js
7
+ ```javascript
8
+ db.createCollection('users');
9
+ db.createCollection('goods');
10
+ ```
11
+ * 02-create-default-users.js
12
+ ```javascript
13
+ db.users.insertOne({firstName: 'foo', lastName: 'zoo'});
14
+ db.users.insertOne({firstName: 'baz', lastName: 'poo'});
15
+ ```
16
+ * 03-create-default-goods.js
17
+ ```javascript
18
+ db.goods.insertOne({name: 'lemon'});
19
+ db.goods.insertOne({name: 'orange'});
20
+ ```
21
+ * indexes/
22
+ * users.json
23
+ ```json
24
+ [
25
+ {name: 1}
26
+ ]
27
+ ```
28
+ * goods.json
29
+ ```json
30
+ [
31
+ [{name: 1}, { unique: true }]
32
+ ]
33
+ ```
34
+ * version-1/
35
+ * 01-02-2023-add-full-name.js
36
+ ```javascript
37
+ export const test = () => db.users.countDocuments({ fullName: { $exists: false } });
38
+ export const up = () => db.users.updateMany({ fullName: { $exists: false } }, [{ $set: { fullName: { $concat: ['$firstName', ' ', '$lastName'] } } }])
39
+ export const down = () => db.users.updateMany({}, {$unset: { fullName: 1 }})
40
+ ```
41
+ * 04-05-2023-add-user-age.js
42
+ ```javascript
43
+ export const test = () => db.users.countDocuments({ age: { $exists: false } });
44
+ export const up => () => db.users.updateMany({ age: { $exists: false } }, {$set: { age: 135 }})
45
+ export const down = () => db.users.updateMany({}, {$unset: { age: 1 }})
46
+ ```
47
+ * version-2/
48
+ * 05-08-2023-add-price-to-goods.js
49
+ ```javascript
50
+ export const test = () => db.goods.countDocuments({ price: { $exists: false } });
51
+ export const up => () => db.goods.updateMany({ price: { $exists: false } }, { $set: {price: 12.24} })
52
+ export const down = () => db.goods.updateMany({}, {$unset: { price: 1 }})
53
+ ```
54
+
package/dist/evaluator.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { inspect } from 'node:util';
3
3
  import { createContext, runInContext, SourceTextModule, SyntheticModule } from 'node:vm';
4
- import { CliServiceProvider } from '@mongosh/service-provider-server';
4
+ import { NodeDriverServiceProvider } from '@mongosh/service-provider-node-driver';
5
5
  import { ShellInstanceState } from '@mongosh/shell-api';
6
6
  import { ShellEvaluator } from '@mongosh/shell-evaluator';
7
7
  import colors from 'colors';
@@ -27,7 +27,7 @@ export async function evaluateMongo(client, code, filename = '[no file]') {
27
27
  const context = createContext({ __env });
28
28
  const bus = new EventEmitter();
29
29
  // console.log(`Client status! ${(<{ topology: { isConnected: () => boolean } } & MongoClient>client)?.topology?.isConnected() ? '' : 'not'} connected`)
30
- const cliServiceProvider = new CliServiceProvider(client, bus, {
30
+ const cliServiceProvider = new NodeDriverServiceProvider(client, bus, {
31
31
  productName: 'MIRI: Migration manager',
32
32
  productDocsLink: 'https://example.com/',
33
33
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@13w/miri",
3
3
  "description": "MongoDB patch manager",
4
- "version": "1.1.23",
4
+ "version": "1.1.25",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=18"
@@ -25,21 +25,21 @@
25
25
  "main": "bin/miri",
26
26
  "types": "types/miri.d.ts",
27
27
  "dependencies": {
28
- "@mongosh/service-provider-server": "^2.3.2",
29
- "@mongosh/shell-api": "^3.29.1",
30
- "@mongosh/shell-evaluator": "^3.29.1",
28
+ "@mongosh/service-provider-node-driver": "^5.0.11",
29
+ "@mongosh/shell-api": "^5.1.9",
30
+ "@mongosh/shell-evaluator": "^5.1.9",
31
31
  "colors": "^1.4.0",
32
- "commander": "^14.0.2",
32
+ "commander": "^14.0.3",
33
33
  "console-table-printer": "^2.15.0",
34
- "mongodb": "^6.21.0",
35
- "ssh-config": "^5.0.4",
34
+ "mongodb": "^7.2.0",
35
+ "ssh-config": "^5.1.0",
36
36
  "tunnel-ssh": "^5.2.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^25.1.0",
40
- "mongodb-log-writer": "^2.5.5",
39
+ "@types/node": "^25.6.0",
40
+ "mongodb-log-writer": "^2.5.12",
41
41
  "ts-node": "^10.9.2",
42
- "typescript": "^5.9.3"
42
+ "typescript": "^6.0.3"
43
43
  },
44
44
  "license": "MIT",
45
45
  "scripts": {