@13w/miri 1.1.5 → 1.1.7

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/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');
@@ -11,10 +13,10 @@ const getMiri = async () => {
11
13
  const opts = program.opts();
12
14
  const client = await connection(opts.db);
13
15
  return new Miri(client, {
14
- localMigrations: program.opts().migrations,
16
+ localMigrations: opts.migrations,
15
17
  });
16
18
  };
17
- const stats = async (remote = false) => {
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('stats')
47
+ program.command('status')
46
48
  .description('Displays list of applied migrations')
47
- .action(() => stats(true));
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
+ .command('status')
63
+ .argument('[collection]', 'MongoDB Collection name')
64
+ .option('-q --quiet', 'Show only changes', false)
65
+ .action(async () => {
66
+ const collection = indexesProgram.args[0];
67
+ const { quiet } = indexesProgram.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
+ indexesProgram.command('sync')
97
+ .option('<collection>', 'MongoDB Collection name', '')
98
+ .action(async () => {
99
+ const miri = await getMiri();
100
+ let group = '';
101
+ console.group('Starting synchronisation...', indexesProgram.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(() => stats());
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,8 +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 connect from './mongodb.js';
9
- const mongoClient = await connect();
8
+ import colors from 'colors';
10
9
  const print = (values, type) => {
11
10
  const prepare = (object, type = 'print') => {
12
11
  if (type === 'print') {
@@ -21,57 +20,50 @@ const print = (values, type) => {
21
20
  if (printable === void 0) {
22
21
  continue;
23
22
  }
24
- process.stdout.write(prepare(printable, value.type ?? type));
25
- process.stdout.write('\n');
23
+ console.log(colors.blue(prepare(printable, value.type ?? type)));
26
24
  }
27
25
  };
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]') {
26
+ const __env = Object.fromEntries(Object.entries(process.env)
27
+ .filter(([key]) => key.startsWith('MIRI_'))
28
+ .map(([key, value]) => [key.substring(5), value]));
29
+ export async function evaluateMongo(client, code, filename = '[no file]') {
30
+ const context = createContext({ __env });
31
+ const bus = new EventEmitter();
32
+ // console.log(`Client status! ${(<{ topology: { isConnected: () => boolean } } & MongoClient>client)?.topology?.isConnected() ? '' : 'not'} connected`)
33
+ const cliServiceProvider = new CliServiceProvider(client, bus, {
34
+ productName: 'MIRI: Migration manager',
35
+ productDocsLink: 'https://example.com/',
36
+ });
37
+ const instanceState = new ShellInstanceState(cliServiceProvider);
38
+ instanceState.setCtx(context);
39
+ instanceState.setEvaluationListener({
40
+ onPrint(values, type) {
41
+ return print(values, type);
42
+ },
43
+ onPrompt() {
44
+ throw new Error('Prompt isn\'t supported');
45
+ },
46
+ onLoad() {
47
+ throw new Error('Load isn\'t supported');
48
+ },
49
+ async onExit() {
50
+ throw new Error('Exit isn\'t supported');
51
+ },
52
+ });
59
53
  const output = await new ShellEvaluator(instanceState)
60
- .customEval(originalEval, `${code};`, context, filename);
61
- // print([output])
54
+ .customEval(runInContext, `${code};`, context, filename);
62
55
  return output.rawValue;
63
56
  }
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
57
  export async function evaluateJs(code, identifier = '[no file]') {
72
- const context = createContext({ console });
58
+ const context = createContext({ __env, console });
73
59
  const module = new SourceTextModule(code, { context, identifier });
74
- await module.link(linker);
60
+ await module.link(async function linker(specifier, referencingModule) {
61
+ return import(specifier).then((module) => {
62
+ return new SyntheticModule(['default'], function () {
63
+ this.setExport('default', module.default);
64
+ }, { context: referencingModule.context });
65
+ });
66
+ });
75
67
  await module.evaluate()
76
68
  .catch((error) => {
77
69
  error.message += ` # at file ${identifier}`;
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 { join } 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;
@@ -69,23 +76,25 @@ export default class Migrator {
69
76
  localPatches.sort(sortPatches);
70
77
  return localPatches;
71
78
  }
72
- async applyPatchContent(content, wrap = true) {
79
+ async applyPatchContent(content, filename, wrap = true) {
73
80
  if (!content?.body) {
74
81
  return -1;
75
82
  }
76
83
  const code = Buffer.from(content.body, 'base64').toString();
77
- return evaluateMongo(wrap ? `(${code})();` : code)
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) {
93
+ const filename = `${patch.group}/${patch.name}`;
85
94
  console.group(`Patch ${patch.group} / ${patch.name}...`);
86
95
  if (patch.status === PatchStatus.Removed) {
87
96
  console.group('Reverting changes');
88
- const result = await this.applyPatchContent(patch.remoteContent?.down);
97
+ const result = await this.applyPatchContent(patch.remoteContent?.down, filename);
89
98
  console.log(result === -1 ? 'Revert script not found' : 'done');
90
99
  console.groupEnd();
91
100
  await this.#collection.deleteOne({ _id: patch._id });
@@ -94,23 +103,23 @@ export default class Migrator {
94
103
  }
95
104
  if (patch.status === PatchStatus.Changed) {
96
105
  console.group('Reverting changes');
97
- const result = await this.applyPatchContent(patch.remoteContent?.down);
106
+ const result = await this.applyPatchContent(patch.remoteContent?.down, filename);
98
107
  console.log(result === -1 ? 'Revert script not found' : 'done');
99
108
  console.groupEnd();
100
109
  patch.status = PatchStatus.New;
101
110
  }
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');
111
+ if (all || degraded || patch.status === PatchStatus.New) {
112
+ console.group(colors.white('Testing migration'));
113
+ const test = await this.applyPatchContent(patch.content.test, filename);
114
+ console.log(colors.white(`degradation level: ${test}`));
115
+ if (all || test !== 0) {
116
+ console.group(colors.cyan('Applying migration...'));
117
+ await this.applyPatchContent(patch.content.up, filename);
118
+ console.log(colors.green('Done'));
110
119
  console.groupEnd();
111
120
  }
112
121
  else {
113
- console.log('nothing to apply');
122
+ console.log(colors.green('nothing to apply'));
114
123
  }
115
124
  console.groupEnd();
116
125
  }
@@ -126,7 +135,7 @@ export default class Migrator {
126
135
  }
127
136
  }
128
137
  async getLocalPatches(raw = false, group = '', name = '') {
129
- const migrationsDir = await realpath(join(process.cwd(), this.#options.localMigrations ?? 'migrations'));
138
+ const migrationsDir = await realpath(resolve(process.cwd(), this.#options.localMigrations ?? 'migrations'));
130
139
  console.debug(`Reading ${migrationsDir}...`);
131
140
  const files = await readdir(migrationsDir, { recursive: true, withFileTypes: true });
132
141
  const patches = [];
@@ -134,10 +143,15 @@ export default class Migrator {
134
143
  if (!file.isFile()) {
135
144
  continue;
136
145
  }
146
+ const extension = extname(file.name);
147
+ if (!['.js', '.json'].includes(extension)) {
148
+ continue;
149
+ }
137
150
  const patchObject = {
138
151
  _id: new ObjectId(),
139
152
  group: file.path.substring(migrationsDir.length + 1),
140
153
  name: file.name,
154
+ title: basename(file.name, extension),
141
155
  content: {},
142
156
  };
143
157
  if (group ? patchObject.group !== group : ['init', 'indexes'].includes(patchObject.group)) {
@@ -147,7 +161,7 @@ export default class Migrator {
147
161
  continue;
148
162
  }
149
163
  patches.push(patchObject);
150
- const patchContent = await readFile(join(file.path, file.name));
164
+ const patchContent = await readFile(resolve(process.cwd(), file.path, file.name));
151
165
  if (raw) {
152
166
  patchObject.raw = patchContent.toString('base64');
153
167
  continue;
@@ -201,6 +215,66 @@ export default class Migrator {
201
215
  patches.sort(sortPatches);
202
216
  return patches;
203
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
+ }
204
278
  async init() {
205
279
  const localInits = await this.getLocalPatches(true, 'init');
206
280
  const remoteInits = await this.getRemotePatches('init');
@@ -218,7 +292,7 @@ export default class Migrator {
218
292
  continue;
219
293
  }
220
294
  console.log('applying');
221
- await this.applyPatchContent({ body: patch.raw, hash: '' }, false);
295
+ await this.applyPatchContent({ body: patch.raw, hash: '' }, `${patch.group}/${patch.name}`, false);
222
296
  await this.#collection.insertOne({
223
297
  group: patch.group,
224
298
  name: patch.name,
@@ -232,7 +306,7 @@ export default class Migrator {
232
306
  const patches = await (remote ? this.getRemotePatches() : this.diff());
233
307
  for (const patch of patches) {
234
308
  patch.status = patch.status ?? PatchStatus.Ok;
235
- const degradation = await this.applyPatchContent(patch.content.test);
309
+ const degradation = await this.applyPatchContent(patch.content.test, `${patch.group}/${patch.name}`);
236
310
  patch.degradation = degradation === -1 ? '-' : degradation;
237
311
  if (degradation > 0 && [PatchStatus.Ok, PatchStatus.Updated].includes(patch.status)) {
238
312
  patch.status = PatchStatus.Degraded;
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
- return client ?? (client = await new MongoClient(uri).connect());
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.5",
4
+ "version": "1.1.7",
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"