@13w/miri 1.1.4
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 +0 -0
- package/bin/miri +3 -0
- package/dist/cli.js +71 -0
- package/dist/evaluator.js +82 -0
- package/dist/miri.js +244 -0
- package/dist/mongodb.js +6 -0
- package/package.json +45 -0
package/Readme.md
ADDED
|
File without changes
|
package/bin/miri
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { Table } from 'console-table-printer';
|
|
5
|
+
import connection from './mongodb.js';
|
|
6
|
+
import Miri, { PatchStatus } from './miri.js';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program.option('-m --migrations <folder>', 'Folder with migrations', join(process.cwd(), 'migrations'));
|
|
9
|
+
program.option('-d --db <mongo-uri>', 'MongoDB Connection URI', 'mongodb://localhost:27017/test');
|
|
10
|
+
const getMiri = async () => {
|
|
11
|
+
const opts = program.opts();
|
|
12
|
+
const client = await connection(opts.db);
|
|
13
|
+
return new Miri(client, {
|
|
14
|
+
localMigrations: program.opts().migrations,
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
const stats = async (remote = false) => {
|
|
18
|
+
const miri = await getMiri();
|
|
19
|
+
const patches = await miri.stat(remote);
|
|
20
|
+
await miri[Symbol.asyncDispose]();
|
|
21
|
+
if (!patches.length) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const table = new Table({});
|
|
25
|
+
for (const patch of patches) {
|
|
26
|
+
const printable = {
|
|
27
|
+
group: patch.group,
|
|
28
|
+
name: patch.name,
|
|
29
|
+
status: PatchStatus[patch.status],
|
|
30
|
+
degradation: patch.degradation,
|
|
31
|
+
};
|
|
32
|
+
table.addRow(printable, {
|
|
33
|
+
color: {
|
|
34
|
+
[PatchStatus.Ok]: 'white',
|
|
35
|
+
[PatchStatus.New]: 'cyan',
|
|
36
|
+
[PatchStatus.Updated]: 'white_bold',
|
|
37
|
+
[PatchStatus.Changed]: 'yellow',
|
|
38
|
+
[PatchStatus.Degraded]: 'blue',
|
|
39
|
+
[PatchStatus.Removed]: 'red',
|
|
40
|
+
}[patch.status],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
table.printTable();
|
|
44
|
+
};
|
|
45
|
+
program.command('stats')
|
|
46
|
+
.description('Displays list of applied migrations')
|
|
47
|
+
.action(() => stats(true));
|
|
48
|
+
const initProgram = program.command('init');
|
|
49
|
+
initProgram.command('apply')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
const miri = await getMiri();
|
|
52
|
+
await miri.init();
|
|
53
|
+
await miri[Symbol.asyncDispose]();
|
|
54
|
+
});
|
|
55
|
+
initProgram.command('status')
|
|
56
|
+
.action(() => {
|
|
57
|
+
console.log('There should be init status....');
|
|
58
|
+
});
|
|
59
|
+
program.command('diff')
|
|
60
|
+
.description('Displays difference between local and applied migrations')
|
|
61
|
+
.action(() => stats());
|
|
62
|
+
program.command('sync')
|
|
63
|
+
.description('Applies migrations')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const miri = await getMiri();
|
|
66
|
+
await miri.sync();
|
|
67
|
+
await miri[Symbol.asyncDispose]();
|
|
68
|
+
});
|
|
69
|
+
program.parse();
|
|
70
|
+
const opts = program.opts();
|
|
71
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { createContext, runInContext, SourceTextModule, SyntheticModule } from 'node:vm';
|
|
4
|
+
import { inspect } from 'node:util';
|
|
5
|
+
import { CliServiceProvider } from '@mongosh/service-provider-server';
|
|
6
|
+
import { ShellInstanceState } from '@mongosh/shell-api';
|
|
7
|
+
import { ShellEvaluator } from '@mongosh/shell-evaluator';
|
|
8
|
+
import connect from './mongodb.js';
|
|
9
|
+
const mongoClient = await connect();
|
|
10
|
+
const print = (values, type) => {
|
|
11
|
+
const prepare = (object, type = 'print') => {
|
|
12
|
+
if (type === 'print') {
|
|
13
|
+
return String(object);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
return inspect(object, { colors: true, customInspect: true, depth: 22 });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
for (const value of values) {
|
|
20
|
+
const printable = value.printable ?? value.rawValue;
|
|
21
|
+
if (printable === void 0) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
process.stdout.write(prepare(printable, value.type ?? type));
|
|
25
|
+
process.stdout.write('\n');
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const bus = new EventEmitter();
|
|
29
|
+
const cliServiceProvider = new CliServiceProvider(mongoClient, bus, {
|
|
30
|
+
productName: 'MIRI: Migration manager',
|
|
31
|
+
productDocsLink: 'https://example.com/',
|
|
32
|
+
});
|
|
33
|
+
const context = createContext({
|
|
34
|
+
__env: Object.fromEntries(Object.entries(process.env)
|
|
35
|
+
.filter(([key]) => key.startsWith('MIRI_'))
|
|
36
|
+
.map(([key, value]) => [key.substring(5), value])),
|
|
37
|
+
});
|
|
38
|
+
const originalEval = (code) => runInContext(code, context);
|
|
39
|
+
const instanceState = new ShellInstanceState(cliServiceProvider);
|
|
40
|
+
instanceState.setCtx(context);
|
|
41
|
+
instanceState.setEvaluationListener({
|
|
42
|
+
onPrint(values, type) {
|
|
43
|
+
// console.dir(['onPrint', type, values])
|
|
44
|
+
return print(values, type);
|
|
45
|
+
},
|
|
46
|
+
onPrompt() {
|
|
47
|
+
throw new Error('Prompt isn\'t supported');
|
|
48
|
+
},
|
|
49
|
+
onLoad() {
|
|
50
|
+
throw new Error('Load isn\'t supported');
|
|
51
|
+
},
|
|
52
|
+
async onExit(exitCode) {
|
|
53
|
+
process.stdout.write(`onExit: ${exitCode}\n`);
|
|
54
|
+
await mongoClient.close();
|
|
55
|
+
process.exit(exitCode);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
export async function evaluateMongo(code, filename = '[no file]') {
|
|
59
|
+
const output = await new ShellEvaluator(instanceState)
|
|
60
|
+
.customEval(originalEval, `${code};`, context, filename);
|
|
61
|
+
// print([output])
|
|
62
|
+
return output.rawValue;
|
|
63
|
+
}
|
|
64
|
+
async function linker(specifier, referencingModule) {
|
|
65
|
+
return import(specifier).then((module) => {
|
|
66
|
+
return new SyntheticModule(['default'], function () {
|
|
67
|
+
this.setExport('default', module.default);
|
|
68
|
+
}, { context: referencingModule.context });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function evaluateJs(code, identifier = '[no file]') {
|
|
72
|
+
const context = createContext({ console });
|
|
73
|
+
const module = new SourceTextModule(code, { context, identifier });
|
|
74
|
+
await module.link(linker);
|
|
75
|
+
await module.evaluate()
|
|
76
|
+
.catch((error) => {
|
|
77
|
+
error.message += ` # at file ${identifier}`;
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
return module.namespace;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=evaluator.js.map
|
package/dist/miri.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ObjectId } from 'mongodb';
|
|
6
|
+
import { evaluateJs, evaluateMongo } from './evaluator.js';
|
|
7
|
+
export var PatchStatus;
|
|
8
|
+
(function (PatchStatus) {
|
|
9
|
+
PatchStatus[PatchStatus["Ok"] = 0] = "Ok";
|
|
10
|
+
PatchStatus[PatchStatus["New"] = 1] = "New";
|
|
11
|
+
PatchStatus[PatchStatus["Updated"] = 2] = "Updated";
|
|
12
|
+
PatchStatus[PatchStatus["Changed"] = 3] = "Changed";
|
|
13
|
+
PatchStatus[PatchStatus["Removed"] = 4] = "Removed";
|
|
14
|
+
PatchStatus[PatchStatus["Degraded"] = 5] = "Degraded";
|
|
15
|
+
})(PatchStatus || (PatchStatus = {}));
|
|
16
|
+
const sortPatches = (a, b) => {
|
|
17
|
+
if (a.group === b.group) {
|
|
18
|
+
return a.name < b.name ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
return a.group < b.group ? 1 : -1;
|
|
21
|
+
};
|
|
22
|
+
export default class Migrator {
|
|
23
|
+
client;
|
|
24
|
+
options;
|
|
25
|
+
#client;
|
|
26
|
+
#collection;
|
|
27
|
+
#options;
|
|
28
|
+
constructor(client, options = {}) {
|
|
29
|
+
this.client = client;
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.#client = client;
|
|
32
|
+
this.#collection = client.db().collection('migrations');
|
|
33
|
+
this.#options = options;
|
|
34
|
+
}
|
|
35
|
+
async [Symbol.asyncDispose]() {
|
|
36
|
+
await this.#client.close();
|
|
37
|
+
}
|
|
38
|
+
async diff() {
|
|
39
|
+
const localPatches = await this.getLocalPatches();
|
|
40
|
+
const remotePatches = await this.getRemotePatches();
|
|
41
|
+
for (const localPatch of localPatches) {
|
|
42
|
+
const remotePatch = remotePatches.find(({ group, name }) => localPatch.group === group && localPatch.name === name);
|
|
43
|
+
if (remotePatch) {
|
|
44
|
+
localPatch._id = remotePatch._id;
|
|
45
|
+
localPatch.status = PatchStatus.Ok;
|
|
46
|
+
if (localPatch.content.up?.hash !== remotePatch.content.up?.hash) {
|
|
47
|
+
localPatch.status = PatchStatus.Changed;
|
|
48
|
+
}
|
|
49
|
+
else if (localPatch.content.test?.hash !== remotePatch.content.test?.hash
|
|
50
|
+
|| localPatch.content.down?.hash !== remotePatch.content.down?.hash) {
|
|
51
|
+
localPatch.status = PatchStatus.Updated;
|
|
52
|
+
}
|
|
53
|
+
if (localPatch.status !== PatchStatus.Ok) {
|
|
54
|
+
Object.defineProperty(localPatch, 'remoteContent', {
|
|
55
|
+
value: remotePatch.content,
|
|
56
|
+
enumerable: false,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
remotePatches.splice(remotePatches.indexOf(remotePatch), 1);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
localPatch.status = PatchStatus.New;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
localPatches.push(...remotePatches.map((remotePatch) => {
|
|
66
|
+
remotePatch.status = PatchStatus.Removed;
|
|
67
|
+
return remotePatch;
|
|
68
|
+
}));
|
|
69
|
+
localPatches.sort(sortPatches);
|
|
70
|
+
return localPatches;
|
|
71
|
+
}
|
|
72
|
+
async applyPatchContent(content, wrap = true) {
|
|
73
|
+
if (!content?.body) {
|
|
74
|
+
return -1;
|
|
75
|
+
}
|
|
76
|
+
const code = Buffer.from(content.body, 'base64').toString();
|
|
77
|
+
return evaluateMongo(wrap ? `(${code})();` : code)
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
throw error;
|
|
80
|
+
}) ?? 0;
|
|
81
|
+
}
|
|
82
|
+
async sync() {
|
|
83
|
+
const patches = await this.diff();
|
|
84
|
+
for (const patch of patches) {
|
|
85
|
+
console.group(`Patch ${patch.group} / ${patch.name}...`);
|
|
86
|
+
if (patch.status === PatchStatus.Removed) {
|
|
87
|
+
console.group('Reverting changes');
|
|
88
|
+
const result = await this.applyPatchContent(patch.remoteContent?.down);
|
|
89
|
+
console.log(result === -1 ? 'Revert script not found' : 'done');
|
|
90
|
+
console.groupEnd();
|
|
91
|
+
await this.#collection.deleteOne({ _id: patch._id });
|
|
92
|
+
console.groupEnd();
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (patch.status === PatchStatus.Changed) {
|
|
96
|
+
console.group('Reverting changes');
|
|
97
|
+
const result = await this.applyPatchContent(patch.remoteContent?.down);
|
|
98
|
+
console.log(result === -1 ? 'Revert script not found' : 'done');
|
|
99
|
+
console.groupEnd();
|
|
100
|
+
patch.status = PatchStatus.New;
|
|
101
|
+
}
|
|
102
|
+
if (patch.status === PatchStatus.New) {
|
|
103
|
+
console.group('Testing migration');
|
|
104
|
+
const test = await this.applyPatchContent(patch.content.test);
|
|
105
|
+
console.log(`degradation level: ${test}`);
|
|
106
|
+
if (test !== 0) {
|
|
107
|
+
console.group('Applying migration...');
|
|
108
|
+
await this.applyPatchContent(patch.content.up);
|
|
109
|
+
console.log('Done');
|
|
110
|
+
console.groupEnd();
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log('nothing to apply');
|
|
114
|
+
}
|
|
115
|
+
console.groupEnd();
|
|
116
|
+
}
|
|
117
|
+
console.groupEnd();
|
|
118
|
+
await this.#collection.updateOne({ _id: patch._id }, {
|
|
119
|
+
$set: {
|
|
120
|
+
_id: patch._id,
|
|
121
|
+
group: patch.group,
|
|
122
|
+
name: patch.name,
|
|
123
|
+
content: patch.content,
|
|
124
|
+
},
|
|
125
|
+
}, { upsert: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async getLocalPatches(raw = false, group = '', name = '') {
|
|
129
|
+
const migrationsDir = await realpath(join(process.cwd(), this.#options.localMigrations ?? 'migrations'));
|
|
130
|
+
console.debug(`Reading ${migrationsDir}...`);
|
|
131
|
+
const files = await readdir(migrationsDir, { recursive: true, withFileTypes: true });
|
|
132
|
+
const patches = [];
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
if (!file.isFile()) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const patchObject = {
|
|
138
|
+
_id: new ObjectId(),
|
|
139
|
+
group: file.path.substring(migrationsDir.length + 1),
|
|
140
|
+
name: file.name,
|
|
141
|
+
content: {},
|
|
142
|
+
};
|
|
143
|
+
if (group ? patchObject.group !== group : ['init', 'indexes'].includes(patchObject.group)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (name && patchObject.name !== name) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
patches.push(patchObject);
|
|
150
|
+
const patchContent = await readFile(join(file.path, file.name));
|
|
151
|
+
if (raw) {
|
|
152
|
+
patchObject.raw = patchContent.toString('base64');
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const patchExports = await evaluateJs(patchContent.toString());
|
|
156
|
+
for (const key of ['test', 'up', 'down']) {
|
|
157
|
+
const func = patchExports[key];
|
|
158
|
+
if (typeof func !== 'function') {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const body = Buffer.from(func.toString()).toString('base64');
|
|
162
|
+
patchObject.content[key] = {
|
|
163
|
+
body,
|
|
164
|
+
hash: createHash('sha256').update(body).digest('hex'),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return patches;
|
|
169
|
+
}
|
|
170
|
+
async getRemotePatches(group = '', name = '') {
|
|
171
|
+
const filter = {
|
|
172
|
+
group: { $nin: ['init', 'index'] },
|
|
173
|
+
};
|
|
174
|
+
if (group) {
|
|
175
|
+
filter.group = group;
|
|
176
|
+
}
|
|
177
|
+
if (name) {
|
|
178
|
+
filter.name = name;
|
|
179
|
+
}
|
|
180
|
+
const patches = await this.#collection.find(filter, {
|
|
181
|
+
projection: {
|
|
182
|
+
_id: 1,
|
|
183
|
+
group: 1,
|
|
184
|
+
name: 1,
|
|
185
|
+
content: {
|
|
186
|
+
test: {
|
|
187
|
+
hash: 1,
|
|
188
|
+
body: 1,
|
|
189
|
+
},
|
|
190
|
+
up: {
|
|
191
|
+
hash: 1,
|
|
192
|
+
body: 1,
|
|
193
|
+
},
|
|
194
|
+
down: {
|
|
195
|
+
hash: 1,
|
|
196
|
+
body: 1,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}).toArray();
|
|
201
|
+
patches.sort(sortPatches);
|
|
202
|
+
return patches;
|
|
203
|
+
}
|
|
204
|
+
async init() {
|
|
205
|
+
const localInits = await this.getLocalPatches(true, 'init');
|
|
206
|
+
const remoteInits = await this.getRemotePatches('init');
|
|
207
|
+
if (!localInits.length) {
|
|
208
|
+
console.log('Nothing to apply');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
console.group('Applying initial patches...');
|
|
212
|
+
for (const patch of localInits) {
|
|
213
|
+
console.group(`Testing ${patch.name}...`);
|
|
214
|
+
const remoteInit = remoteInits.find(({ group, name }) => patch.group === group && patch.name === name);
|
|
215
|
+
if (remoteInit) {
|
|
216
|
+
console.log('skip');
|
|
217
|
+
console.groupEnd();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
console.log('applying');
|
|
221
|
+
await this.applyPatchContent({ body: patch.raw, hash: '' }, false);
|
|
222
|
+
await this.#collection.insertOne({
|
|
223
|
+
group: patch.group,
|
|
224
|
+
name: patch.name,
|
|
225
|
+
content: {},
|
|
226
|
+
});
|
|
227
|
+
console.groupEnd();
|
|
228
|
+
}
|
|
229
|
+
console.groupEnd();
|
|
230
|
+
}
|
|
231
|
+
async stat(remote = false) {
|
|
232
|
+
const patches = await (remote ? this.getRemotePatches() : this.diff());
|
|
233
|
+
for (const patch of patches) {
|
|
234
|
+
patch.status = patch.status ?? PatchStatus.Ok;
|
|
235
|
+
const degradation = await this.applyPatchContent(patch.content.test);
|
|
236
|
+
patch.degradation = degradation === -1 ? '-' : degradation;
|
|
237
|
+
if (degradation > 0 && [PatchStatus.Ok, PatchStatus.Updated].includes(patch.status)) {
|
|
238
|
+
patch.status = PatchStatus.Degraded;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return patches;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=miri.js.map
|
package/dist/mongodb.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@13w/miri",
|
|
3
|
+
"description": "MongoDB patch manager",
|
|
4
|
+
"version": "1.1.4",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "v20"
|
|
8
|
+
},
|
|
9
|
+
"packageManager": "^pnpm@8.15.4",
|
|
10
|
+
"bin": {
|
|
11
|
+
"miri": "bin/miri"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/miri",
|
|
15
|
+
"dist/cli.js",
|
|
16
|
+
"dist/evaluator.js",
|
|
17
|
+
"dist/miri.js",
|
|
18
|
+
"dist/mongodb.js"
|
|
19
|
+
],
|
|
20
|
+
"main": "bin/miri",
|
|
21
|
+
"types": "types/miri.d.ts",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@mongosh/service-provider-server": "^2.1.5",
|
|
24
|
+
"@mongosh/shell-api": "^2.1.5",
|
|
25
|
+
"@mongosh/shell-evaluator": "^2.1.5",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"console-table-printer": "^2.12.0",
|
|
28
|
+
"mongodb": "^6.3.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.11.21",
|
|
32
|
+
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
|
33
|
+
"@typescript-eslint/parser": "^7.1.0",
|
|
34
|
+
"eslint": "^8.57.0",
|
|
35
|
+
"eslint-plugin-deprecation": "^2.0.0",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"typescript": "^5.3.3"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"lint": "eslint src/ --ext .ts --quiet",
|
|
43
|
+
"postinstall": "pnpm run build"
|
|
44
|
+
}
|
|
45
|
+
}
|