@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 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 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
+ 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(() => 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,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
- process.stdout.write(prepare(printable, value.type ?? type));
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
- 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.6",
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"