@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 +288 -53
- package/Readme.orig.md +54 -0
- package/dist/evaluator.js +2 -2
- package/package.json +10 -10
package/Readme.md
CHANGED
|
@@ -1,53 +1,288 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
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-
|
|
29
|
-
"@mongosh/shell-api": "^
|
|
30
|
-
"@mongosh/shell-evaluator": "^
|
|
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.
|
|
32
|
+
"commander": "^14.0.3",
|
|
33
33
|
"console-table-printer": "^2.15.0",
|
|
34
|
-
"mongodb": "^
|
|
35
|
-
"ssh-config": "^5.0
|
|
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.
|
|
40
|
-
"mongodb-log-writer": "^2.5.
|
|
39
|
+
"@types/node": "^25.6.0",
|
|
40
|
+
"mongodb-log-writer": "^2.5.12",
|
|
41
41
|
"ts-node": "^10.9.2",
|
|
42
|
-
"typescript": "^
|
|
42
|
+
"typescript": "^6.0.3"
|
|
43
43
|
},
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"scripts": {
|