diff --git a/lib/ContentPluginModule.js b/lib/ContentPluginModule.js index 528f974..f92ac56 100644 --- a/lib/ContentPluginModule.js +++ b/lib/ContentPluginModule.js @@ -348,6 +348,148 @@ class ContentPluginModule extends AbstractApiModule { return pkg } + /** + * Loads JSON data into relevant directories + */ + async writeCourseData () { + return Promise.all(Object.keys(this.courseData).map(async (type) => { + const { dir, fileName, data } = this.courseData[type] + if (data) { + const filePath = path.join(dir, fileName) + const pathDir = await fs.access(dir).then(() => true).catch(() => false) + if (!pathDir) await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(filePath, JSON.stringify(data, null, 2)) + } + })); + } + + /** + * Run grunt task + * @return {Promise} + */ + runGruntTask (subTask, { outputDir, captureDir, file }) { + const args = [ + 'npx grunt migration:' + subTask, + outputDir ? `--outputdir=${outputDir}` : '', + captureDir ? `--capturedir=${captureDir}` : '', + file ? `--file=${file}` : '' + ].filter(Boolean).join(' '); + return this.execPromise(args); + } + + async execPromise (task) { + this.log('debug', 'EXEC', task) + return new Promise((resolve, reject) => { + exec(task, { cwd: this.framework.path }, (error, stdout, stderr) => { + if (stdout) this.log('debug', 'EXEC_STDOUT', task, stdout) + if (stderr) this.log('debug', 'EXEC_STDERR', task, stderr) + error ? reject(error) : resolve(stdout) + }) + }) + } + + /** + * Finds all courses using a plugin, saves data to the course directory and runs grunt:migration capture + * @param {String} _id + name of the plugin being updated + * @return Returns promise to find/save course data and run grunt:migration capture + */ + async capturePluginCourses (_id, name) { + const content = await this.app.waitForModule('content') + const courses = await this.getPluginUses(_id) + if (!courses.length) return + + await Promise.all(courses.map(async c => { + const contentItems = await content.find({ _courseId: c._id }) + if (!contentItems.length) return + + const outputDir = path.join(this.framework.path, 'src/course', c._id.toString()) + const courseDir = path.join(outputDir, 'course') + const langDir = path.join(courseDir, 'en') + + this.courseData = { + course: { dir: langDir, fileName: 'course.json', data: undefined }, + config: { dir: courseDir, fileName: 'config.json', data: undefined }, + contentObject: { dir: langDir, fileName: 'contentObjects.json', data: [] }, + article: { dir: langDir, fileName: 'articles.json', data: [] }, + block: { dir: langDir, fileName: 'blocks.json', data: [] }, + component: { dir: langDir, fileName: 'components.json', data: [] } + } + + contentItems.forEach(item => { + const type = item._type === 'page' || item._type === 'menu' ? 'contentObject' : item._type; + if (typeof this.courseData[type].data === 'object') { + this.courseData[type].data.push(item); + return + } + this.courseData[type].data = item + }) + + await this.writeCourseData() + const folderName = `${c._id}-migrations` + const captureDir = path.join('./', folderName) + const opts = { outputDir: outputDir, captureDir: captureDir, file: name } + + this.capturedCourses.push({ + outputDir: outputDir, + captureDir: captureDir, + courseDir, + courseId: c._id, + }) + + await this.runGruntTask('capture', opts) + })); + } + + /** + * Takes an array of filePaths and extracts the JSON data from each file + * @param {String} filePaths array of file paths to JSON files + * @return Returns all course data in an array + */ + async loadJsonData (filePaths) { + const data = []; + for (const filePath of filePaths) { + const content = await fs.readFile(filePath, 'utf8') + const data = JSON.parse(content) + if (Array.isArray(data)) { + data.push(...data) + } else { + data.push(data) + } + } + return data; + } + + /** + * Migrates each course within this.capturedCourses + * @param {String} _id The _id for the plugin to update and name of the plugin + * @return Resolves with logging confirmation + */ + async migratePluginCourses (_id, name) { + if (!this.capturedCourses.length) return + const content = await this.app.waitForModule('content') + await Promise.all(this.capturedCourses.map(async c => { + const opts = { outputDir: c.outputDir, captureDir: c.captureDir, file: name } + await this.runGruntTask('migrate', opts) + + const filePaths = Array.from(await new Promise(resolve => { + globs([ + '**/*.json', + '*.json' + ], { cwd: path.join(c.outputDir, 'course'), absolute: true }, (err, files) => resolve(err ? null : files)) + })).filter((file, index, self) => self.indexOf(file) === index) + const contentItems = await this.loadJsonData(filePaths); + + contentItems.forEach(async item => { + await content.update({ _id: item._id }, item) + }); + + const captureDir = path.join(this.framework.path, c.captureDir) + await fs.rm(c.outputDir, { recursive: true, force: true }) + await fs.rm(captureDir, { recursive: true, force: true }) + return this.log('info', `successfully updated course ${c.courseId} with plugin ${name}`) + })); + } + /** * Updates a single plugin * @param {String} _id The _id for the plugin to update @@ -355,8 +497,10 @@ class ContentPluginModule extends AbstractApiModule { */ async updatePlugin (_id) { const [{ name }] = await this.find({ _id }) + await this.capturePluginCourses(_id, name) const [pluginData] = await this.framework.runCliCommand({ plugins: [name] }) const p = await this.update({ name }, pluginData._sourceInfo) + await this.migratePluginCourses(_id, name) this.log('info', `successfully updated plugin ${p.name}@${p.version}`) return p }