Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions lib/ContentPluginModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Object<string, string>>}
*/
this.pluginSchemas = {}
/**
Expand All @@ -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 })
Expand All @@ -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) {
Expand Down Expand Up @@ -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] })
Expand Down Expand Up @@ -209,43 +215,59 @@ 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()
return Promise.all(schemaPaths.map(async schemaPath => {
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
* @return {Boolean}
*/
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] ?? {})
}

/**
Expand Down Expand Up @@ -389,9 +411,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
}
Expand Down
Loading