adapt-migrations 1.0.0 → 1.1.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.
@@ -1,159 +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
- };
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
+ };
@@ -1,78 +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
- });
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 CHANGED
@@ -1,62 +1,70 @@
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
- }
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 {
34
+ getConfig,
35
+ getCourse,
36
+ getComponents
37
+ } from './api/helpers.js'
38
+ import Journal from './lib/Journal.js'
39
+ import CacheManager from './lib/CacheManager.js'
40
+ import Logger from './lib/Logger.js'
41
+
42
+ export {
43
+ // commands
44
+ load,
45
+ capture,
46
+ migrate,
47
+ test,
48
+ // migration script api
49
+ describe,
50
+ whereContent,
51
+ whereFromPlugin,
52
+ whereToPlugin,
53
+ checkContent,
54
+ mutateContent,
55
+ ifErroredAsk,
56
+ throwError,
57
+ testErrorWhere,
58
+ testStopWhere,
59
+ testSuccessWhere,
60
+ updatePlugin,
61
+ removePlugin,
62
+ addPlugin,
63
+ getConfig,
64
+ getCourse,
65
+ getComponents,
66
+ // environment objects
67
+ Journal,
68
+ CacheManager,
69
+ Logger
70
+ }