adapt-authoring-adaptframework 1.12.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/releases.yml +1 -1
- package/.github/workflows/standardjs.yml +1 -2
- package/.github/workflows/tests.yml +1 -2
- package/index.js +0 -1
- package/lib/AdaptFrameworkBuild.js +17 -28
- package/lib/AdaptFrameworkImport.js +47 -47
- package/lib/AdaptFrameworkModule.js +12 -11
- package/lib/handlers.js +164 -0
- package/lib/utils/copyFrameworkSource.js +35 -0
- package/lib/utils/getImportContentCounts.js +13 -0
- package/lib/utils/getImportSummary.js +55 -0
- package/lib/utils/getPluginUpdateStatus.js +20 -0
- package/lib/utils/inferBuildAction.js +9 -0
- package/lib/utils/log.js +37 -0
- package/lib/utils/retrieveBuildData.js +18 -0
- package/lib/utils/runCliCommand.js +24 -0
- package/lib/utils/slugifyTitle.js +18 -0
- package/lib/utils.js +9 -0
- package/package.json +5 -5
- package/tests/AdaptFrameworkBuild.spec.js +5 -9
- package/tests/utils-getImportContentCounts.spec.js +30 -0
- package/tests/utils-getPluginUpdateStatus.spec.js +34 -0
- package/tests/utils-inferBuildAction.spec.js +21 -0
- package/lib/AdaptFrameworkUtils.js +0 -399
- package/tests/AdaptFrameworkUtils.spec.js +0 -160
package/index.js
CHANGED
|
@@ -5,4 +5,3 @@
|
|
|
5
5
|
export { default } from './lib/AdaptFrameworkModule.js'
|
|
6
6
|
export { default as AdaptFrameworkBuild } from './lib/AdaptFrameworkBuild.js'
|
|
7
7
|
export { default as AdaptFrameworkImport } from './lib/AdaptFrameworkImport.js'
|
|
8
|
-
export { default as AdaptFrameworkUtils } from './lib/AdaptFrameworkUtils.js'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
-
import { App, Hook } from 'adapt-authoring-core'
|
|
2
|
+
import { App, Hook, ensureDir, writeJson } from 'adapt-authoring-core'
|
|
3
3
|
import { createWriteStream } from 'fs'
|
|
4
4
|
import AdaptCli from 'adapt-cli'
|
|
5
|
-
import
|
|
5
|
+
import { log, logDir, logMemory, copyFrameworkSource } from './utils.js'
|
|
6
6
|
import fs from 'fs/promises'
|
|
7
7
|
import path from 'upath'
|
|
8
8
|
import semver from 'semver'
|
|
@@ -152,17 +152,6 @@ class AdaptFrameworkBuild {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
* Makes sure the directory exists
|
|
156
|
-
* @param {string} dir
|
|
157
|
-
*/
|
|
158
|
-
async ensureDir (dir) {
|
|
159
|
-
try {
|
|
160
|
-
await fs.mkdir(dir, { recursive: true })
|
|
161
|
-
} catch (e) {
|
|
162
|
-
if (e.code !== 'EEXIST') throw e
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
155
|
/**
|
|
167
156
|
* Runs the Adapt framework build tools to generate a course build
|
|
168
157
|
* @return {Promise} Resolves with the output directory
|
|
@@ -182,19 +171,19 @@ class AdaptFrameworkBuild {
|
|
|
182
171
|
|
|
183
172
|
const cacheDir = path.join(framework.getConfig('buildDir'), 'cache')
|
|
184
173
|
|
|
185
|
-
await
|
|
186
|
-
await
|
|
187
|
-
await
|
|
174
|
+
await ensureDir(this.dir)
|
|
175
|
+
await ensureDir(this.buildDir)
|
|
176
|
+
await ensureDir(cacheDir)
|
|
188
177
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
178
|
+
logDir('dir', this.dir)
|
|
179
|
+
logDir('buildDir', this.buildDir)
|
|
180
|
+
logDir('cacheDir', this.cacheDir)
|
|
192
181
|
|
|
193
182
|
await this.loadCourseData()
|
|
194
183
|
|
|
195
184
|
await Promise.all([
|
|
196
185
|
this.copyAssets(),
|
|
197
|
-
|
|
186
|
+
copyFrameworkSource({
|
|
198
187
|
destDir: this.dir,
|
|
199
188
|
enabledPlugins: this.enabledPlugins.map(p => p.name),
|
|
200
189
|
linkNodeModules: !this.isExport
|
|
@@ -204,11 +193,11 @@ class AdaptFrameworkBuild {
|
|
|
204
193
|
|
|
205
194
|
await this.writeContentJson()
|
|
206
195
|
|
|
207
|
-
|
|
196
|
+
logDir('courseDir', this.courseDir)
|
|
208
197
|
|
|
209
198
|
if (!this.isExport) {
|
|
210
199
|
try {
|
|
211
|
-
|
|
200
|
+
logMemory()
|
|
212
201
|
await AdaptCli.buildCourse({
|
|
213
202
|
cwd: this.dir,
|
|
214
203
|
sourceMaps: !this.isPublish,
|
|
@@ -216,9 +205,9 @@ class AdaptFrameworkBuild {
|
|
|
216
205
|
cachePath: path.resolve(cacheDir, this.courseId),
|
|
217
206
|
logger: { log: (...args) => App.instance.logger.log('debug', 'adapt-cli', ...args) }
|
|
218
207
|
})
|
|
219
|
-
|
|
208
|
+
logMemory()
|
|
220
209
|
} catch (e) {
|
|
221
|
-
|
|
210
|
+
logMemory()
|
|
222
211
|
throw App.instance.errors.FW_CLI_BUILD_FAILED
|
|
223
212
|
.setData(e)
|
|
224
213
|
}
|
|
@@ -405,7 +394,7 @@ class AdaptFrameworkBuild {
|
|
|
405
394
|
if (a.url) {
|
|
406
395
|
return
|
|
407
396
|
}
|
|
408
|
-
await
|
|
397
|
+
await ensureDir(path.dirname(this.assetData.idMap[a._id]))
|
|
409
398
|
const inputStream = await assets.createFsWrapper(a).read(a)
|
|
410
399
|
const outputStream = createWriteStream(this.assetData.idMap[a._id])
|
|
411
400
|
inputStream.pipe(outputStream)
|
|
@@ -434,10 +423,10 @@ class AdaptFrameworkBuild {
|
|
|
434
423
|
data.push(this.assetData)
|
|
435
424
|
}
|
|
436
425
|
return Promise.all(data.map(async ({ dir, fileName, data }) => {
|
|
437
|
-
await
|
|
426
|
+
await ensureDir(dir)
|
|
438
427
|
const filepath = path.join(dir, fileName)
|
|
439
|
-
const returnData = await
|
|
440
|
-
|
|
428
|
+
const returnData = await writeJson(filepath, data)
|
|
429
|
+
log('verbose', 'WRITE', filepath)
|
|
441
430
|
return returnData
|
|
442
431
|
}))
|
|
443
432
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { App, Hook,
|
|
1
|
+
import { App, Hook, spawn, readJson, writeJson } from 'adapt-authoring-core'
|
|
2
2
|
import fs from 'fs/promises'
|
|
3
3
|
import { glob } from 'glob'
|
|
4
4
|
import octopus from 'adapt-octopus'
|
|
@@ -6,7 +6,7 @@ import path from 'upath'
|
|
|
6
6
|
import { randomBytes } from 'node:crypto'
|
|
7
7
|
import semver from 'semver'
|
|
8
8
|
import { unzip } from 'zipper'
|
|
9
|
-
import
|
|
9
|
+
import { log, logDir, getImportSummary, getImportContentCounts } from './utils.js'
|
|
10
10
|
|
|
11
11
|
import ComponentTransform from './migrations/component.js'
|
|
12
12
|
import ConfigTransform from './migrations/config.js'
|
|
@@ -255,8 +255,8 @@ class AdaptFrameworkImport {
|
|
|
255
255
|
*/
|
|
256
256
|
this.jsonschema = jsonschema
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
log('debug', 'IMPORT_USER', this.userId)
|
|
259
|
+
log('debug', 'IMPORT_SETTINGS', JSON.stringify(this.settings, null, 2))
|
|
260
260
|
|
|
261
261
|
const { isDryRun, importContent, importPlugins, migrateContent } = this.settings
|
|
262
262
|
const tasks = [
|
|
@@ -323,11 +323,11 @@ class AdaptFrameworkImport {
|
|
|
323
323
|
this.framework.log('error', e)
|
|
324
324
|
throw (e?.statusCode ? e : App.instance.errors.FW_IMPORT_INVALID_COURSE.setData({ reason: e.message }))
|
|
325
325
|
}
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
logDir('unzipPath', this.path)
|
|
327
|
+
logDir('coursePath', this.coursePath)
|
|
328
328
|
|
|
329
329
|
try {
|
|
330
|
-
/** @ignore */this.pkg = await
|
|
330
|
+
/** @ignore */this.pkg = await readJson(`${this.path}/package.json`)
|
|
331
331
|
} catch (e) {
|
|
332
332
|
throw App.instance.errors.FW_IMPORT_INVALID.setData({ reason: e.message })
|
|
333
333
|
}
|
|
@@ -344,7 +344,7 @@ class AdaptFrameworkImport {
|
|
|
344
344
|
this.statusReport.info.push({ code: 'MIGRATE_CONTENT', data })
|
|
345
345
|
}
|
|
346
346
|
await this.convertSchemas()
|
|
347
|
-
|
|
347
|
+
log('debug', 'preparation tasks completed successfully')
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
/**
|
|
@@ -354,7 +354,7 @@ class AdaptFrameworkImport {
|
|
|
354
354
|
async convertSchemas () {
|
|
355
355
|
return octopus.runRecursive({
|
|
356
356
|
cwd: this.path,
|
|
357
|
-
logger: { log: (...args) =>
|
|
357
|
+
logger: { log: (...args) => log('debug', ...args) }
|
|
358
358
|
})
|
|
359
359
|
}
|
|
360
360
|
|
|
@@ -369,11 +369,11 @@ class AdaptFrameworkImport {
|
|
|
369
369
|
}
|
|
370
370
|
try {
|
|
371
371
|
const customStyle = await fs.readFile(customStylePath, 'utf8')
|
|
372
|
-
const courseJson = await
|
|
373
|
-
await
|
|
374
|
-
|
|
372
|
+
const courseJson = await readJson(courseJsonPath)
|
|
373
|
+
await writeJson(courseJsonPath, { customStyle, ...courseJson })
|
|
374
|
+
log('info', 'patched course customStyle')
|
|
375
375
|
} catch (e) {
|
|
376
|
-
|
|
376
|
+
log('warn', 'failed to patch course customStyle', e)
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
|
|
@@ -383,13 +383,13 @@ class AdaptFrameworkImport {
|
|
|
383
383
|
async patchThemeName () {
|
|
384
384
|
try {
|
|
385
385
|
const configJsonPath = `${this.coursePath}/config.json`
|
|
386
|
-
const configJson = await
|
|
386
|
+
const configJson = await readJson(configJsonPath)
|
|
387
387
|
if (configJson._theme) return
|
|
388
388
|
configJson._theme = Object.values(this.usedContentPlugins).find(p => p.type === 'theme').name
|
|
389
|
-
await
|
|
390
|
-
|
|
389
|
+
await writeJson(configJsonPath, configJson)
|
|
390
|
+
log('info', 'patched config _theme')
|
|
391
391
|
} catch (e) {
|
|
392
|
-
|
|
392
|
+
log('warn', 'failed to patch config _theme', e)
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
|
@@ -401,14 +401,14 @@ class AdaptFrameworkImport {
|
|
|
401
401
|
this.assetData = []
|
|
402
402
|
const metaFiles = await glob(`${this.langPath}/assets.json`, { absolute: true })
|
|
403
403
|
if (metaFiles.length) { // process included asset metadata
|
|
404
|
-
|
|
404
|
+
log('debug', 'processing metadata files', metaFiles)
|
|
405
405
|
await Promise.all(metaFiles.map(async f => {
|
|
406
|
-
const metaJson = await
|
|
406
|
+
const metaJson = await readJson(f)
|
|
407
407
|
Object.entries(metaJson).forEach(([filename, metadata]) => this.assetData.push({ filename, ...metadata }))
|
|
408
408
|
}))
|
|
409
409
|
} else { // process the file metadata manually
|
|
410
410
|
const assetFiles = await glob(`${this.langPath}/*/*`, { absolute: true })
|
|
411
|
-
|
|
411
|
+
log('debug', 'processing asset files manually', assetFiles.length)
|
|
412
412
|
this.assetData.push(...assetFiles.map(f => Object.assign({}, { title: path.basename(f), filepath: f })))
|
|
413
413
|
}
|
|
414
414
|
const hasGlobalTags = !!this.tags.length
|
|
@@ -431,9 +431,9 @@ class AdaptFrameworkImport {
|
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
433
|
await Promise.all(usedPluginPaths.map(async p => {
|
|
434
|
-
const bowerJson = await
|
|
434
|
+
const bowerJson = await readJson(`${p}/bower.json`)
|
|
435
435
|
const { name, version, targetAttribute } = bowerJson
|
|
436
|
-
|
|
436
|
+
log('debug', 'found plugin', name)
|
|
437
437
|
this.usedContentPlugins[path.basename(p)] = { name, path: p, version, targetAttribute, type: getPluginType(bowerJson) }
|
|
438
438
|
}))
|
|
439
439
|
this.contentJson.config._enabledPlugins = Object.keys(this.usedContentPlugins)
|
|
@@ -446,8 +446,8 @@ class AdaptFrameworkImport {
|
|
|
446
446
|
async loadCourseData () {
|
|
447
447
|
const files = await glob('**/*.json', { cwd: this.langPath, absolute: true, ignore: { ignored: p => p.name === 'assets.json' } })
|
|
448
448
|
const mapped = await Promise.all(files.map(f => this.loadContentFile(f)))
|
|
449
|
-
this.statusReport.info.push({ code: 'CONTENT_IMPORTED', data:
|
|
450
|
-
|
|
449
|
+
this.statusReport.info.push({ code: 'CONTENT_IMPORTED', data: getImportContentCounts(this.contentJson) })
|
|
450
|
+
log('info', 'loaded course data successfully')
|
|
451
451
|
return mapped
|
|
452
452
|
}
|
|
453
453
|
|
|
@@ -458,7 +458,7 @@ class AdaptFrameworkImport {
|
|
|
458
458
|
async loadContentFile (filePath) {
|
|
459
459
|
let contents
|
|
460
460
|
try {
|
|
461
|
-
contents = await
|
|
461
|
+
contents = await readJson(filePath)
|
|
462
462
|
} catch (e) {
|
|
463
463
|
if (e.constructor.name === 'SyntaxError') {
|
|
464
464
|
throw App.instance.errors.FILE_SYNTAX_ERROR.setData({ path: filePath.replace(this.path, ''), message: e.message })
|
|
@@ -481,12 +481,12 @@ class AdaptFrameworkImport {
|
|
|
481
481
|
contents.forEach(c => {
|
|
482
482
|
this.contentJson.contentObjects[c._id] = c
|
|
483
483
|
if (!c._type) {
|
|
484
|
-
|
|
484
|
+
log('warn', App.instance.errors.FW_IMPORT_INVALID_CONTENT.setData({ item: c }))
|
|
485
485
|
this.statusReport.warn.push({ code: 'INVALID_CONTENT', data: c })
|
|
486
486
|
}
|
|
487
487
|
})
|
|
488
488
|
}
|
|
489
|
-
|
|
489
|
+
log('debug', 'LOAD_CONTENT', path.resolve(filePath))
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
/**
|
|
@@ -494,7 +494,7 @@ class AdaptFrameworkImport {
|
|
|
494
494
|
* @return {Promise}
|
|
495
495
|
*/
|
|
496
496
|
async runGruntMigration (subTask, { outputDir, captureDir, outputFilePath }) {
|
|
497
|
-
const output = await
|
|
497
|
+
const output = await spawn({
|
|
498
498
|
cmd: 'npx',
|
|
499
499
|
args: ['grunt', `migration:${subTask}`, `--outputdir=${outputDir}`, `--capturedir=${captureDir}`],
|
|
500
500
|
cwd: this.frameworkPath ?? this.framework.path
|
|
@@ -517,16 +517,16 @@ class AdaptFrameworkImport {
|
|
|
517
517
|
captureDir: path.join(`./${migrationId}-migrations`),
|
|
518
518
|
outputFilePath: path.join(this.framework.path, 'migrations', `${migrationId}.txt`)
|
|
519
519
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
520
|
+
log('debug', 'MIGRATION_ID', migrationId)
|
|
521
|
+
logDir('captureDir', opts.captureDir)
|
|
522
|
+
logDir('outputDir', opts.outputDir)
|
|
523
523
|
|
|
524
524
|
await this.runGruntMigration('capture', opts)
|
|
525
525
|
await this.runGruntMigration('migrate', opts)
|
|
526
526
|
|
|
527
527
|
await fs.rm(path.join(this.framework.path, opts.captureDir), { recursive: true })
|
|
528
528
|
} catch (error) {
|
|
529
|
-
|
|
529
|
+
log('error', 'Migration process failed', error)
|
|
530
530
|
throw App.instance.errors.FW_IMPORT_MIGRATION_FAILED.setData({ reason: error.message })
|
|
531
531
|
}
|
|
532
532
|
}
|
|
@@ -568,7 +568,7 @@ class AdaptFrameworkImport {
|
|
|
568
568
|
if (course.tags) {
|
|
569
569
|
course.tags = course.tags.map(t => existingTagMap[t])
|
|
570
570
|
}
|
|
571
|
-
|
|
571
|
+
log('debug', 'imported tags successfully')
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
/**
|
|
@@ -602,7 +602,7 @@ class AdaptFrameworkImport {
|
|
|
602
602
|
}
|
|
603
603
|
imagesImported++
|
|
604
604
|
}))
|
|
605
|
-
|
|
605
|
+
log('debug', 'imported course assets successfully')
|
|
606
606
|
this.statusReport.info.push({ code: 'ASSETS_IMPORTED_SUCCESSFULLY', data: { count: imagesImported } })
|
|
607
607
|
}
|
|
608
608
|
|
|
@@ -632,7 +632,7 @@ class AdaptFrameworkImport {
|
|
|
632
632
|
const { version: installedVersion, isLocalInstall } = installedP
|
|
633
633
|
if (semver.lt(importVersion, installedVersion)) {
|
|
634
634
|
this.statusReport.info.push({ code: 'PLUGIN_INSTALL_MIGRATING', data: { name: p, installedVersion, importVersion } })
|
|
635
|
-
|
|
635
|
+
log('debug', `migrating '${p}@${importVersion}' during import, installed version is newer (${installedVersion})`)
|
|
636
636
|
this.pluginsToMigrate.push(p)
|
|
637
637
|
return
|
|
638
638
|
}
|
|
@@ -642,12 +642,12 @@ class AdaptFrameworkImport {
|
|
|
642
642
|
}
|
|
643
643
|
if (semver.eq(importVersion, installedVersion)) {
|
|
644
644
|
this.statusReport.info.push({ code: 'PLUGIN_INSTALL_NOT_NEWER', data: { name: p, installedVersion, importVersion } })
|
|
645
|
-
|
|
645
|
+
log('debug', `not updating '${p}@${importVersion}' during import, installed version is equal to (${installedVersion})`)
|
|
646
646
|
return
|
|
647
647
|
}
|
|
648
648
|
if (!isLocalInstall) {
|
|
649
649
|
this.statusReport.warn.push({ code: 'MANAGED_PLUGIN_INSTALL_SKIPPED', data: { name: p, installedVersion, importVersion } })
|
|
650
|
-
|
|
650
|
+
log('debug', `cannot update '${p}' during import, plugin managed via UI`)
|
|
651
651
|
}
|
|
652
652
|
pluginsToUpdate.push(p)
|
|
653
653
|
})
|
|
@@ -670,10 +670,10 @@ class AdaptFrameworkImport {
|
|
|
670
670
|
}
|
|
671
671
|
// try and infer a targetAttribute if there isn't one
|
|
672
672
|
const pluginBowerPath = path.join(this.usedContentPlugins[p].path, 'bower.json')
|
|
673
|
-
const bowerJson = await
|
|
673
|
+
const bowerJson = await readJson(pluginBowerPath)
|
|
674
674
|
if (!bowerJson.targetAttribute) {
|
|
675
675
|
bowerJson.targetAttribute = `_${bowerJson.component || bowerJson.extension || bowerJson.menu || bowerJson.theme}`
|
|
676
|
-
await
|
|
676
|
+
await writeJson(pluginBowerPath, bowerJson)
|
|
677
677
|
}
|
|
678
678
|
if (!this.settings.isDryRun) {
|
|
679
679
|
const [pluginData] = await this.contentplugin.installPlugins([[p, this.usedContentPlugins[p].path]], { strict: true })
|
|
@@ -684,9 +684,9 @@ class AdaptFrameworkImport {
|
|
|
684
684
|
this.statusReport.info.push({ code: 'INSTALL_PLUGIN', data: { name: p, version: bowerJson.version } })
|
|
685
685
|
} catch (e) {
|
|
686
686
|
if (e.code === 'EEXIST') {
|
|
687
|
-
|
|
687
|
+
log('warn', 'PLUGIN_ALREADY_EXISTS', p)
|
|
688
688
|
} else {
|
|
689
|
-
|
|
689
|
+
log('error', 'PLUGIN_IMPORT_FAILED', p, e)
|
|
690
690
|
errors.push({ plugin: p, error: e.data?.errors?.[0] ?? e })
|
|
691
691
|
}
|
|
692
692
|
}
|
|
@@ -699,7 +699,7 @@ class AdaptFrameworkImport {
|
|
|
699
699
|
this.componentNameMap = Object.values({ ...this.installedPlugins, ...this.newContentPlugins }).reduce((m, v) => {
|
|
700
700
|
return { ...m, [v.targetAttribute.slice(1)]: v.name }
|
|
701
701
|
}, {})
|
|
702
|
-
|
|
702
|
+
log('debug', 'imported course plugins successfully')
|
|
703
703
|
}
|
|
704
704
|
|
|
705
705
|
/**
|
|
@@ -744,7 +744,7 @@ class AdaptFrameworkImport {
|
|
|
744
744
|
}
|
|
745
745
|
}
|
|
746
746
|
if (errors.length) throw App.instance.errors.FW_IMPORT_CONTENT_FAILED.setData({ errors })
|
|
747
|
-
|
|
747
|
+
log('debug', 'imported course data successfully')
|
|
748
748
|
}
|
|
749
749
|
|
|
750
750
|
/**
|
|
@@ -790,7 +790,7 @@ class AdaptFrameworkImport {
|
|
|
790
790
|
try {
|
|
791
791
|
this.extractAssets(schema.built.properties, insertData)
|
|
792
792
|
} catch (e) {
|
|
793
|
-
|
|
793
|
+
log('error', `failed to extract asset data for attribute '${e.attribute}' of schema '${schemaName}', ${e}`)
|
|
794
794
|
}
|
|
795
795
|
insertData = await schema.sanitise(insertData)
|
|
796
796
|
let doc
|
|
@@ -847,7 +847,7 @@ class AdaptFrameworkImport {
|
|
|
847
847
|
* @return {Promise}
|
|
848
848
|
*/
|
|
849
849
|
async generateSummary () {
|
|
850
|
-
this.summary = await
|
|
850
|
+
this.summary = await getImportSummary(this)
|
|
851
851
|
}
|
|
852
852
|
|
|
853
853
|
/**
|
|
@@ -902,10 +902,10 @@ class AdaptFrameworkImport {
|
|
|
902
902
|
|
|
903
903
|
const restoreTasks = []
|
|
904
904
|
for (const [pluginName, originalMetadata] of Object.entries(this.updatedContentPlugins)) {
|
|
905
|
-
|
|
905
|
+
log('info', `restoring plugin '${pluginName}' to previous version ${originalMetadata.version}`)
|
|
906
906
|
restoreTasks.push(
|
|
907
907
|
this.contentplugin.restorePluginFromBackup(pluginName)
|
|
908
|
-
.catch(e =>
|
|
908
|
+
.catch(e => log('error', `failed to restore plugin '${pluginName}' from backup, ${e.message}`))
|
|
909
909
|
)
|
|
910
910
|
}
|
|
911
911
|
return Promise.allSettled(restoreTasks)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { AbstractModule, Hook } from 'adapt-authoring-core'
|
|
1
|
+
import { AbstractModule, Hook, readJson } from 'adapt-authoring-core'
|
|
2
2
|
import AdaptFrameworkBuild from './AdaptFrameworkBuild.js'
|
|
3
3
|
import AdaptFrameworkImport from './AdaptFrameworkImport.js'
|
|
4
4
|
import ApiDefs from './apidefs.js'
|
|
5
5
|
import fs from 'fs/promises'
|
|
6
|
-
import
|
|
6
|
+
import { getHandler, postHandler, importHandler, postUpdateHandler, getUpdateHandler } from './handlers.js'
|
|
7
|
+
import { runCliCommand } from './utils.js'
|
|
7
8
|
import path from 'path'
|
|
8
9
|
import semver from 'semver'
|
|
9
10
|
|
|
@@ -73,10 +74,10 @@ class AdaptFrameworkModule extends AbstractModule {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
|
-
* Reference to
|
|
77
|
+
* Reference to runCliCommand utility
|
|
77
78
|
*/
|
|
78
79
|
get runCliCommand () {
|
|
79
|
-
return
|
|
80
|
+
return runCliCommand
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
@@ -132,7 +133,7 @@ class AdaptFrameworkModule extends AbstractModule {
|
|
|
132
133
|
* @return {Promise}
|
|
133
134
|
*/
|
|
134
135
|
async getManifestPlugins () {
|
|
135
|
-
const manifest = await
|
|
136
|
+
const manifest = await readJson(path.resolve(this.path, 'adapt.json'))
|
|
136
137
|
return Object.entries(manifest.dependencies)
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -220,7 +221,7 @@ class AdaptFrameworkModule extends AbstractModule {
|
|
|
220
221
|
route: '/preview/:id/{*splat}',
|
|
221
222
|
handlers: {
|
|
222
223
|
get: (req, res, next) => { // fail silently
|
|
223
|
-
|
|
224
|
+
getHandler(req, res, e => e ? res.status(e.statusCode || 500).end() : next())
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
})
|
|
@@ -232,22 +233,22 @@ class AdaptFrameworkModule extends AbstractModule {
|
|
|
232
233
|
this.apiRouter.addRoute(
|
|
233
234
|
{
|
|
234
235
|
route: '/preview/:id',
|
|
235
|
-
handlers: { post:
|
|
236
|
+
handlers: { post: postHandler },
|
|
236
237
|
meta: ApiDefs.preview
|
|
237
238
|
},
|
|
238
239
|
{
|
|
239
240
|
route: '/publish/:id',
|
|
240
|
-
handlers: { post:
|
|
241
|
+
handlers: { post: postHandler, get: getHandler },
|
|
241
242
|
meta: ApiDefs.publish
|
|
242
243
|
},
|
|
243
244
|
{
|
|
244
245
|
route: '/import',
|
|
245
|
-
handlers: { post: [
|
|
246
|
+
handlers: { post: [importHandler] },
|
|
246
247
|
meta: ApiDefs.import
|
|
247
248
|
},
|
|
248
249
|
{
|
|
249
250
|
route: '/export/:id',
|
|
250
|
-
handlers: { post:
|
|
251
|
+
handlers: { post: postHandler, get: getHandler },
|
|
251
252
|
meta: ApiDefs.export
|
|
252
253
|
}
|
|
253
254
|
)
|
|
@@ -262,7 +263,7 @@ class AdaptFrameworkModule extends AbstractModule {
|
|
|
262
263
|
if (this.getConfig('enableUpdateApi')) {
|
|
263
264
|
this.apiRouter.addRoute({
|
|
264
265
|
route: '/update',
|
|
265
|
-
handlers: { post:
|
|
266
|
+
handlers: { post: postUpdateHandler, get: getUpdateHandler },
|
|
266
267
|
meta: ApiDefs.update
|
|
267
268
|
})
|
|
268
269
|
auth.secureRoute(`${this.apiRouter.path}/update`, 'get', ['update:adapt'])
|
package/lib/handlers.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { App, toBoolean } from 'adapt-authoring-core'
|
|
2
|
+
import path from 'upath'
|
|
3
|
+
import semver from 'semver'
|
|
4
|
+
import { inferBuildAction, retrieveBuildData, slugifyTitle, log } from './utils.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles GET requests to the API
|
|
8
|
+
* @param {external:ExpressRequest} req
|
|
9
|
+
* @param {external:ExpressResponse} res
|
|
10
|
+
* @param {Function} next
|
|
11
|
+
* @return {Promise}
|
|
12
|
+
*/
|
|
13
|
+
export async function getHandler (req, res, next) {
|
|
14
|
+
const action = inferBuildAction(req)
|
|
15
|
+
const id = req.params.id
|
|
16
|
+
let buildData
|
|
17
|
+
try {
|
|
18
|
+
buildData = await retrieveBuildData(id)
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return next(e)
|
|
21
|
+
}
|
|
22
|
+
if (!buildData || new Date(buildData.expiresAt).getTime() < Date.now()) {
|
|
23
|
+
return next(App.instance.errors.FW_BUILD_NOT_FOUND.setData({ _id: id }))
|
|
24
|
+
}
|
|
25
|
+
if (action === 'publish' || action === 'export') {
|
|
26
|
+
res.set('content-disposition', `attachment; filename="${await slugifyTitle(buildData)}.zip"`)
|
|
27
|
+
return res.sendFile(path.resolve(buildData.location), e => e && next(e))
|
|
28
|
+
}
|
|
29
|
+
if (action === 'preview') {
|
|
30
|
+
if (!req.auth.user) {
|
|
31
|
+
return res.status(App.instance.errors.MISSING_AUTH_HEADER.statusCode).end()
|
|
32
|
+
}
|
|
33
|
+
const filePath = path.resolve(buildData.location, req.path.slice(req.path.indexOf(id) + id.length + 1) || 'index.html')
|
|
34
|
+
await res.sendFile(filePath, e => {
|
|
35
|
+
if (!e) return
|
|
36
|
+
if (e.code === 'ENOENT') e = App.instance.errors.NOT_FOUND.setData({ type: 'file', id: filePath })
|
|
37
|
+
next(e)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handles POST requests to the API
|
|
44
|
+
* @param {external:ExpressRequest} req
|
|
45
|
+
* @param {external:ExpressResponse} res
|
|
46
|
+
* @param {Function} next
|
|
47
|
+
* @return {Promise}
|
|
48
|
+
*/
|
|
49
|
+
export async function postHandler (req, res, next) {
|
|
50
|
+
const framework = await App.instance.waitForModule('adaptframework')
|
|
51
|
+
const startTime = Date.now()
|
|
52
|
+
const action = inferBuildAction(req)
|
|
53
|
+
const courseId = req.params.id
|
|
54
|
+
const userId = req.auth.user._id.toString()
|
|
55
|
+
|
|
56
|
+
log('info', `running ${action} for course '${courseId}' initiated by ${userId}`)
|
|
57
|
+
try {
|
|
58
|
+
const { isPreview, buildData } = await framework.buildCourse({ action, courseId, userId })
|
|
59
|
+
const duration = Math.round((Date.now() - startTime) / 10) / 100
|
|
60
|
+
log('info', `finished ${action} for course '${courseId}' in ${duration} seconds`)
|
|
61
|
+
const urlRoot = isPreview ? framework.rootRouter.url : framework.apiRouter.url
|
|
62
|
+
res.json({
|
|
63
|
+
[`${action}_url`]: `${urlRoot}/${action}/${buildData._id}/`,
|
|
64
|
+
versions: buildData.versions
|
|
65
|
+
})
|
|
66
|
+
} catch (e) {
|
|
67
|
+
log('error', `failed to ${action} course '${courseId}'`)
|
|
68
|
+
return next(e)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deals with an incoming course (supports both local zip and remote URL stream)
|
|
74
|
+
* @param {external:ExpressRequest} req
|
|
75
|
+
* @param {external:ExpressResponse} res
|
|
76
|
+
* @return {Promise}
|
|
77
|
+
*/
|
|
78
|
+
async function handleImportFile (req, res) {
|
|
79
|
+
const [fw, middleware] = await App.instance.waitForModule('adaptframework', 'middleware')
|
|
80
|
+
const handler = req.get('Content-Type').indexOf('multipart/form-data') === 0
|
|
81
|
+
? middleware.fileUploadParser
|
|
82
|
+
: middleware.urlUploadParser
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
handler(middleware.zipTypes, { maxFileSize: fw.getConfig('importMaxFileSize'), unzip: true })(req, res, e => e ? reject(e) : resolve())
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handles POST /import requests to the API
|
|
90
|
+
* @param {external:ExpressRequest} req
|
|
91
|
+
* @param {external:ExpressResponse} res
|
|
92
|
+
* @param {Function} next
|
|
93
|
+
* @return {Promise}
|
|
94
|
+
*/
|
|
95
|
+
export async function importHandler (req, res, next) {
|
|
96
|
+
try {
|
|
97
|
+
const framework = await App.instance.waitForModule('adaptframework')
|
|
98
|
+
let importPath = req.body.importPath
|
|
99
|
+
if (req.get('Content-Type').indexOf('multipart/form-data') === 0) {
|
|
100
|
+
await handleImportFile(req, res)
|
|
101
|
+
const [course] = req.fileUpload.files.course
|
|
102
|
+
importPath = course.filepath
|
|
103
|
+
}
|
|
104
|
+
const importer = await framework.importCourse({
|
|
105
|
+
importPath,
|
|
106
|
+
userId: req.auth.user._id.toString(),
|
|
107
|
+
isDryRun: toBoolean(req.body.dryRun),
|
|
108
|
+
assetFolders: req.body.formAssetFolders,
|
|
109
|
+
tags: req.body.tags?.length > 0 ? req.body.tags?.split(',') : [],
|
|
110
|
+
importContent: toBoolean(req.body.importContent),
|
|
111
|
+
importPlugins: toBoolean(req.body.importPlugins),
|
|
112
|
+
migrateContent: toBoolean(req.body.migrateContent),
|
|
113
|
+
updatePlugins: toBoolean(req.body.updatePlugins)
|
|
114
|
+
})
|
|
115
|
+
res.json(importer.summary)
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return next(e?.statusCode ? e : App.instance.errors.FW_IMPORT_FAILED.setData({ error: e }))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handles POST /update requests to the API
|
|
123
|
+
* @param {external:ExpressRequest} req
|
|
124
|
+
* @param {external:ExpressResponse} res
|
|
125
|
+
* @param {Function} next
|
|
126
|
+
* @return {Promise}
|
|
127
|
+
*/
|
|
128
|
+
export async function postUpdateHandler (req, res, next) {
|
|
129
|
+
try {
|
|
130
|
+
log('info', 'running framework update')
|
|
131
|
+
const framework = await App.instance.waitForModule('adaptframework')
|
|
132
|
+
const previousVersion = framework.version
|
|
133
|
+
await framework.updateFramework(req.body.version)
|
|
134
|
+
const currentVersion = framework.version !== previousVersion ? framework.version : undefined
|
|
135
|
+
res.json({
|
|
136
|
+
from: previousVersion,
|
|
137
|
+
to: currentVersion
|
|
138
|
+
})
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return next(e)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handles GET /update requests to the API
|
|
146
|
+
* @param {external:ExpressRequest} req
|
|
147
|
+
* @param {external:ExpressResponse} res
|
|
148
|
+
* @param {Function} next
|
|
149
|
+
* @return {Promise}
|
|
150
|
+
*/
|
|
151
|
+
export async function getUpdateHandler (req, res, next) {
|
|
152
|
+
try {
|
|
153
|
+
const framework = await App.instance.waitForModule('adaptframework')
|
|
154
|
+
const current = framework.version
|
|
155
|
+
const latest = await framework.getLatestVersion()
|
|
156
|
+
res.json({
|
|
157
|
+
canBeUpdated: semver.gt(latest, current),
|
|
158
|
+
currentVersion: current,
|
|
159
|
+
latestCompatibleVersion: latest
|
|
160
|
+
})
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return next(e)
|
|
163
|
+
}
|
|
164
|
+
}
|