adapt-migrations 1.0.0
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/.eslintignore +1 -0
- package/.eslintrc.json +14 -0
- package/README.md +43 -0
- package/api/commands.js +33 -0
- package/api/data.js +26 -0
- package/api/describe.js +12 -0
- package/api/errors.js +28 -0
- package/api/plugins.js +44 -0
- package/api/tests.js +57 -0
- package/api/where.js +46 -0
- package/examples/migrations.js +159 -0
- package/examples/script.js +78 -0
- package/index.js +62 -0
- package/lib/CacheManager.js +95 -0
- package/lib/Journal.js +303 -0
- package/lib/Logger.js +91 -0
- package/lib/Task.js +381 -0
- package/lib/TaskContext.js +27 -0
- package/lib/TaskTest.js +19 -0
- package/lib/lifecycle.js +50 -0
- package/package.json +18 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
package/.eslintrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# adapt-migrations
|
|
2
|
+
|
|
3
|
+
### Todos
|
|
4
|
+
https://github.com/cgkineo/adapt-migrations/issues/1
|
|
5
|
+
|
|
6
|
+
### Commands API
|
|
7
|
+
https://github.com/cgkineo/adapt-migrations/blob/master/api/commands.js
|
|
8
|
+
* `load({ cwd, cachePath, scripts })` - loads all migration tasks
|
|
9
|
+
* `capture({ cwd, content, fromPlugins })` - captures current plugins and content
|
|
10
|
+
* `migrate({ cwd, toPlugins })` - migrates content from capture to new plugins
|
|
11
|
+
* `test({ cwd })` - tests the migrations with dummy content
|
|
12
|
+
|
|
13
|
+
### Migration script API
|
|
14
|
+
Functions:
|
|
15
|
+
* `describe(description, describeFunction)` Describe a migration
|
|
16
|
+
* `whereContent(description, contentFilterFunction)` Limit when the migration runs, return true/false/throw Error
|
|
17
|
+
* `whereFromPlugin(description, fromPluginFilterFunction)` Limit when the migration runs, return true/false/throw Error
|
|
18
|
+
* `whereToPlugin(description, toPluginFilterFunction)` Limit when the migration runs, return true/false/throw Error
|
|
19
|
+
* `mutateContent(contentFunction)` Change content, return true/false/throw Error
|
|
20
|
+
* `checkContent(contentFunction)` Check content, return true/false/throw Error
|
|
21
|
+
* `throwError(description)` Throw an error
|
|
22
|
+
* `testSuccessWhere({ fromPlugins, toPlugins, content })` Supply some tests content which should end in success
|
|
23
|
+
* `testStopWhere({ fromPlugins, toPlugins, content })` Supply some tests content which should end prematurely
|
|
24
|
+
* `testErrorWhere({ fromPlugins, toPlugins, content })` Supply some tests content which will trigger an error
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
* `describeFunction = () => {}` Function body has a collection of migration script functions
|
|
28
|
+
* `contentFilterFunction = content => {}` Function body should return true/false/throw Error
|
|
29
|
+
* `fromPluginFilterFunction = fromPlugins => {}` Function body should return true/false/throw Error
|
|
30
|
+
* `toPluginFilterFunction = toPlugins => {}` Function body should return true/false/throw Error
|
|
31
|
+
* `contentFunction = content => { }` Function body should mutate or check the content, returning true/false/throw Error
|
|
32
|
+
* `fromPlugins = [{ name: 'quickNav , version: '1.0.0' }]` Test data describing the original plugins
|
|
33
|
+
* `toPlugins = [{ name: 'pageNav , version: '1.0.0' }]` Test data describing the destination plugins
|
|
34
|
+
* `content = [{ _id: 'c-05, ... }]` Test content for the course content
|
|
35
|
+
|
|
36
|
+
### Grunt Commands
|
|
37
|
+
```sh
|
|
38
|
+
grunt migration:capture # captures current plugins and content
|
|
39
|
+
# do plugin/fw updates
|
|
40
|
+
grunt migration:migrate # migrates content from capture to new plugins
|
|
41
|
+
grunt migration:test # tests the migrations with dummy content
|
|
42
|
+
grunt migration:test --file=adapt-contrib-text/migrations/text.js # tests the migrations with dummy content
|
|
43
|
+
```
|
package/api/commands.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Task from '../lib/Task.js'
|
|
2
|
+
|
|
3
|
+
export async function load ({ cwd = process.cwd(), scripts = [], cachePath, logger } = {}) {
|
|
4
|
+
return Task.load({
|
|
5
|
+
cwd,
|
|
6
|
+
scripts,
|
|
7
|
+
cachePath,
|
|
8
|
+
logger
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function capture ({ content, fromPlugins, logger }) {
|
|
13
|
+
return {
|
|
14
|
+
content,
|
|
15
|
+
fromPlugins,
|
|
16
|
+
logger
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function migrate ({ cwd = process.cwd(), journal, logger }) {
|
|
21
|
+
return Task.runApplicable({
|
|
22
|
+
cwd,
|
|
23
|
+
journal,
|
|
24
|
+
logger
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function test ({ cwd = process.cwd(), logger } = {}) {
|
|
29
|
+
return Task.runTests({
|
|
30
|
+
cwd,
|
|
31
|
+
logger
|
|
32
|
+
})
|
|
33
|
+
}
|
package/api/data.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { deferOrRunWrap, successStopOrErrorWrap } from '../lib/lifecycle.js'
|
|
2
|
+
|
|
3
|
+
export function mutateContent (description, callback) {
|
|
4
|
+
return deferOrRunWrap(function (context) {
|
|
5
|
+
return successStopOrErrorWrap('mutateContent', description, async () => {
|
|
6
|
+
return callback(context.content)
|
|
7
|
+
})
|
|
8
|
+
}, { description, type: 'action' })
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function checkContent (description, callback) {
|
|
12
|
+
return deferOrRunWrap(function (context) {
|
|
13
|
+
return successStopOrErrorWrap('checkContent', description, async () => {
|
|
14
|
+
context.journal.freeze()
|
|
15
|
+
let result
|
|
16
|
+
try {
|
|
17
|
+
result = await callback(context.content)
|
|
18
|
+
} catch (err) {
|
|
19
|
+
context.journal.unfreeze()
|
|
20
|
+
throw err
|
|
21
|
+
}
|
|
22
|
+
context.journal.unfreeze()
|
|
23
|
+
return result
|
|
24
|
+
})
|
|
25
|
+
}, { description, type: 'action' })
|
|
26
|
+
};
|
package/api/describe.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Task from '../lib/Task.js'
|
|
2
|
+
import Logger from './../lib/Logger.js'
|
|
3
|
+
|
|
4
|
+
export function describe (description, load) {
|
|
5
|
+
const logger = Logger.getInstance();
|
|
6
|
+
logger.info(`Describe -- ${description} -- Registered`)
|
|
7
|
+
if (Task.current) {
|
|
8
|
+
logger.error(`Describe -- Cannot nest describe statements -- ${description}`)
|
|
9
|
+
}
|
|
10
|
+
// eslint-disable-next-line no-new
|
|
11
|
+
new Task({ description, load })
|
|
12
|
+
};
|
package/api/errors.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { deferOrRunWrap, successStopOrErrorWrap } from '../lib/lifecycle.js'
|
|
2
|
+
import Logger from './../lib/Logger.js'
|
|
3
|
+
|
|
4
|
+
const logger = Logger.getInstance();
|
|
5
|
+
|
|
6
|
+
export function throwError (description) {
|
|
7
|
+
let error = description
|
|
8
|
+
if (!(description instanceof Error)) {
|
|
9
|
+
logger.error(`Errors -- ${description}`)
|
|
10
|
+
error = new Error(description)
|
|
11
|
+
} else {
|
|
12
|
+
description = description.message
|
|
13
|
+
}
|
|
14
|
+
return deferOrRunWrap(function (context) {
|
|
15
|
+
return successStopOrErrorWrap('throwError', description, async () => {
|
|
16
|
+
throw error
|
|
17
|
+
})
|
|
18
|
+
}, { type: 'action' })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ifErroredAsk (config) {
|
|
22
|
+
return deferOrRunWrap(function (context) {
|
|
23
|
+
return successStopOrErrorWrap('isErroredAsk', config.question, async () => {
|
|
24
|
+
if (!context.hasErrored) return true
|
|
25
|
+
// Ask a question
|
|
26
|
+
})
|
|
27
|
+
}, { type: 'error' })
|
|
28
|
+
};
|
package/api/plugins.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { deferOrRunWrap, successStopOrErrorWrap } from '../lib/lifecycle.js'
|
|
2
|
+
|
|
3
|
+
export function removePlugin (description, config) {
|
|
4
|
+
return deferOrRunWrap(function (context) {
|
|
5
|
+
return successStopOrErrorWrap('removePlugin', description, async () => {
|
|
6
|
+
if (!description || !config) throw new Error('removePlugin - incorrectly configured')
|
|
7
|
+
|
|
8
|
+
context.fromPlugins = context.fromPlugins.filter(plugin => plugin.name !== config.name)
|
|
9
|
+
|
|
10
|
+
return true
|
|
11
|
+
})
|
|
12
|
+
}, { description, type: 'action' })
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function addPlugin (description, config) {
|
|
16
|
+
return deferOrRunWrap(function (context) {
|
|
17
|
+
return successStopOrErrorWrap('addPlugin', description, async () => {
|
|
18
|
+
if (!description || !config) throw new Error('addPlugin - incorrectly configured')
|
|
19
|
+
|
|
20
|
+
const newPlugin = context.toPlugins.find(plugin => (plugin.name === config.name))
|
|
21
|
+
if (!newPlugin) throw new Error(`addPlugin - ${config.name} not found`)
|
|
22
|
+
context.fromPlugins.push(newPlugin)
|
|
23
|
+
|
|
24
|
+
return true
|
|
25
|
+
})
|
|
26
|
+
}, { description, type: 'action' })
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function updatePlugin (description, config) {
|
|
30
|
+
return deferOrRunWrap(function (context) {
|
|
31
|
+
return successStopOrErrorWrap('updatePlugin', description, async () => {
|
|
32
|
+
if (!description || !config) throw new Error('updatePlugin - incorrectly configured')
|
|
33
|
+
|
|
34
|
+
context.fromPlugins.forEach(plugin => {
|
|
35
|
+
if (plugin.name !== config.name) return
|
|
36
|
+
plugin.version = config.version
|
|
37
|
+
if (!config.framework) return
|
|
38
|
+
plugin.framework = config.framework
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return true
|
|
42
|
+
})
|
|
43
|
+
}, { description, type: 'action' })
|
|
44
|
+
};
|
package/api/tests.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import TaskTest from '../lib/TaskTest.js'
|
|
2
|
+
import { deferOrRunWrap } from '../lib/lifecycle.js'
|
|
3
|
+
import Logger from '../lib/Logger.js'
|
|
4
|
+
|
|
5
|
+
const logger = Logger.getInstance();
|
|
6
|
+
|
|
7
|
+
export function testSuccessWhere (description, {
|
|
8
|
+
fromPlugins,
|
|
9
|
+
toPlugins,
|
|
10
|
+
content
|
|
11
|
+
}) {
|
|
12
|
+
return deferOrRunWrap(() => {
|
|
13
|
+
logger.debug(`Tests -- testSuccessWhere ${description}`)
|
|
14
|
+
return new TaskTest({
|
|
15
|
+
description,
|
|
16
|
+
shouldRun: true,
|
|
17
|
+
fromPlugins,
|
|
18
|
+
toPlugins,
|
|
19
|
+
content
|
|
20
|
+
})
|
|
21
|
+
}, { type: 'test' })
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function testStopWhere (description, {
|
|
25
|
+
fromPlugins,
|
|
26
|
+
toPlugins,
|
|
27
|
+
content
|
|
28
|
+
}) {
|
|
29
|
+
return deferOrRunWrap(() => {
|
|
30
|
+
logger.debug(`Tests -- testStopWhere ${description}`)
|
|
31
|
+
return new TaskTest({
|
|
32
|
+
description,
|
|
33
|
+
shouldStop: true,
|
|
34
|
+
shouldRun: false,
|
|
35
|
+
fromPlugins,
|
|
36
|
+
toPlugins,
|
|
37
|
+
content
|
|
38
|
+
})
|
|
39
|
+
}, { type: 'test' })
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function testErrorWhere (description, {
|
|
43
|
+
fromPlugins,
|
|
44
|
+
toPlugins,
|
|
45
|
+
content
|
|
46
|
+
}) {
|
|
47
|
+
return deferOrRunWrap(() => {
|
|
48
|
+
logger.debug(`Tests -- testErrorWhere ${description}`)
|
|
49
|
+
return new TaskTest({
|
|
50
|
+
description,
|
|
51
|
+
shouldError: true,
|
|
52
|
+
fromPlugins,
|
|
53
|
+
toPlugins,
|
|
54
|
+
content
|
|
55
|
+
})
|
|
56
|
+
}, { type: 'test' })
|
|
57
|
+
};
|
package/api/where.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import semver from 'semver'
|
|
2
|
+
import { deferOrRunWrap, successStopOrErrorWrap } from '../lib/lifecycle.js'
|
|
3
|
+
|
|
4
|
+
export function whereContent (description, callback) {
|
|
5
|
+
return deferOrRunWrap(({ content }) => {
|
|
6
|
+
return successStopOrErrorWrap('whereContent', description, async () => {
|
|
7
|
+
return callback(content)
|
|
8
|
+
})
|
|
9
|
+
}, { description, type: 'where' })
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function whereFromPlugin (description, callbackOrConfig) {
|
|
13
|
+
return deferOrRunWrap(({ fromPlugins }) => {
|
|
14
|
+
return successStopOrErrorWrap('whereFromPlugin', description, async () => {
|
|
15
|
+
const isCallback = (typeof callbackOrConfig === 'function')
|
|
16
|
+
if (isCallback) {
|
|
17
|
+
const callback = callbackOrConfig
|
|
18
|
+
return callback(fromPlugins)
|
|
19
|
+
}
|
|
20
|
+
const config = callbackOrConfig
|
|
21
|
+
return fromPlugins.some(plugin => {
|
|
22
|
+
if (config.name && plugin.name !== config.name) return false
|
|
23
|
+
if (config.version && !semver.satisfies(plugin.version, config.version)) return false
|
|
24
|
+
return true
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
}, { description, type: 'where' })
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function whereToPlugin (description, callbackOrConfig) {
|
|
31
|
+
return deferOrRunWrap(({ toPlugins }) => {
|
|
32
|
+
return successStopOrErrorWrap('whereToPlugin', description, async () => {
|
|
33
|
+
const isCallback = (typeof callbackOrConfig === 'function')
|
|
34
|
+
if (isCallback) {
|
|
35
|
+
const callback = callbackOrConfig
|
|
36
|
+
return callback(toPlugins)
|
|
37
|
+
}
|
|
38
|
+
const config = callbackOrConfig
|
|
39
|
+
return toPlugins.some(plugin => {
|
|
40
|
+
if (config.name && plugin.name !== config.name) return false
|
|
41
|
+
if (config.version && !semver.satisfies(plugin.version, config.version)) return false
|
|
42
|
+
return true
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
}, { description, type: 'where' })
|
|
46
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module.exports = function(grunt) {
|
|
2
|
+
|
|
3
|
+
const Helpers = require('../helpers')(grunt);
|
|
4
|
+
const globs = require('globs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const _ = require('underscore');
|
|
8
|
+
|
|
9
|
+
function unix(path) {
|
|
10
|
+
return path.replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function dressPathIndex(fileItem) {
|
|
14
|
+
return {
|
|
15
|
+
...fileItem.item,
|
|
16
|
+
__index__: fileItem.index,
|
|
17
|
+
__path__: unix(fileItem.file.path)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function undressPathIndex(object) {
|
|
22
|
+
const clone = { ...object };
|
|
23
|
+
delete clone.__index__;
|
|
24
|
+
delete clone.__path__;
|
|
25
|
+
return clone;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
grunt.registerTask('migration', 'Migrate from on verion to another', function(mode) {
|
|
29
|
+
const next = this.async();
|
|
30
|
+
const buildConfig = Helpers.generateConfigData();
|
|
31
|
+
const fileNameIncludes = grunt.option('file');
|
|
32
|
+
|
|
33
|
+
(async function() {
|
|
34
|
+
const migrations = await import('adapt-migrations');
|
|
35
|
+
const logger = migrations.Logger.getInstance();
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
const outputPath = path.join(cwd, './migrations/');
|
|
38
|
+
const cache = new migrations.CacheManager();
|
|
39
|
+
const cachePath = await cache.getCachePath({
|
|
40
|
+
outputPath: buildConfig.outputdir,
|
|
41
|
+
tempPath: outputPath
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const framework = Helpers.getFramework();
|
|
45
|
+
logger.debug(`Using ${framework.useOutputData ? framework.outputPath : framework.sourcePath} folder for course data...`);
|
|
46
|
+
|
|
47
|
+
const plugins = framework.getPlugins().getAllPackageJSONFileItems().map(fileItem => fileItem.item);
|
|
48
|
+
const migrationScripts = Array.from(await new Promise(resolve => {
|
|
49
|
+
globs([
|
|
50
|
+
'*/*/migrations/**/*.js',
|
|
51
|
+
'core/migrations/**/*.js'
|
|
52
|
+
], { cwd: path.join(cwd, './src/'), absolute: true }, (err, files) => resolve(err ? null : files));
|
|
53
|
+
})).filter(filePath => {
|
|
54
|
+
if (!fileNameIncludes) return true;
|
|
55
|
+
return filePath.includes(fileNameIncludes);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await migrations.load({
|
|
59
|
+
cachePath,
|
|
60
|
+
scripts: migrationScripts,
|
|
61
|
+
logger
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (mode === 'capture') {
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath);
|
|
67
|
+
const languages = framework.getData().languages.map((language) => language.name);
|
|
68
|
+
const languageFile = path.join(outputPath, 'captureLanguages.json');
|
|
69
|
+
fs.writeJSONSync(languageFile, languages);
|
|
70
|
+
languages.forEach(async (language, index) => {
|
|
71
|
+
logger.debug(`Migration -- Capture ${language}`)
|
|
72
|
+
const data = framework.getData();
|
|
73
|
+
// get all items from config.json file and all language files, append __index__ and __path__ to each item
|
|
74
|
+
const content = [
|
|
75
|
+
...data.configFile.fileItems,
|
|
76
|
+
...data.languages[index].getAllFileItems()
|
|
77
|
+
].map(dressPathIndex);
|
|
78
|
+
const captured = await migrations.capture({ content, fromPlugins: plugins, logger });
|
|
79
|
+
const outputFile = path.join(outputPath, `capture_${language}.json`);
|
|
80
|
+
fs.writeJSONSync(outputFile, captured);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
logger.output(outputPath, 'capture');
|
|
84
|
+
return next();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (mode === 'migrate') {
|
|
88
|
+
try {
|
|
89
|
+
const languagesFile = path.join(outputPath, 'captureLanguages.json');
|
|
90
|
+
const languages = fs.readJSONSync(languagesFile);
|
|
91
|
+
|
|
92
|
+
for (const language of languages) {
|
|
93
|
+
logger.debug(`Migration -- Migrate ${language}`)
|
|
94
|
+
const Journal = migrations.Journal;
|
|
95
|
+
if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath);
|
|
96
|
+
const outputFile = path.join(outputPath, `capture_${language}.json`);
|
|
97
|
+
const { content, fromPlugins } = fs.readJSONSync(outputFile);
|
|
98
|
+
const originalFromPlugins = JSON.parse(JSON.stringify(fromPlugins));
|
|
99
|
+
const journal = new Journal({
|
|
100
|
+
logger,
|
|
101
|
+
data: {
|
|
102
|
+
content,
|
|
103
|
+
fromPlugins,
|
|
104
|
+
originalFromPlugins,
|
|
105
|
+
toPlugins: plugins,
|
|
106
|
+
},
|
|
107
|
+
supplementEntry: (entry, data) => {
|
|
108
|
+
entry._id = data[entry.keys[0]][entry.keys[1]]?._id ?? '';
|
|
109
|
+
entry._type = data[entry.keys[0]][entry.keys[1]]?._type ?? '';
|
|
110
|
+
if (entry._type && data[entry.keys[0]][entry.keys[1]]?.[`_${entry._type}`]) {
|
|
111
|
+
entry[`_${entry._type}`] = data[entry.keys[0]][entry.keys[1]]?.[`_${entry._type}`] ?? '';
|
|
112
|
+
}
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
await migrations.migrate({ journal, logger });
|
|
117
|
+
|
|
118
|
+
// Todo - {
|
|
119
|
+
// display changes success/failure and request user confirmation before completing
|
|
120
|
+
// move saving of content outside of the language loop
|
|
121
|
+
// only 1 confirmation per course rather than 1 per language
|
|
122
|
+
// output journal entries for revert
|
|
123
|
+
// }
|
|
124
|
+
|
|
125
|
+
// group all content items by path
|
|
126
|
+
const outputFilePathItems = _.groupBy(content, '__path__');
|
|
127
|
+
// sort items inside each path
|
|
128
|
+
Object.values(outputFilePathItems).forEach(outputFile => outputFile.sort((a, b) => a.__index__ - b.__index__));
|
|
129
|
+
// get paths
|
|
130
|
+
const outputFilePaths = Object.keys(outputFilePathItems);
|
|
131
|
+
|
|
132
|
+
outputFilePaths.forEach(outputPath => {
|
|
133
|
+
const outputItems = outputFilePathItems[outputPath];
|
|
134
|
+
if (!outputItems?.length) return;
|
|
135
|
+
const isSingleObject = (outputItems.length === 1 && outputItems[0].__index__ === null);
|
|
136
|
+
const stripped = isSingleObject
|
|
137
|
+
? undressPathIndex(outputItems[0]) // config.json, course.json
|
|
138
|
+
: outputItems.map(undressPathIndex); // contentObjects.json, articles.json, blocks.json, components.json
|
|
139
|
+
// console.log(journal.entries)
|
|
140
|
+
fs.writeJSONSync(outputPath, stripped, { replacer: null, spaces: 2 });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.error(error.stack);
|
|
145
|
+
}
|
|
146
|
+
logger.output(outputPath, 'migrate');
|
|
147
|
+
return next();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (mode === 'test') {
|
|
151
|
+
await migrations.test();
|
|
152
|
+
return next();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return next();
|
|
156
|
+
})();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Example migration script
|
|
2
|
+
|
|
3
|
+
import { describe, whereContent, whereFromPlugin, whereToPlugin, mutateContent, checkContent, throwError, ifErroredAsk, testSuccessWhere, testErrorWhere, testStopWhere } from 'adapt-migrations';
|
|
4
|
+
|
|
5
|
+
describe('update plugin from v6.1.4 to v6.1.5 and add "Ollie" to display title', async () => {
|
|
6
|
+
whereFromPlugin('text v6.1.4', { name: 'adapt-contrib-text', version: '<=6.1.4' });
|
|
7
|
+
whereContent('has configured displayTitles', async content =>
|
|
8
|
+
content.some(({ displayTitle }) => displayTitle)
|
|
9
|
+
);
|
|
10
|
+
mutateContent('change displayTitle', async content => {
|
|
11
|
+
const quicknavs = content.filter(({ displayTitle }) => displayTitle);
|
|
12
|
+
quicknavs.forEach(item => (item.displayTitle += ' ollie'));
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
checkContent('check everything is ok', async content => {
|
|
16
|
+
const isInvalid = content.some(({ displayTitle }) => displayTitle && !String(displayTitle).endsWith(' ollie'));
|
|
17
|
+
if (isInvalid) throw new Error('found displayTitle without ollie at the end');
|
|
18
|
+
return true;
|
|
19
|
+
});
|
|
20
|
+
updatePlugin('update text plugin', {name: 'adapt-contrib-text', version: '6.1.5', framework: '>=5.19.4'})
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('quicknav to pagenav', async () => {
|
|
24
|
+
addPlugin('add pagenav plugin', { name: 'pagenav', version: '1.0.0'});
|
|
25
|
+
whereFromPlugin('quicknav v1.0.0', { name: 'quicknav', version: '1.0.0' });
|
|
26
|
+
whereToPlugin('pagenav v1.0.0', { name: 'pagenav', version: '1.0.0' });
|
|
27
|
+
removePlugin('remove quicknav plugin', {name: 'quicknav'})
|
|
28
|
+
whereContent('has configured quicknavs', async content =>
|
|
29
|
+
content.some(({ _component }) => _component === 'quicknav')
|
|
30
|
+
);
|
|
31
|
+
mutateContent('change _component name', async content => {
|
|
32
|
+
const quicknavs = content.filter(({ _component }) => _component === 'quicknav');
|
|
33
|
+
quicknavs.forEach(item => (item._component = 'pagenav'));
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
checkContent('check everything is ok', async content => {
|
|
37
|
+
const isInvalid = content.some(({ isInvalid }) => isInvalid);
|
|
38
|
+
if (isInvalid) throw new Error('found invalid content attribute');
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
// TODO: handle errors with question, allow to run without ui
|
|
42
|
+
// ifErroredAsk({ question: 'Skip error', yes: 'Yes', no: 'No', defaultSkipError: true });
|
|
43
|
+
// TODO: modify stack traces one errors to refer to the original migration script rather than the cached one,keep map of cached files to original files
|
|
44
|
+
testSuccessWhere('Valid plugins and content', {
|
|
45
|
+
fromPlugins: [{ name: 'quicknav', version: '1.0.0' }],
|
|
46
|
+
toPlugins: [{ name: 'pagenav', version: '1.0.0' }],
|
|
47
|
+
content: [{ _component: 'quicknav' }]
|
|
48
|
+
});
|
|
49
|
+
testStopWhere('Invalid content', {
|
|
50
|
+
fromPlugins: [{ name: 'quicknav', version: '1.0.0' }],
|
|
51
|
+
toPlugins: [{ name: 'pagenav', version: '1.0.0' }],
|
|
52
|
+
content: [{ _component: 'quicknav1' }]
|
|
53
|
+
});
|
|
54
|
+
testStopWhere('Invalid origin plugins', {
|
|
55
|
+
fromPlugins: [{ name: 'quicknav', version: '0.1.0' }],
|
|
56
|
+
toPlugins: [{ name: 'pagenav', version: '0.1.0' }],
|
|
57
|
+
content: [{ _component: 'quicknav' }]
|
|
58
|
+
});
|
|
59
|
+
testStopWhere('Invalid destination plugins', {
|
|
60
|
+
content: [{ _component: 'quicknav' }],
|
|
61
|
+
fromPlugins: [{ name: 'quicknav', version: '1.0.0' }],
|
|
62
|
+
toPlugins: [{ name: 'pagenav', version: '0.1.0' }]
|
|
63
|
+
});
|
|
64
|
+
testErrorWhere('Has invalid configuration', {
|
|
65
|
+
fromPlugins: [{ name: 'quicknav', version: '1.0.0' }],
|
|
66
|
+
toPlugins: [{ name: 'pagenav', version: '1.0.0' }],
|
|
67
|
+
content: [{ _component: 'quicknav', isInvalid: true }]
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('where quicknav is weirdly configured', async () => {
|
|
72
|
+
checkContent('check everything is ok', async content => {
|
|
73
|
+
const isInvalid = content.some(({ isInvalid }) => isInvalid);
|
|
74
|
+
if (isInvalid) throw new Error('Something went wrong');
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
throwError('this is an error');
|
|
78
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
load,
|
|
3
|
+
capture,
|
|
4
|
+
migrate,
|
|
5
|
+
test
|
|
6
|
+
} from './api/commands.js'
|
|
7
|
+
import {
|
|
8
|
+
describe
|
|
9
|
+
} from './api/describe.js'
|
|
10
|
+
import {
|
|
11
|
+
whereContent,
|
|
12
|
+
whereFromPlugin,
|
|
13
|
+
whereToPlugin
|
|
14
|
+
} from './api/where.js'
|
|
15
|
+
import {
|
|
16
|
+
checkContent,
|
|
17
|
+
mutateContent
|
|
18
|
+
} from './api/data.js'
|
|
19
|
+
import {
|
|
20
|
+
ifErroredAsk,
|
|
21
|
+
throwError
|
|
22
|
+
} from './api/errors.js'
|
|
23
|
+
import {
|
|
24
|
+
testErrorWhere,
|
|
25
|
+
testStopWhere,
|
|
26
|
+
testSuccessWhere
|
|
27
|
+
} from './api/tests.js'
|
|
28
|
+
import {
|
|
29
|
+
updatePlugin,
|
|
30
|
+
removePlugin,
|
|
31
|
+
addPlugin
|
|
32
|
+
} from './api/plugins.js'
|
|
33
|
+
import Journal from './lib/Journal.js'
|
|
34
|
+
import CacheManager from './lib/CacheManager.js'
|
|
35
|
+
import Logger from './lib/Logger.js'
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
// commands
|
|
39
|
+
load,
|
|
40
|
+
capture,
|
|
41
|
+
migrate,
|
|
42
|
+
test,
|
|
43
|
+
// migration script api
|
|
44
|
+
describe,
|
|
45
|
+
whereContent,
|
|
46
|
+
whereFromPlugin,
|
|
47
|
+
whereToPlugin,
|
|
48
|
+
checkContent,
|
|
49
|
+
mutateContent,
|
|
50
|
+
ifErroredAsk,
|
|
51
|
+
throwError,
|
|
52
|
+
testErrorWhere,
|
|
53
|
+
testStopWhere,
|
|
54
|
+
testSuccessWhere,
|
|
55
|
+
updatePlugin,
|
|
56
|
+
removePlugin,
|
|
57
|
+
addPlugin,
|
|
58
|
+
// environment objects
|
|
59
|
+
Journal,
|
|
60
|
+
CacheManager,
|
|
61
|
+
Logger
|
|
62
|
+
}
|