Skip to content

Commit 98f9a90

Browse files
committed
README and improved tests
1 parent 7646abd commit 98f9a90

9 files changed

+200
-29
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,44 @@
11
# migrations
22
Mongoose Migration Framework
3+
4+
### Getting Started
5+
6+
```javascript
7+
// Import the migration framework. Put this import **before** any other imports,
8+
// except dotenv or other environment configuration
9+
const migrations = require('@mongoosejs/migrations');
10+
11+
const Character = require('./models/character');
12+
const mongoose = require('mongoose');
13+
14+
run().catch(err => { console.error(err); process.exit(-1); });
15+
16+
async function run() {
17+
await mongoose.connect('mongodb://localhost:27017/migrations_examples');
18+
// Tell the migration framework that a migration is starting.
19+
// Set `restart` option if you want to restart a previously failed migration.
20+
await migrations.startMigration({ restart: process.env.RESTART });
21+
22+
try {
23+
// Put any migration logic here - update, save, eachAsync, etc.
24+
await Character.updateMany({}, { $set: { ship: 'USS Enterprise' } });
25+
} finally {
26+
// Make sure to always call `endMigration()`, even if errors occur
27+
await migrations.endMigration();
28+
await mongoose.disconnect();
29+
}
30+
}
31+
```
32+
33+
### UI Setup
34+
35+
You can run the Migration Framework UI as an executable using the provided `migration-ui` command:
36+
37+
```
38+
env PORT=3001 env MONGO_URI="mongodb://localhost:27017/mydb" ./node_modules/.bin/migration-ui
39+
```
40+
41+
You can also import the Migration Framework UI to your Express app:
42+
43+
**Note:** you are responsible for securing the Migration Framework UI. There is currently no restrictions on the UI or API: anyone that can access the Migration Framework UI's URL can read migration data.
44+
**Note:** do **not** run the Migration Framework UI in the same process that runs migrations.

bin/ui.js renamed to bin/migration-ui.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ const ui = require('../src/ui');
66

77
const app = express();
88

9+
const port = process.env.PORT || 3001;
10+
911
mongoose.connect(process.env.MONGO_URL || 'mongodb://localhost:27017/migrationstest');
1012

1113
app.use('/', ui(mongoose, express));
1214

