New
Run adapt-migrations directly via its JS API (no grunt) for framework update, course import, and plugin update.
Replaces the grunt-based approach attempted in adapt-security/adapt-authoring-contentplugin#29
Changes
- Add
adapt-migrations as a direct npm dependency of adaptframework
- Create shared migration utilities that call the
adapt-migrations JS API
- After framework update, migrate all existing courses in the DB
- Refactor course import to use the same mechanism instead of spawning grunt
- After plugin update, migrate courses using that plugin
Trigger Points
| Trigger |
Scope |
Module |
Framework update (--update-framework / POST /adapt/update) |
All courses |
adaptframework |
Course import with migrateContent: true |
Single imported course |
adaptframework |
Plugin update (updatePlugin()) |
Courses using that plugin |
contentplugin |
Shared Utilities (in adaptframework/lib/utils/)
readFrameworkPluginVersions.js
async readFrameworkPluginVersions(frameworkDir) -> [{name, version}]
- Globs
src/core/bower.json + src/{components,extensions,menu,theme}/*/bower.json
- Reads each with
readJson, returns [{name, version}]
collectMigrationScripts.js
async collectMigrationScripts(frameworkDir) -> [string]
- Globs
src/core/migrations/**/*.js + src/*/*/migrations/**/*.js
- Returns absolute paths
runContentMigration.js
async runContentMigration({ content, fromPlugins, toPlugins, scripts, cachePath }) -> content[]
- Core function shared by all three trigger points
- Calls
load() with migration scripts
- Creates
Journal with { content, fromPlugins, originalFromPlugins, toPlugins }
- Calls
migrate({ journal, logger })
- Returns
journal.data.content (mutated in-place by Proxy)
Flow 1: Framework Update
lib/utils/migrateExistingCourses.js
async migrateExistingCourses({ fromPlugins, toPlugins, frameworkDir }) -> { migrated, failed, errors[] }
- Collects migration scripts via
collectMigrationScripts()
- Queries all courses via
content.find({ _type: 'course' })
- For each course (sequentially):
- Fetches content: course + config +
content.find({ _courseId })
- Snapshots originals via
JSON.parse(JSON.stringify(...))
- Calls
runContentMigration()
- Compares each item with
util.isDeepStrictEqual, updates changed items via content.update()
- Catches per-course errors, logs, continues
Changes to AdaptFrameworkModule.js — updateFramework()
async updateFramework (version) {
const fromPlugins = await readFrameworkPluginVersions(this.path) // BEFORE update
// ... existing CLI update ...
const toPlugins = await readFrameworkPluginVersions(this.path) // AFTER update
await migrateExistingCourses({ fromPlugins, toPlugins, frameworkDir: this.path })
await this.postUpdateHook.invoke() // fix: add missing await
}
Changes to handlers.js — postUpdateHandler()
- Include migration results in the JSON response
Flow 2: Course Import (replaces grunt)
Changes to AdaptFrameworkImport.js
Replace migrateCourseData() (currently spawns grunt capture+migrate):
- Keep
patchThemeName() and patchCustomStyle() (patch JSON files on disk)
- Call
loadCourseData() to read import's JSON into memory
- Flatten
this.contentJson into array format for adapt-migrations
- Build
fromPlugins from this.usedContentPlugins (import's bower.json versions)
- Build
toPlugins from readFrameworkPluginVersions(this.framework.path)
- Call
runContentMigration({ content, fromPlugins, toPlugins, scripts })
- Write migrated content back into
this.contentJson structure
Update task pipeline:
[this.loadCourseData, importContent], // always load first
[this.migrateCourseData, !isDryRun && migrateContent], // in-memory migration
[this.importCourseData, !isDryRun && importContent], // no reload needed
Remove runGruntMigration() method.
Flow 3: Plugin Update
New method on AdaptFrameworkModule — migrateCourses()
Public method that contentplugin can call via this.framework:
async migrateCourses ({ fromPlugins, toPlugins, courseIds }) -> { migrated, failed, errors[] }
- Collects migration scripts from
this.path
- For each courseId: fetch content, run migration, diff, update DB
- Reuses
runContentMigration internally
migrateExistingCourses becomes a thin wrapper that queries all courses then calls this
Changes to contentplugin/lib/ContentPluginModule.js — updatePlugin()
async updatePlugin (_id) {
const [{ name }] = await this.find({ _id })
const fromPlugins = await readFrameworkPluginVersions(this.framework.path) // BEFORE
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) // AFTER
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
}
Migration Applicability & Tracking
No per-course migration tracking is needed. Each migration script gates itself via whereFromPlugin version checks (e.g. { name: 'adapt-contrib-core', version: '<6.24.2' }). Only migrations matching the fromPlugins→toPlugins range will run; others are skipped. After each migration, updatePlugin bumps the version in fromPlugins so subsequent migrations in the same run chain correctly.
All courses are assumed to be at the same framework/plugin version because:
- New courses are created with current schemas
- Imported courses are migrated at import time
- Framework/plugin updates migrate all relevant courses
This means on each update, every relevant course is processed, but only migrations for the specific version delta actually execute. The version gating is load-bearing — migrations are not all independently idempotent, so correct fromPlugins is essential. Per-course version tracking could be added later as a performance optimisation for large-scale deployments.
Files Summary
New files (adaptframework/lib/utils/)
readFrameworkPluginVersions.js
collectMigrationScripts.js
runContentMigration.js
migrateExistingCourses.js
Modified files
| File |
Changes |
adaptframework/lib/AdaptFrameworkModule.js |
Add migrateCourses() method; update updateFramework() |
adaptframework/lib/AdaptFrameworkImport.js |
Replace grunt with in-memory migration; update task pipeline |
adaptframework/lib/handlers.js |
Include migration results in update response |
adaptframework/lib/utils.js |
Add barrel exports |
adaptframework/errors/errors.json |
Add FW_UPDATE_MIGRATION_FAILED |
adaptframework/package.json |
Add adapt-migrations dependency |
contentplugin/lib/ContentPluginModule.js |
Call migrateCourses() from updatePlugin() |
Tests
| File |
Tests |
tests/utils-readFrameworkPluginVersions.spec.js |
Parses bower.json, handles missing dirs |
tests/utils-collectMigrationScripts.spec.js |
Finds core + plugin migration scripts |
tests/utils-runContentMigration.spec.js |
Loads scripts, creates journal, runs migrate |
tests/utils-migrateExistingCourses.spec.js |
Mocks content module, verifies DB updates, error isolation |
Key Design Decisions
adapt-migrations as direct dependency of adaptframework
- Shared
runContentMigration — single core function used by all three triggers
- Public
migrateCourses() on AdaptFrameworkModule — allows contentplugin to trigger targeted migrations via this.framework
- Plugin versions from bower.json — read before/after any update to determine
fromPlugins/toPlugins
- Import: in-memory migration — load content first, migrate in memory, skip disk round-trip
- Sequential per-course — avoids memory pressure
- Error isolation — one course failing doesn't block others
- Minimal DB writes — deep-compare original vs migrated, only update changed items
- No per-course tracking — version gating in migration scripts is sufficient; tracking can be added later for performance
Relevant Files (for reference)
Verification
- Unit tests:
node --test --experimental-test-module-mocks 'tests/utils-*.spec.js'
- Lint:
npx standard
- Integration (update): trigger framework update with courses in DB
- Integration (import): import course from older framework version
- Integration (plugin): update a plugin with courses using it; verify content migrated
New
Run
adapt-migrationsdirectly via its JS API (no grunt) for framework update, course import, and plugin update.Replaces the grunt-based approach attempted in adapt-security/adapt-authoring-contentplugin#29
Changes
adapt-migrationsas a direct npm dependency ofadaptframeworkadapt-migrationsJS APITrigger Points
--update-framework/POST /adapt/update)adaptframeworkmigrateContent: trueadaptframeworkupdatePlugin())contentpluginShared Utilities (in
adaptframework/lib/utils/)readFrameworkPluginVersions.jsasync readFrameworkPluginVersions(frameworkDir) -> [{name, version}]src/core/bower.json+src/{components,extensions,menu,theme}/*/bower.jsonreadJson, returns[{name, version}]collectMigrationScripts.jsasync collectMigrationScripts(frameworkDir) -> [string]src/core/migrations/**/*.js+src/*/*/migrations/**/*.jsrunContentMigration.jsasync runContentMigration({ content, fromPlugins, toPlugins, scripts, cachePath }) -> content[]load()with migration scriptsJournalwith{ content, fromPlugins, originalFromPlugins, toPlugins }migrate({ journal, logger })journal.data.content(mutated in-place by Proxy)Flow 1: Framework Update
lib/utils/migrateExistingCourses.jsasync migrateExistingCourses({ fromPlugins, toPlugins, frameworkDir }) -> { migrated, failed, errors[] }collectMigrationScripts()content.find({ _type: 'course' })content.find({ _courseId })JSON.parse(JSON.stringify(...))runContentMigration()util.isDeepStrictEqual, updates changed items viacontent.update()Changes to
AdaptFrameworkModule.js—updateFramework()Changes to
handlers.js—postUpdateHandler()Flow 2: Course Import (replaces grunt)
Changes to
AdaptFrameworkImport.jsReplace
migrateCourseData()(currently spawns grunt capture+migrate):patchThemeName()andpatchCustomStyle()(patch JSON files on disk)loadCourseData()to read import's JSON into memorythis.contentJsoninto array format foradapt-migrationsfromPluginsfromthis.usedContentPlugins(import's bower.json versions)toPluginsfromreadFrameworkPluginVersions(this.framework.path)runContentMigration({ content, fromPlugins, toPlugins, scripts })this.contentJsonstructureUpdate task pipeline:
Remove
runGruntMigration()method.Flow 3: Plugin Update
New method on
AdaptFrameworkModule—migrateCourses()Public method that
contentplugincan call viathis.framework:this.pathrunContentMigrationinternallymigrateExistingCoursesbecomes a thin wrapper that queries all courses then calls thisChanges to
contentplugin/lib/ContentPluginModule.js—updatePlugin()Migration Applicability & Tracking
No per-course migration tracking is needed. Each migration script gates itself via
whereFromPluginversion checks (e.g.{ name: 'adapt-contrib-core', version: '<6.24.2' }). Only migrations matching thefromPlugins→toPluginsrange will run; others are skipped. After each migration,updatePluginbumps the version infromPluginsso subsequent migrations in the same run chain correctly.All courses are assumed to be at the same framework/plugin version because:
This means on each update, every relevant course is processed, but only migrations for the specific version delta actually execute. The version gating is load-bearing — migrations are not all independently idempotent, so correct
fromPluginsis essential. Per-course version tracking could be added later as a performance optimisation for large-scale deployments.Files Summary
New files (
adaptframework/lib/utils/)readFrameworkPluginVersions.jscollectMigrationScripts.jsrunContentMigration.jsmigrateExistingCourses.jsModified files
adaptframework/lib/AdaptFrameworkModule.jsmigrateCourses()method; updateupdateFramework()adaptframework/lib/AdaptFrameworkImport.jsadaptframework/lib/handlers.jsadaptframework/lib/utils.jsadaptframework/errors/errors.jsonFW_UPDATE_MIGRATION_FAILEDadaptframework/package.jsonadapt-migrationsdependencycontentplugin/lib/ContentPluginModule.jsmigrateCourses()fromupdatePlugin()Tests
tests/utils-readFrameworkPluginVersions.spec.jstests/utils-collectMigrationScripts.spec.jstests/utils-runContentMigration.spec.jstests/utils-migrateExistingCourses.spec.jsKey Design Decisions
adapt-migrationsas direct dependency ofadaptframeworkrunContentMigration— single core function used by all three triggersmigrateCourses()on AdaptFrameworkModule — allowscontentpluginto trigger targeted migrations viathis.frameworkfromPlugins/toPluginsRelevant Files (for reference)
grunt/tasks/migration.js— Current grunt task wrapper (being replaced)src/core/migrations/— Core migration scriptsload,migrate,Journal,Logger)Verification
node --test --experimental-test-module-mocks 'tests/utils-*.spec.js'npx standard