@13w/miri 1.1.6 → 1.1.8
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 +53 -0
- package/dist/cli.js +74 -8
- package/dist/evaluator.js +3 -2
- package/dist/miri.js +84 -11
- package/dist/mongodb.js +5 -1
- package/package.json +2 -1
package/Readme.md
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
```
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
Error.stackTraceLimit = Infinity;
|
|
1
2
|
import { join } from 'node:path';
|
|
2
3
|
/* eslint-disable no-console */
|
|
3
4
|
import { Command } from 'commander';
|
|
4
5
|
import { Table } from 'console-table-printer';
|
|
6
|
+
import colors from 'colors';
|
|
5
7
|
import connection from './mongodb.js';
|
|
6
|
-
import Miri, { PatchStatus } from './miri.js';
|
|
8
|
+
import Miri, { IndexStatus, PatchStatus } from './miri.js';
|
|
7
9
|
const program = new Command();
|
|
8
10
|
program.option('-m --migrations <folder>', 'Folder with migrations', join(process.cwd(), 'migrations'));
|
|
9
11
|
program.option('-d --db <mongo-uri>', 'MongoDB Connection URI', 'mongodb://localhost:27017/test');
|
|
@@ -14,7 +16,7 @@ const getMiri = async () => {
|
|
|
14
16
|
localMigrations: opts.migrations,
|
|
15
17
|
});
|
|
16
18
|
};
|
|
17
|
-
const
|
|
19
|
+
const status = async (remote = false) => {
|
|
18
20
|
const miri = await getMiri();
|
|
19
21
|
const patches = await miri.stat(remote);
|
|
20
22
|
await miri[Symbol.asyncDispose]();
|
|
@@ -42,9 +44,9 @@ const stats = async (remote = false) => {
|
|
|
42
44
|
}
|
|
43
45
|
table.printTable();
|
|
44
46
|
};
|
|
45
|
-
program.command('
|
|
47
|
+
program.command('status')
|
|
46
48
|
.description('Displays list of applied migrations')
|
|
47
|
-
.action(() =>
|
|
49
|
+
.action(() => status(true));
|
|
48
50
|
const initProgram = program.command('init');
|
|
49
51
|
initProgram.command('apply')
|
|
50
52
|
.action(async () => {
|
|
@@ -56,16 +58,80 @@ initProgram.command('status')
|
|
|
56
58
|
.action(() => {
|
|
57
59
|
console.log('There should be init status....');
|
|
58
60
|
});
|
|
61
|
+
const indexesProgram = program.command('indexes');
|
|
62
|
+
const indexStatusProgram = indexesProgram.command('status')
|
|
63
|
+
.argument('[collection]', 'MongoDB Collection name')
|
|
64
|
+
.option('-q --quiet', 'Show only changes', false)
|
|
65
|
+
.action(async () => {
|
|
66
|
+
const collection = indexStatusProgram.args[0];
|
|
67
|
+
const { quiet } = indexStatusProgram.opts();
|
|
68
|
+
const miri = await getMiri();
|
|
69
|
+
const structure = await miri.indexesDiff(collection);
|
|
70
|
+
await miri[Symbol.asyncDispose]();
|
|
71
|
+
for (const [collection, indexes] of Object.entries(structure)) {
|
|
72
|
+
let changes = false;
|
|
73
|
+
for (const [name, detail] of Object.entries(indexes)) {
|
|
74
|
+
if (quiet && detail.status === IndexStatus.Applied) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const color = {
|
|
78
|
+
[IndexStatus.New]: colors.green,
|
|
79
|
+
[IndexStatus.Applied]: colors.white,
|
|
80
|
+
[IndexStatus.Removed]: colors.red,
|
|
81
|
+
}[detail.status];
|
|
82
|
+
const point = {
|
|
83
|
+
[IndexStatus.New]: colors.green.bold('+ '),
|
|
84
|
+
[IndexStatus.Applied]: colors.cyan('\u00b7 '),
|
|
85
|
+
[IndexStatus.Removed]: colors.red.bold('- '),
|
|
86
|
+
}[detail.status];
|
|
87
|
+
if (!changes) {
|
|
88
|
+
console.group(colors.bold(`Collection ${collection}:`));
|
|
89
|
+
changes = true;
|
|
90
|
+
}
|
|
91
|
+
console.log(color(`${point}${name}`));
|
|
92
|
+
}
|
|
93
|
+
console.groupEnd();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const indexSyncProgram = indexesProgram.command('sync');
|
|
97
|
+
indexSyncProgram.option('<collection>', 'MongoDB Collection name', '')
|
|
98
|
+
.action(async () => {
|
|
99
|
+
const miri = await getMiri();
|
|
100
|
+
let group = '';
|
|
101
|
+
console.group('Starting synchronisation...', indexSyncProgram.opts());
|
|
102
|
+
for await (const { collection, status, name, error } of miri.indexesSync()) {
|
|
103
|
+
if (group !== collection) {
|
|
104
|
+
if (group) {
|
|
105
|
+
console.log('Done');
|
|
106
|
+
console.groupEnd();
|
|
107
|
+
}
|
|
108
|
+
group = collection;
|
|
109
|
+
console.group(`Collection: ${collection}...`);
|
|
110
|
+
}
|
|
111
|
+
if (status === IndexStatus.Applied) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
console[error ? 'group' : 'log'](`${status === IndexStatus.New ? 'Creating' : 'Removing'} index ${name}... ${error ? 'failed' : 'done'}`);
|
|
115
|
+
if (error) {
|
|
116
|
+
console.log(colors.red(error.message));
|
|
117
|
+
console.groupEnd();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
console.groupEnd();
|
|
121
|
+
await miri[Symbol.asyncDispose]();
|
|
122
|
+
});
|
|
59
123
|
program.command('diff')
|
|
60
124
|
.description('Displays difference between local and applied migrations')
|
|
61
|
-
.action(() =>
|
|
62
|
-
program.command('sync')
|
|
125
|
+
.action(() => status());
|
|
126
|
+
const programSync = program.command('sync')
|
|
127
|
+
.option('--remote', 'Remote only')
|
|
128
|
+
.option('--degraded', 'Re-apply patches on degraded migrations')
|
|
129
|
+
.option('--all', 'Re-apply all patches')
|
|
63
130
|
.description('Applies migrations')
|
|
64
131
|
.action(async () => {
|
|
65
132
|
const miri = await getMiri();
|
|
66
|
-
await miri.sync();
|
|
133
|
+
await miri.sync(programSync.opts());
|
|
67
134
|
await miri[Symbol.asyncDispose]();
|
|
68
135
|
});
|
|
69
136
|
program.parse();
|
|
70
|
-
const opts = program.opts();
|
|
71
137
|
//# sourceMappingURL=cli.js.map
|
package/dist/evaluator.js
CHANGED
|
@@ -5,6 +5,7 @@ import { inspect } from 'node:util';
|
|
|
5
5
|
import { CliServiceProvider } from '@mongosh/service-provider-server';
|
|
6
6
|
import { ShellInstanceState } from '@mongosh/shell-api';
|
|
7
7
|
import { ShellEvaluator } from '@mongosh/shell-evaluator';
|
|
8
|
+
import colors from 'colors';
|
|
8
9
|
const print = (values, type) => {
|
|
9
10
|
const prepare = (object, type = 'print') => {
|
|
10
11
|
if (type === 'print') {
|
|
@@ -19,8 +20,7 @@ const print = (values, type) => {
|
|
|
19
20
|
if (printable === void 0) {
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
process.stdout.write('\n');
|
|
23
|
+
console.log(colors.blue(prepare(printable, value.type ?? type)));
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
const __env = Object.fromEntries(Object.entries(process.env)
|
|
@@ -29,6 +29,7 @@ const __env = Object.fromEntries(Object.entries(process.env)
|
|
|
29
29
|
export async function evaluateMongo(client, code, filename = '[no file]') {
|
|
30
30
|
const context = createContext({ __env });
|
|
31
31
|
const bus = new EventEmitter();
|
|
32
|
+
// console.log(`Client status! ${(<{ topology: { isConnected: () => boolean } } & MongoClient>client)?.topology?.isConnected() ? '' : 'not'} connected`)
|
|
32
33
|
const cliServiceProvider = new CliServiceProvider(client, bus, {
|
|
33
34
|
productName: 'MIRI: Migration manager',
|
|
34
35
|
productDocsLink: 'https://example.com/',
|
package/dist/miri.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
4
|
+
import { resolve, extname, basename } from 'node:path';
|
|
5
5
|
import { ObjectId } from 'mongodb';
|
|
6
6
|
import { evaluateJs, evaluateMongo } from './evaluator.js';
|
|
7
|
+
import colors from 'colors';
|
|
7
8
|
export var PatchStatus;
|
|
8
9
|
(function (PatchStatus) {
|
|
9
10
|
PatchStatus[PatchStatus["Ok"] = 0] = "Ok";
|
|
@@ -13,6 +14,12 @@ export var PatchStatus;
|
|
|
13
14
|
PatchStatus[PatchStatus["Removed"] = 4] = "Removed";
|
|
14
15
|
PatchStatus[PatchStatus["Degraded"] = 5] = "Degraded";
|
|
15
16
|
})(PatchStatus || (PatchStatus = {}));
|
|
17
|
+
export var IndexStatus;
|
|
18
|
+
(function (IndexStatus) {
|
|
19
|
+
IndexStatus[IndexStatus["New"] = 0] = "New";
|
|
20
|
+
IndexStatus[IndexStatus["Applied"] = 1] = "Applied";
|
|
21
|
+
IndexStatus[IndexStatus["Removed"] = 2] = "Removed";
|
|
22
|
+
})(IndexStatus || (IndexStatus = {}));
|
|
16
23
|
const sortPatches = (a, b) => {
|
|
17
24
|
if (a.group === b.group) {
|
|
18
25
|
return a.name < b.name ? -1 : 1;
|
|
@@ -74,13 +81,14 @@ export default class Migrator {
|
|
|
74
81
|
return -1;
|
|
75
82
|
}
|
|
76
83
|
const code = Buffer.from(content.body, 'base64').toString();
|
|
77
|
-
return evaluateMongo(this.client, wrap ? `(${code})();` : code, filename)
|
|
84
|
+
return evaluateMongo(this.client, wrap ? `(async ${code})();` : code, filename)
|
|
78
85
|
.catch((error) => {
|
|
79
86
|
throw error;
|
|
80
87
|
}) ?? 0;
|
|
81
88
|
}
|
|
82
|
-
async sync() {
|
|
83
|
-
const patches = await this.diff();
|
|
89
|
+
async sync({ remote = false, all = false, degraded = false } = {}) {
|
|
90
|
+
const patches = remote ? await this.getRemotePatches() : await this.diff();
|
|
91
|
+
// console.log(`Sync: remote: ${remote ? 'on' : 'off'} | all: ${all ? 'on' : 'off'} | degraded: ${degraded ? 'on' : 'off'}`)
|
|
84
92
|
for (const patch of patches) {
|
|
85
93
|
const filename = `${patch.group}/${patch.name}`;
|
|
86
94
|
console.group(`Patch ${patch.group} / ${patch.name}...`);
|
|
@@ -100,18 +108,18 @@ export default class Migrator {
|
|
|
100
108
|
console.groupEnd();
|
|
101
109
|
patch.status = PatchStatus.New;
|
|
102
110
|
}
|
|
103
|
-
if (patch.status === PatchStatus.New) {
|
|
104
|
-
console.group('Testing migration');
|
|
111
|
+
if (all || degraded || patch.status === PatchStatus.New) {
|
|
112
|
+
console.group(colors.white('Testing migration'));
|
|
105
113
|
const test = await this.applyPatchContent(patch.content.test, filename);
|
|
106
|
-
console.log(`degradation level: ${test}`);
|
|
107
|
-
if (test !== 0) {
|
|
108
|
-
console.group('Applying migration...');
|
|
114
|
+
console.log(colors.white(`degradation level: ${test}`));
|
|
115
|
+
if (all || test !== 0) {
|
|
116
|
+
console.group(colors.cyan('Applying migration...'));
|
|
109
117
|
await this.applyPatchContent(patch.content.up, filename);
|
|
110
|
-
console.log('Done');
|
|
118
|
+
console.log(colors.green('Done'));
|
|
111
119
|
console.groupEnd();
|
|
112
120
|
}
|
|
113
121
|
else {
|
|
114
|
-
console.log('nothing to apply');
|
|
122
|
+
console.log(colors.green('nothing to apply'));
|
|
115
123
|
}
|
|
116
124
|
console.groupEnd();
|
|
117
125
|
}
|
|
@@ -135,10 +143,15 @@ export default class Migrator {
|
|
|
135
143
|
if (!file.isFile()) {
|
|
136
144
|
continue;
|
|
137
145
|
}
|
|
146
|
+
const extension = extname(file.name);
|
|
147
|
+
if (!['.js', '.json'].includes(extension)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
138
150
|
const patchObject = {
|
|
139
151
|
_id: new ObjectId(),
|
|
140
152
|
group: file.path.substring(migrationsDir.length + 1),
|
|
141
153
|
name: file.name,
|
|
154
|
+
title: basename(file.name, extension),
|
|
142
155
|
content: {},
|
|
143
156
|
};
|
|
144
157
|
if (group ? patchObject.group !== group : ['init', 'indexes'].includes(patchObject.group)) {
|
|
@@ -202,6 +215,66 @@ export default class Migrator {
|
|
|
202
215
|
patches.sort(sortPatches);
|
|
203
216
|
return patches;
|
|
204
217
|
}
|
|
218
|
+
async indexesDiff(collection) {
|
|
219
|
+
const localIndexes = await this.getLocalPatches(true, 'indexes', collection && `${collection}.json`);
|
|
220
|
+
const structure = {};
|
|
221
|
+
const db = this.#client.db();
|
|
222
|
+
for (const patch of localIndexes) {
|
|
223
|
+
const indexes = JSON.parse(Buffer.from(patch.raw, 'base64').toString());
|
|
224
|
+
structure[patch.title] = {};
|
|
225
|
+
const collection = structure[patch.title];
|
|
226
|
+
// console.log(`Reading indexes from ${patch.title}...`)
|
|
227
|
+
const remoteIndexes = await db.collection(patch.title).indexes({ full: true })
|
|
228
|
+
.catch(() => []);
|
|
229
|
+
// console.dir([patch.title, remoteIndexes], { colors: true, customInspect: true, depth: 22 })
|
|
230
|
+
for (const index of indexes) {
|
|
231
|
+
// noinspection SuspiciousTypeOfGuard
|
|
232
|
+
if (typeof index === 'string') {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const [key, options] = (Array.isArray(index) ? index : [index, {}]);
|
|
236
|
+
const name = Object.entries(key).reduce((result, [key, value]) => `${result ? `${result}_` : ''}${key}_${value}`, '');
|
|
237
|
+
const remoteIndex = remoteIndexes.find(({ name: indexName }) => indexName === name);
|
|
238
|
+
collection[name] = { key, options, status: remoteIndex ? IndexStatus.Applied : IndexStatus.New };
|
|
239
|
+
if (remoteIndex) {
|
|
240
|
+
remoteIndexes.splice(remoteIndexes.indexOf(remoteIndex), 1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (remoteIndexes.length) {
|
|
244
|
+
for (const index of remoteIndexes) {
|
|
245
|
+
if (index.name === '_id_') {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
collection[index.name] = { key: index.key, options: index, status: IndexStatus.Removed };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return structure;
|
|
253
|
+
}
|
|
254
|
+
async *indexesSync(collection) {
|
|
255
|
+
const structure = await this.indexesDiff(collection);
|
|
256
|
+
const db = this.#client.db();
|
|
257
|
+
for (const [collection, indexes] of Object.entries(structure)) {
|
|
258
|
+
const coll = db.collection(collection);
|
|
259
|
+
for (const [name, { key, status, options }] of Object.entries(indexes)) {
|
|
260
|
+
if (status === IndexStatus.Removed) {
|
|
261
|
+
const error = await coll.dropIndex(name)
|
|
262
|
+
.then(() => { })
|
|
263
|
+
.catch((error) => error);
|
|
264
|
+
yield { collection, name, error, status };
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (status === IndexStatus.New) {
|
|
268
|
+
const error = await coll.createIndex(key, options)
|
|
269
|
+
.then(() => { })
|
|
270
|
+
.catch((error) => error);
|
|
271
|
+
yield { collection, name, error, status };
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
yield { collection, name, status };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
205
278
|
async init() {
|
|
206
279
|
const localInits = await this.getLocalPatches(true, 'init');
|
|
207
280
|
const remoteInits = await this.getRemotePatches('init');
|
package/dist/mongodb.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { MongoClient } from 'mongodb';
|
|
2
2
|
let client;
|
|
3
3
|
export default async function connect(uri = 'mongodb://localhost:27017/test') {
|
|
4
|
-
|
|
4
|
+
if (!client) {
|
|
5
|
+
client = await new MongoClient(uri).connect();
|
|
6
|
+
client?.topology?.socket?.unref();
|
|
7
|
+
}
|
|
8
|
+
return client;
|
|
5
9
|
}
|
|
6
10
|
//# sourceMappingURL=mongodb.js.map
|
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.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "v20"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@mongosh/service-provider-server": "^2.1.5",
|
|
24
24
|
"@mongosh/shell-api": "^2.1.5",
|
|
25
25
|
"@mongosh/shell-evaluator": "^2.1.5",
|
|
26
|
+
"colors": "^1.4.0",
|
|
26
27
|
"commander": "^12.0.0",
|
|
27
28
|
"console-table-printer": "^2.12.0",
|
|
28
29
|
"mongodb": "^6.3.0"
|