From 2675b4042147728f39043542ece6bd498c5e0330 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Tue, 17 Mar 2026 17:53:13 +0000 Subject: [PATCH 1/2] Update: Trigger content migrations on plugin update Call framework.migrateCourses() after updating a plugin to migrate content in courses that use the updated plugin. --- lib/ContentPluginModule.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/ContentPluginModule.js b/lib/ContentPluginModule.js index 481d5a4..4d553ce 100644 --- a/lib/ContentPluginModule.js +++ b/lib/ContentPluginModule.js @@ -389,9 +389,20 @@ class ContentPluginModule extends AbstractApiModule { */ async updatePlugin (_id) { const [{ name }] = await this.find({ _id }) + const { readFrameworkPluginVersions } = await import('adapt-authoring-adaptframework') + const fromPlugins = await readFrameworkPluginVersions(this.framework.path) const [pluginData] = await this.framework.runCliCommand('updatePlugins', { plugins: [name] }) const p = await this.update({ name }, pluginData._sourceInfo) await this.processPluginSchemas(pluginData) + const toPlugins = await readFrameworkPluginVersions(this.framework.path) + const courses = await this.getPluginUses(_id) + if (courses.length) { + await this.framework.migrateCourses({ + fromPlugins, + toPlugins, + courseIds: courses.map(c => c._id) + }) + } this.log('info', `successfully updated plugin ${p.name}@${p.version}`) return p } From 43305bb3f90bd235b1f27392ef66dca3c6f1555d Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Tue, 5 May 2026 13:11:32 +0100 Subject: [PATCH 2/2] Fix: re-register plugin schemas after JsonSchema registry reset --- lib/ContentPluginModule.js | 50 +++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/ContentPluginModule.js b/lib/ContentPluginModule.js index 4d553ce..87ce3f1 100644 --- a/lib/ContentPluginModule.js +++ b/lib/ContentPluginModule.js @@ -23,8 +23,10 @@ class ContentPluginModule extends AbstractApiModule { /** @ignore */ this.root = 'contentplugins' /** @ignore */ this.schemaName = 'contentplugin' /** - * Reference to all content plugin schemas, grouped by plugin - * @type {Object} + * Maps plugin name to a map of schema $anchor → file path. The file path + * lets us re-register schemas after JsonSchemaModule resets the registry + * on app ready (it only re-registers schemas owned by app.dependencies). + * @type {Object>} */ this.pluginSchemas = {} /** @@ -44,7 +46,7 @@ class ContentPluginModule extends AbstractApiModule { if (!process.env.ADAPT_ALLOW_PRERELEASE) { process.env.ADAPT_ALLOW_PRERELEASE = 'true' } - const [framework, mongodb] = await this.app.waitForModule('adaptframework', 'mongodb') + const [framework, jsonschema, mongodb] = await this.app.waitForModule('adaptframework', 'jsonschema', 'mongodb') await mongodb.setIndex(this.collectionName, 'name', { unique: true }) await mongodb.setIndex(this.collectionName, 'displayName', { unique: true }) @@ -55,6 +57,10 @@ class ContentPluginModule extends AbstractApiModule { */ this.framework = framework + // JsonSchemaModule resets the registry at app ready and only re-registers + // schemas from app.dependencies — plugin schemas would otherwise be lost. + jsonschema.registerSchemasHook.tap(() => this.reregisterPluginSchemas()) + try { await this.initPlugins() } catch (e) { @@ -110,8 +116,8 @@ class ContentPluginModule extends AbstractApiModule { const pluginData = await this.findOne({ _id }) // unregister any schemas const jsonschema = await this.app.waitForModule('jsonschema') - const schemas = this.pluginSchemas[pluginData.name] ?? [] - schemas.forEach(s => jsonschema.deregisterSchema(s)) + const schemas = this.pluginSchemas[pluginData.name] ?? {} + Object.keys(schemas).forEach(s => jsonschema.deregisterSchema(s)) delete this.pluginSchemas[pluginData.name] await this.framework.runCliCommand('uninstallPlugins', { plugins: [pluginData.name] }) @@ -209,9 +215,9 @@ class ContentPluginModule extends AbstractApiModule { const jsonschema = await this.app.waitForModule('jsonschema') return Promise.all(pluginInfo.map(async plugin => { const name = plugin.name - const oldSchemaPaths = this.pluginSchemas[name] - if (oldSchemaPaths) { - Object.values(oldSchemaPaths).forEach(s => jsonschema.deregisterSchema(s)) + const existing = this.pluginSchemas[name] + if (existing) { + Object.keys(existing).forEach(s => jsonschema.deregisterSchema(s)) delete this.pluginSchemas[name] } const schemaPaths = await plugin.getSchemaPaths() @@ -219,15 +225,31 @@ class ContentPluginModule extends AbstractApiModule { const schema = await this.readJson(schemaPath) const source = schema?.$patch?.source?.$ref if (source) { - if (!this.pluginSchemas[name]) this.pluginSchemas[name] = [] - if (this.pluginSchemas[name].includes(schema.$anchor)) jsonschema.deregisterSchema(this.pluginSchemas[name][source]) - this.pluginSchemas[name].push(schema.$anchor) + if (!this.pluginSchemas[name]) this.pluginSchemas[name] = {} + this.pluginSchemas[name][schema.$anchor] = schemaPath } return jsonschema.registerSchema(schemaPath, { replace: true }) })) })) } + /** + * Re-registers tracked plugin schemas (called via JsonSchemaModule.registerSchemasHook) + * @return {Promise} + */ + async reregisterPluginSchemas () { + const jsonschema = await this.app.waitForModule('jsonschema') + for (const schemas of Object.values(this.pluginSchemas)) { + for (const schemaPath of Object.values(schemas)) { + try { + jsonschema.registerSchema(schemaPath, { replace: true }) + } catch (e) { + this.log('warn', `failed to re-register plugin schema ${schemaPath}`, e) + } + } + } + } + /** * Returns whether a schema is registered by a plugin * @param {String} schemaName Name of the schema to check @@ -235,17 +257,17 @@ class ContentPluginModule extends AbstractApiModule { */ isPluginSchema (schemaName) { for (const p in this.pluginSchemas) { - if (this.pluginSchemas[p].includes(schemaName)) return true + if (schemaName in this.pluginSchemas[p]) return true } } /** * Returns all schemas registered by a plugin * @param {String} pluginName Plugin name - * @return {Array} List of the plugin's registered schemas + * @return {Array} List of the plugin's registered schema $anchors */ getPluginSchemas (pluginName) { - return this.pluginSchemas[pluginName] ?? [] + return Object.keys(this.pluginSchemas[pluginName] ?? {}) } /**