From fc53e00170af233d3e7315a33059548b508e2ddd Mon Sep 17 00:00:00 2001 From: joe-allen-89 <85872286+joe-allen-89@users.noreply.github.com> Date: Wed, 21 May 2025 10:01:55 +0100 Subject: [PATCH 1/4] New: migration scripts to run on plugin update --- lib/ContentPluginModule.js | 143 +++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/lib/ContentPluginModule.js b/lib/ContentPluginModule.js index 528f974..23a671e 100644 --- a/lib/ContentPluginModule.js +++ b/lib/ContentPluginModule.js @@ -348,6 +348,147 @@ class ContentPluginModule extends AbstractApiModule { return pkg } + /** + * Loads JSON data into relevant directories + */ + async loadJson () { + 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 }) { + return this.execPromise(`npx grunt migration:${subTask} --outputdir=${outputDir} --capturedir=${captureDir} --file=${file}`) + } + + 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 plugin = await this.find({ name: name }) + if (!plugin.length) return + 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.loadJson() + + 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 +496,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 } From 8f20dd98499a55b534c6167ca5b703a5410e21cb Mon Sep 17 00:00:00 2001 From: joe-allen-89 <85872286+joe-allen-89@users.noreply.github.com> Date: Wed, 21 May 2025 10:08:44 +0100 Subject: [PATCH 2/4] removed unnecessary return early --- lib/ContentPluginModule.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/ContentPluginModule.js b/lib/ContentPluginModule.js index 23a671e..61c5812 100644 --- a/lib/ContentPluginModule.js +++ b/lib/ContentPluginModule.js @@ -389,14 +389,10 @@ class ContentPluginModule extends AbstractApiModule { */ async capturePluginCourses (_id, name) { const content = await this.app.waitForModule('content') - const plugin = await this.find({ name: name }) - if (!plugin.length) return 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 From 51fffb365725a630f6c14c9afcfa8d35c0337289 Mon Sep 17 00:00:00 2001 From: joe-allen-89 <85872286+joe-allen-89@users.noreply.github.com> Date: Wed, 21 May 2025 10:20:22 +0100 Subject: [PATCH 3/4] updated function name --- lib/ContentPluginModule.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/ContentPluginModule.js b/lib/ContentPluginModule.js index 61c5812..8c5eb07 100644 --- a/lib/ContentPluginModule.js +++ b/lib/ContentPluginModule.js @@ -351,7 +351,7 @@ class ContentPluginModule extends AbstractApiModule { /** * Loads JSON data into relevant directories */ - async loadJson () { + async writeCourseData () { return Promise.all(Object.keys(this.courseData).map(async (type) => { const { dir, fileName, data } = this.courseData[type] if (data) { @@ -418,8 +418,7 @@ class ContentPluginModule extends AbstractApiModule { this.courseData[type].data = item }) - await this.loadJson() - + await this.writeCourseData() const folderName = `${c._id}-migrations` const captureDir = path.join('./', folderName) const opts = { outputDir: outputDir, captureDir: captureDir, file: name } From baa6a93b28b20de52c4767cbd37c93cf71923e01 Mon Sep 17 00:00:00 2001 From: joe-allen-89 <85872286+joe-allen-89@users.noreply.github.com> Date: Wed, 21 May 2025 11:31:03 +0100 Subject: [PATCH 4/4] update runGruntTask to run if args are missing, allows file to be specified or not --- lib/ContentPluginModule.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ContentPluginModule.js b/lib/ContentPluginModule.js index 8c5eb07..f92ac56 100644 --- a/lib/ContentPluginModule.js +++ b/lib/ContentPluginModule.js @@ -368,7 +368,13 @@ class ContentPluginModule extends AbstractApiModule { * @return {Promise} */ runGruntTask (subTask, { outputDir, captureDir, file }) { - return this.execPromise(`npx grunt migration:${subTask} --outputdir=${outputDir} --capturedir=${captureDir} --file=${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) {