13-
const server = app.listen(3001, err => {
15+
const server = app.listen(port, err => {
1416
if (err != null) {
1517
console.error(err);
1618
process.exit(-1);
1719
}
18-
console.log('Listening on 3001');
20+
console.log('Listening on ' + port);
1921
});

bin/migrations.js

Lines changed: 0 additions & 19 deletions
This file was deleted.

index.js

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ exports.initMigrationModels = function initMigrationModels(conn) {
4848
exports.initMigrationFramework = function initMigrationFramework(conn) {
4949
conn = conn || mongoose.connection;
5050
didInit = true;
51+
if (didInit) {
52+
return;
53+
}
5154

5255
exports.initMigrationModels(conn);
5356

5457
mongoose.plugin(function(schema) {
55-
schema.pre(writeOps, { query: true, document: false }, async function(next) {
58+
schema.pre(writeOps, { query: true, document: false }, async function migrationPreWriteOp(next) {
5659
if (migration == null) {
5760
return;
5861
}
@@ -61,16 +64,17 @@ exports.initMigrationFramework = function initMigrationFramework(conn) {
6164
Object.assign(opFilter, {
6265
migrationId: migration._id,
6366
modelName: this.model.modelName,
64-
opName: this.op
67+
opName: this.op,
68+
parameters: {
69+
filter: this.getFilter(),
70+
update: this.getUpdate()
71+
}
6572
});
6673
const res = await Operation.findOneAndUpdate(
6774
opFilter,
6875
{
6976
$setOnInsert: {
70-
parameters: {
71-
filter: this.getFilter(),
72-
update: this.getUpdate()
73-
}
77+
status: 'in_progress'
7478
}
7579
},
7680
{ new: true, upsert: true, rawResult: true }
@@ -90,7 +94,7 @@ exports.initMigrationFramework = function initMigrationFramework(conn) {
9094
}
9195
});
9296

93-
schema.post(writeOps, { query: true, document: false }, async function(res) {
97+
schema.post(writeOps, { query: true, document: false }, async function migrationPostWriteOp(res) {
9498
if (migration == null) {
9599
return;
96100
}
@@ -111,6 +115,68 @@ exports.initMigrationFramework = function initMigrationFramework(conn) {
111115

112116
await op.save();
113117
});
118+
119+
schema.pre('save', async function migrationPreSave(next) {
120+
if (migration == null) {
121+
return;
122+
}
123+
124+
const opFilter = migration.lastOperationId ? { _id: { $gt: migration.lastOperationId } } : {};
125+
Object.assign(opFilter, {
126+
migrationId: migration._id,
127+
modelName: this.constructor.modelName,
128+
opName: 'save',
129+
parameters: {
130+
where: this.isNew ? null : this.$__delta()[0],
131+
isNew: this.isNew,
132+
changes: this.getChanges()
133+
}
134+
});
135+
136+
const res = await Operation.findOneAndUpdate(
137+
opFilter,
138+
{
139+
$setOnInsert: {
140+
status: 'in_progress'
141+
}
142+
},
143+
{ new: true, upsert: true, rawResult: true }
144+
);
145+
146+
const op = res.value;
147+
148+
migration.lastOperationId = op._id;
149+
await migration.save();
150+
151+
debug(`${this.constructor.modelName}.save`);
152+
153+
mongooseObjToOp.set(this, op);
154+
155+
if (res.lastErrorObject.updatedExisting) {
156+
next(mongoose.skipMiddlewareFunction(op.result));
157+
}
158+
});
159+
160+
schema.post('save', async function migrationPostSave() {
161+
if (migration == null) {
162+
return;
163+
}
164+
165+
const op = mongooseObjToOp.get(this);
166+
167+
if (op == null) {
168+
return;
169+
}
170+
171+
if (op.status !== 'complete') {
172+
op.endedAt = new Date();
173+
op.status = 'complete';
174+
}
175+
176+
debug(`${this.constructor.modelName}.save`);
177+
178+
await op.save();
179+
});
114180
});
115181
};
116182

@@ -263,6 +329,7 @@ exports.eachAsync = async function eachAsync(model, options, fn) {
263329
opName: 'eachAsync',
264330
userFunctionName: options?.name || fn.name
265331
});
332+
266333
const op = await Operation.findOneAndUpdate(
267334
opFilter,
268335
{

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"lint": "eslint .",
66
"test": "mocha test/*.test.js"
77
},
8+
"bin": {
9+
"migration-ui": "./bin/migration-ui.js"
10+
},
811
"dependencies": {
912
"debug": "4.x"
1013
},

test/eachAsync.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,14 @@ describe('eachAsync', function() {
7979

8080
migration = await migrations.restartMigration({ name: 'test' });
8181
count = 0;
82+
let eachAsyncId = null;
8283
await migrations.eachAsync(TestModel, async function addShip(doc) {
84+
if (eachAsyncId == null) {
85+
eachAsyncId = migration.lastOperationId;
86+
}
8387
++count;
8488

85-
const op = await mongoose.model('_Operation').findById(migration.lastOperationId);
89+
const op = await mongoose.model('_Operation').findById(eachAsyncId);
8690
assert.equal(op.state.current, count + 2);
8791
doc.ship = 'USS Enterprise';
8892
await doc.save();

test/save.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const { before, describe, it, afterEach, beforeEach } = require('mocha');
5+
const mongoose = require('mongoose');
6+
const migrations = require('../');
7+
8+
describe('save', function() {
9+
let TestModel;
10+
let migration;
11+
12+
before(function() {
13+
mongoose.deleteModel(/Test/);
14+
15+
TestModel = mongoose.model('Test', mongoose.Schema({
16+
name: String,
17+
email: String
18+
}));
19+
});
20+
21+
beforeEach(async function() {
22+
await TestModel.collection.deleteMany({});
23+
24+
migration = await migrations.startMigration({ name: 'test' });
25+
});
26+
27+
afterEach(() => migrations.endMigration());
28+
afterEach(() => migrations.models.Migration.deleteMany({ name: 'test' }));
29+
30+
it('stores the result', async function() {
31+
const doc = new TestModel({ name: 'John Smith' });
32+
await doc.save();
33+
34+
const operations = await mongoose.model('_Operation').find({ migrationId: migration._id });
35+
assert.equal(operations.length, 1);
36+
assert.equal(operations[0].status, 'complete');
37+
assert.deepEqual(operations[0].parameters.changes, { $set: { name: 'John Smith' } });
38+
});
39+
40+
it('skips if same op already exists', async function() {
41+
await mongoose.model('_Operation').create({
42+
migrationId: migration._id,
43+
modelName: 'Test',
44+
opName: 'save',
45+
status: 'complete',
46+
parameters: {
47+
where: null,
48+
isNew: true,
49+
changes: { $set: { name: 'John Smith' } }
50+
}
51+
});
52+
53+
const doc = new TestModel({
54+
name: 'John Smith'
55+
});
56+
await doc.save();
57+
58+
const operations = await mongoose.model('_Operation').find({ migrationId: migration._id });
59+
assert.equal(operations.length, 1);
60+
assert.equal(operations[0].status, 'complete');
61+
62+
const count = await TestModel.countDocuments({});
63+
assert.equal(count, 0);
64+
});
65+
});

test/updates.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ describe('updates', function() {
6262
modelName: 'Test',
6363
opName: 'updateOne',
6464
status: 'complete',
65+
parameters: {
66+
filter: { name: 'John Smith' },
67+
update: { name: 'John Smythe' }
68+
},
6569
result: {
6670
matchedCount: 1,
6771
modifiedCount: 1,

ui.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
3+
module.exports = require('./src/ui');

0 commit comments

Comments
 (0)