diff --git a/grunt/tasks/migration.js b/grunt/tasks/migration.js new file mode 100644 index 000000000..9d06b6e72 --- /dev/null +++ b/grunt/tasks/migration.js @@ -0,0 +1,149 @@ +module.exports = function(grunt) { + + const Helpers = require('../helpers')(grunt); + const globs = require('globs'); + const path = require('path'); + const fs = require('fs-extra'); + const _ = require('underscore'); + const minimatch = require('minimatch'); + + function unix(path) { + return path.replace(/\\/g, '/'); + } + + function dressPathIndex(fileItem) { + return { + ...fileItem.item, + __index__: fileItem.index, + __path__: unix(fileItem.file.path) + }; + } + + function undressPathIndex(object) { + const clone = { ...object }; + delete clone.__index__; + delete clone.__path__; + return clone; + } + + grunt.registerTask('migration', 'Migrate from one version to another', function(mode) { + const next = this.async(); + const buildConfig = Helpers.generateConfigData(); + const fileNameIncludes = grunt.option('file'); + + (async function() { + const migrations = await import('adapt-migrations'); + const logger = migrations.Logger.getInstance(); + const cwd = process.cwd(); + const outputPath = path.join(cwd, './migrations/'); + const cache = new migrations.CacheManager(); + const cachePath = await cache.getCachePath({ + outputPath: buildConfig.outputdir, + tempPath: outputPath + }); + + const framework = Helpers.getFramework(); + logger.debug(`Using ${framework.useOutputData ? framework.outputPath : framework.sourcePath} folder for course data...`); + + const plugins = framework.getPlugins().getAllPackageJSONFileItems().map(fileItem => fileItem.item); + const migrationScripts = Array.from(await new Promise(resolve => { + globs([ + '*/*/migrations/**/*.js', + 'core/migrations/**/*.js' + ], { cwd: path.join(cwd, './src/'), absolute: true }, (err, files) => resolve(err ? null : files)); + })).filter(filePath => { + if (!fileNameIncludes) return true; + return minimatch(filePath, '**/' + fileNameIncludes) || filePath.includes(fileNameIncludes); + }); + + if (!migrationScripts.length) { + console.log('No migration scripts found'); + return next(); + } + + await migrations.load({ + cachePath, + scripts: migrationScripts, + logger + }); + + if (mode === 'capture') { + + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + const languages = framework.getData().languages.map((language) => language.name); + const languageFile = path.join(outputPath, 'captureLanguages.json'); + fs.writeJSONSync(languageFile, languages); + languages.forEach(async (language, index) => { + logger.debug(`Migration -- Capture ${language}`); + const data = framework.getData(); + // get all items from config.json file and all language files, append __index__ and __path__ to each item + const content = [ + ...data.configFile.fileItems, + ...data.languages[index].getAllFileItems() + ].map(dressPathIndex); + const captured = await migrations.capture({ content, fromPlugins: plugins, logger }); + const outputFile = path.join(outputPath, `capture_${language}.json`); + fs.writeJSONSync(outputFile, captured); + }); + + logger.output(outputPath, 'capture'); + return next(); + } + + if (mode === 'migrate') { + try { + const languagesFile = path.join(outputPath, 'captureLanguages.json'); + const languages = fs.readJSONSync(languagesFile); + + for (const language of languages) { + logger.debug(`Migration -- Migrate ${language}`); + const Journal = migrations.Journal; + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + const outputFile = path.join(outputPath, `capture_${language}.json`); + const { content, fromPlugins } = fs.readJSONSync(outputFile); + const originalFromPlugins = JSON.parse(JSON.stringify(fromPlugins)); + const journal = new Journal({ + logger, + data: { + content, + fromPlugins, + originalFromPlugins, + toPlugins: plugins + } + }); + await migrations.migrate({ journal, logger }); + + // group all content items by path + const outputFilePathItems = _.groupBy(content, '__path__'); + // sort items inside each path + Object.values(outputFilePathItems).forEach(outputFile => outputFile.sort((a, b) => a.__index__ - b.__index__)); + // get paths + const outputFilePaths = Object.keys(outputFilePathItems); + + outputFilePaths.forEach(outputPath => { + const outputItems = outputFilePathItems[outputPath]; + if (!outputItems?.length) return; + const isSingleObject = (outputItems.length === 1 && outputItems[0].__index__ === null); + const stripped = isSingleObject + ? undressPathIndex(outputItems[0]) // config.json, course.json + : outputItems.map(undressPathIndex); // contentObjects.json, articles.json, blocks.json, components.json + fs.writeJSONSync(outputPath, stripped, { replacer: null, spaces: 2 }); + }); + } + } catch (error) { + logger.error(error.stack); + } + logger.output(outputPath, 'migrate'); + return next(); + } + + if (mode === 'test') { + await migrations.test({ logger }); + return next(); + } + + return next(); + })(); + }); + +}; diff --git a/package-lock.json b/package-lock.json index 881e0c912..2379af8c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@rollup/plugin-terser": "^0.4.0", "@types/backbone": "^1.4.14", "@types/jquery": "^3.5.16", + "adapt-migrations": "^1.0.0", "async": "^3.2.2", "babel-plugin-transform-amd-to-es6": "^0.6.1", "babel-plugin-transform-react-templates": "^0.1.0", @@ -3729,6 +3730,51 @@ "node": ">=0.4.0" } }, + "node_modules/adapt-migrations": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/adapt-migrations/-/adapt-migrations-1.0.0.tgz", + "integrity": "sha512-JvtT0Iyo47Q1tbj4Gmqsa5HwPxL1fs7LxS1/HbXIJBJqKhilc5khLER+M6L92QA0jRXsK30KYCKv1CWzdus99Q==", + "dependencies": { + "fs-extra": "^11.1.1", + "globs": "^0.1.4", + "semver": "^7.5.4" + } + }, + "node_modules/adapt-migrations/node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/adapt-migrations/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/adapt-migrations/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", @@ -24943,6 +24989,42 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "adapt-migrations": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/adapt-migrations/-/adapt-migrations-1.0.0.tgz", + "integrity": "sha512-JvtT0Iyo47Q1tbj4Gmqsa5HwPxL1fs7LxS1/HbXIJBJqKhilc5khLER+M6L92QA0jRXsK30KYCKv1CWzdus99Q==", + "requires": { + "fs-extra": "^11.1.1", + "globs": "^0.1.4", + "semver": "^7.5.4" + }, + "dependencies": { + "fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + } + } + }, "agent-base": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", diff --git a/package.json b/package.json index 7712b7758..af957fce6 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@rollup/plugin-terser": "^0.4.0", "@types/backbone": "^1.4.14", "@types/jquery": "^3.5.16", + "adapt-migrations": "^1.0.0", "async": "^3.2.2", "babel-plugin-transform-amd-to-es6": "^0.6.1", "babel-plugin-transform-react-templates": "^0.1.0",