adapt-authoring-adaptframework 0.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/.eslintignore +1 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/labelled_prs.yml +16 -0
- package/.github/workflows/new.yml +29 -0
- package/adapt-authoring.json +5 -0
- package/conf/config.schema.json +44 -0
- package/errors/errors.json +100 -0
- package/index.js +8 -0
- package/lib/AdaptFrameworkBuild.js +517 -0
- package/lib/AdaptFrameworkImport.js +807 -0
- package/lib/AdaptFrameworkModule.js +319 -0
- package/lib/AdaptFrameworkUtils.js +362 -0
- package/lib/apidefs.js +152 -0
- package/lib/migrations/component.js +12 -0
- package/lib/migrations/config.js +11 -0
- package/lib/migrations/graphic-src.js +10 -0
- package/lib/migrations/nav-order.js +12 -0
- package/lib/migrations/parent-id.js +7 -0
- package/lib/migrations/remove-undef.js +13 -0
- package/lib/migrations/start-page.js +17 -0
- package/lib/migrations/theme-undef.js +9 -0
- package/package.json +31 -0
- package/schema/adaptbuild.schema.json +36 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { App, Hook } from 'adapt-authoring-core'
|
|
3
|
+
import { createWriteStream } from 'fs'
|
|
4
|
+
import AdaptCli from 'adapt-cli'
|
|
5
|
+
import AdaptFrameworkUtils from './AdaptFrameworkUtils.js'
|
|
6
|
+
import fs from 'fs-extra'
|
|
7
|
+
import path from 'upath'
|
|
8
|
+
import semver from 'semver'
|
|
9
|
+
import zipper from 'zipper'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Encapsulates all behaviour needed to build a single Adapt course instance
|
|
13
|
+
* @memberof adaptframework
|
|
14
|
+
*/
|
|
15
|
+
class AdaptFrameworkBuild {
|
|
16
|
+
/**
|
|
17
|
+
* Imports a course zip to the database
|
|
18
|
+
* @param {AdaptFrameworkBuildOptions} options
|
|
19
|
+
* @return {Promise} Resolves to this AdaptFrameworkBuild instance
|
|
20
|
+
*/
|
|
21
|
+
static async run (options) {
|
|
22
|
+
const instance = new AdaptFrameworkBuild(options)
|
|
23
|
+
await instance.build()
|
|
24
|
+
return instance
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns a timestring to be used for an adaptbuild expiry
|
|
29
|
+
* @return {String}
|
|
30
|
+
*/
|
|
31
|
+
static async getBuildExpiry () {
|
|
32
|
+
const framework = await App.instance.waitForModule('adaptframework')
|
|
33
|
+
return new Date(Date.now() + framework.getConfig('buildLifespan')).toISOString()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options to be passed to AdaptFrameworkBuild
|
|
38
|
+
* @typedef {Object} AdaptFrameworkBuildOptions
|
|
39
|
+
* @property {String} action The type of build to execute
|
|
40
|
+
* @property {String} courseId The course to build
|
|
41
|
+
* @property {String} userId The user executing the build
|
|
42
|
+
* @property {String} expiresAt When the build expires
|
|
43
|
+
*
|
|
44
|
+
* @constructor
|
|
45
|
+
* @param {AdaptFrameworkBuildOptions} options
|
|
46
|
+
*/
|
|
47
|
+
constructor ({ action, courseId, userId, expiresAt }) {
|
|
48
|
+
/**
|
|
49
|
+
* The MongoDB collection name
|
|
50
|
+
* @type {String}
|
|
51
|
+
*/
|
|
52
|
+
this.collectionName = 'adaptbuilds'
|
|
53
|
+
/**
|
|
54
|
+
* The build action being performed
|
|
55
|
+
* @type {String}
|
|
56
|
+
*/
|
|
57
|
+
this.action = action
|
|
58
|
+
/**
|
|
59
|
+
* Shorthand for checking if this build is a preview
|
|
60
|
+
* @type {Boolean}
|
|
61
|
+
*/
|
|
62
|
+
this.isPreview = action === 'preview'
|
|
63
|
+
/**
|
|
64
|
+
* Shorthand for checking if this build is a publish
|
|
65
|
+
* @type {Boolean}
|
|
66
|
+
*/
|
|
67
|
+
this.isPublish = action === 'publish'
|
|
68
|
+
/**
|
|
69
|
+
* Shorthand for checking if this build is an export
|
|
70
|
+
* @type {Boolean}
|
|
71
|
+
*/
|
|
72
|
+
this.isExport = action === 'export'
|
|
73
|
+
/**
|
|
74
|
+
* The _id of the course being build
|
|
75
|
+
* @type {String}
|
|
76
|
+
*/
|
|
77
|
+
this.courseId = courseId
|
|
78
|
+
/**
|
|
79
|
+
* All JSON data describing this course
|
|
80
|
+
* @type {Object}
|
|
81
|
+
*/
|
|
82
|
+
this.expiresAt = expiresAt
|
|
83
|
+
/**
|
|
84
|
+
* All JSON data describing this course
|
|
85
|
+
* @type {Object}
|
|
86
|
+
*/
|
|
87
|
+
this.courseData = {}
|
|
88
|
+
/**
|
|
89
|
+
* All metadata related to assets used in this course
|
|
90
|
+
* @type {Object}
|
|
91
|
+
*/
|
|
92
|
+
this.assetData = {}
|
|
93
|
+
/**
|
|
94
|
+
* Metadata describing this build attempt
|
|
95
|
+
* @type {Object}
|
|
96
|
+
*/
|
|
97
|
+
this.buildData = {}
|
|
98
|
+
/**
|
|
99
|
+
* A map of _ids for use with 'friendly' IDs
|
|
100
|
+
* @type {Object}
|
|
101
|
+
*/
|
|
102
|
+
this.idMap = {}
|
|
103
|
+
/**
|
|
104
|
+
* _id of the user initiating the course build
|
|
105
|
+
* @type {String}
|
|
106
|
+
*/
|
|
107
|
+
this.userId = userId
|
|
108
|
+
/**
|
|
109
|
+
* The build output directory
|
|
110
|
+
* @type {String}
|
|
111
|
+
*/
|
|
112
|
+
this.dir = ''
|
|
113
|
+
/**
|
|
114
|
+
* The course build directory
|
|
115
|
+
* @type {String}
|
|
116
|
+
*/
|
|
117
|
+
this.buildDir = ''
|
|
118
|
+
/**
|
|
119
|
+
* The course content directory
|
|
120
|
+
* @type {String}
|
|
121
|
+
*/
|
|
122
|
+
this.courseDir = ''
|
|
123
|
+
/**
|
|
124
|
+
* The final location of the build
|
|
125
|
+
* @type {String}
|
|
126
|
+
*/
|
|
127
|
+
this.location = ''
|
|
128
|
+
/**
|
|
129
|
+
* List of plugins used in this course
|
|
130
|
+
* @type {Array<Object>}
|
|
131
|
+
*/
|
|
132
|
+
this.enabledPlugins = []
|
|
133
|
+
/**
|
|
134
|
+
* List of plugins NOT used in this course
|
|
135
|
+
* @type {Array<Object>}
|
|
136
|
+
*/
|
|
137
|
+
this.disabledPlugins = []
|
|
138
|
+
/**
|
|
139
|
+
* Invoked prior to a course being built.
|
|
140
|
+
* @type {Hook}
|
|
141
|
+
*/
|
|
142
|
+
this.preBuildHook = new Hook({ mutable: true })
|
|
143
|
+
/**
|
|
144
|
+
* Invoked after a course has been built.
|
|
145
|
+
* @type {Hook}
|
|
146
|
+
*/
|
|
147
|
+
this.postBuildHook = new Hook({ mutable: true })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Makes sure the directory exists
|
|
152
|
+
* @param {string} dir
|
|
153
|
+
*/
|
|
154
|
+
async ensureDir (dir) {
|
|
155
|
+
try {
|
|
156
|
+
await fs.mkdir(dir, { recursive: true })
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (e.code !== 'EEXIST') throw e
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Runs the Adapt framework build tools to generate a course build
|
|
164
|
+
* @return {Promise} Resolves with the output directory
|
|
165
|
+
*/
|
|
166
|
+
async build () {
|
|
167
|
+
await this.removeOldBuilds()
|
|
168
|
+
|
|
169
|
+
const framework = await App.instance.waitForModule('adaptframework')
|
|
170
|
+
if (!this.expiresAt) {
|
|
171
|
+
this.expiresAt = await AdaptFrameworkBuild.getBuildExpiry()
|
|
172
|
+
}
|
|
173
|
+
this.dir = path.resolve(framework.getConfig('buildDir'), new Date().getTime().toString())
|
|
174
|
+
this.buildDir = path.join(this.dir, 'build')
|
|
175
|
+
this.courseDir = path.join(this.buildDir, 'course')
|
|
176
|
+
|
|
177
|
+
const cacheDir = path.join(framework.getConfig('buildDir'), 'cache')
|
|
178
|
+
|
|
179
|
+
await this.ensureDir(this.dir)
|
|
180
|
+
await this.ensureDir(this.buildDir)
|
|
181
|
+
await this.ensureDir(cacheDir)
|
|
182
|
+
|
|
183
|
+
AdaptFrameworkUtils.logDir('dir', this.dir)
|
|
184
|
+
AdaptFrameworkUtils.logDir('buildDir', this.buildDir)
|
|
185
|
+
AdaptFrameworkUtils.logDir('cacheDir', this.cacheDir)
|
|
186
|
+
|
|
187
|
+
await this.loadCourseData()
|
|
188
|
+
|
|
189
|
+
await Promise.all([
|
|
190
|
+
this.copyAssets(),
|
|
191
|
+
this.copySource()
|
|
192
|
+
])
|
|
193
|
+
await this.preBuildHook.invoke(this)
|
|
194
|
+
await framework.preBuildHook.invoke(this)
|
|
195
|
+
|
|
196
|
+
await this.writeContentJson()
|
|
197
|
+
|
|
198
|
+
AdaptFrameworkUtils.logDir('courseDir', this.courseDir)
|
|
199
|
+
|
|
200
|
+
if (!this.isExport) {
|
|
201
|
+
try {
|
|
202
|
+
AdaptFrameworkUtils.logMemory()
|
|
203
|
+
await AdaptCli.buildCourse({
|
|
204
|
+
cwd: this.dir,
|
|
205
|
+
sourceMaps: !this.isPublish,
|
|
206
|
+
outputDir: this.buildDir,
|
|
207
|
+
cachePath: path.resolve(cacheDir, this.courseId),
|
|
208
|
+
logger: { log: (...args) => App.instance.logger.log('debug', 'adapt-cli', ...args) }
|
|
209
|
+
})
|
|
210
|
+
AdaptFrameworkUtils.logMemory()
|
|
211
|
+
} catch (e) {
|
|
212
|
+
AdaptFrameworkUtils.logMemory()
|
|
213
|
+
throw App.instance.errors.FW_CLI_BUILD_FAILED
|
|
214
|
+
.setData(e)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.isPreview ? await this.createPreview() : await this.createZip()
|
|
218
|
+
|
|
219
|
+
await this.postBuildHook.invoke(this)
|
|
220
|
+
await framework.postBuildHook.invoke(this)
|
|
221
|
+
|
|
222
|
+
this.buildData = await this.recordBuildAttempt()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Collects and caches all the DB data for the course being built
|
|
227
|
+
* @return {Promise}
|
|
228
|
+
*/
|
|
229
|
+
async loadCourseData () {
|
|
230
|
+
const content = await App.instance.waitForModule('content')
|
|
231
|
+
const [course] = await content.find({ _id: this.courseId, _type: 'course' })
|
|
232
|
+
if (!course) {
|
|
233
|
+
throw App.instance.errors.NOT_FOUND.setData({ type: 'course', id: this.courseId })
|
|
234
|
+
}
|
|
235
|
+
const langDir = path.join(this.courseDir, 'en')
|
|
236
|
+
this.courseData = {
|
|
237
|
+
course: { dir: langDir, fileName: 'course.json', data: undefined },
|
|
238
|
+
config: { dir: this.courseDir, fileName: 'config.json', data: undefined },
|
|
239
|
+
contentObject: { dir: langDir, fileName: 'contentObjects.json', data: [] },
|
|
240
|
+
article: { dir: langDir, fileName: 'articles.json', data: [] },
|
|
241
|
+
block: { dir: langDir, fileName: 'blocks.json', data: [] },
|
|
242
|
+
component: { dir: langDir, fileName: 'components.json', data: [] }
|
|
243
|
+
}
|
|
244
|
+
await this.loadAssetData()
|
|
245
|
+
const contentItems = [course, ...await content.find({ _courseId: course._id })]
|
|
246
|
+
this.createIdMap(contentItems)
|
|
247
|
+
this.sortContentItems(contentItems)
|
|
248
|
+
await this.cachePluginData()
|
|
249
|
+
await this.transformContentItems(contentItems)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Processes and caches the course's assets
|
|
254
|
+
* @return {Promise}
|
|
255
|
+
*/
|
|
256
|
+
async loadAssetData () {
|
|
257
|
+
const [assets, courseassets, mongodb, tags] = await App.instance.waitForModule('assets', 'courseassets', 'mongodb', 'tags')
|
|
258
|
+
|
|
259
|
+
const caRecs = await courseassets.find({ courseId: this.courseId })
|
|
260
|
+
const uniqueAssetIds = new Set(caRecs.map(c => mongodb.ObjectId.parse(c.assetId)))
|
|
261
|
+
const usedAssets = await assets.find({ _id: { $in: [...uniqueAssetIds] } })
|
|
262
|
+
|
|
263
|
+
const usedTagIds = new Set(usedAssets.reduce((m, a) => [...m, ...(a.tags ?? [])], []))
|
|
264
|
+
const usedTags = await tags.find({ _id: { $in: [...usedTagIds] } })
|
|
265
|
+
const tagTitleLookup = t => usedTags.find(u => u._id.toString() === t.toString()).title
|
|
266
|
+
|
|
267
|
+
const idMap = {}
|
|
268
|
+
const assetDocs = []
|
|
269
|
+
const courseDir = this.courseData.course.dir
|
|
270
|
+
|
|
271
|
+
await Promise.all(usedAssets.map(async a => {
|
|
272
|
+
assetDocs.push({ ...a, tags: a?.tags?.map(tagTitleLookup) })
|
|
273
|
+
if (!idMap[a._id]) idMap[a._id] = a.url ? a.url : path.join(courseDir, 'assets', a.path)
|
|
274
|
+
}))
|
|
275
|
+
this.assetData = { dir: courseDir, fileName: 'assets.json', idMap, data: assetDocs }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Caches lists of which plugins are/aren't being used in this course
|
|
280
|
+
* @return {Promise}
|
|
281
|
+
*/
|
|
282
|
+
async cachePluginData () {
|
|
283
|
+
const all = (await (await App.instance.waitForModule('contentplugin')).find({}))
|
|
284
|
+
.reduce((m, p) => Object.assign(m, { [p.name]: p }), {})
|
|
285
|
+
|
|
286
|
+
const _cachePluginDeps = (p, memo = {}) => {
|
|
287
|
+
Object.entries(p?.pluginDependencies ?? {}).forEach(([name, version]) => {
|
|
288
|
+
const p = memo[name] ?? all[name]
|
|
289
|
+
const e = !p
|
|
290
|
+
? App.instance.errors.FW_MISSING_PLUGIN_DEP.setData({ name })
|
|
291
|
+
: !semver.satisfies(p.version, version) ? App.instance.errors.FW_INCOMPAT_PLUGIN_DEP.setData({ name, version }) : undefined
|
|
292
|
+
if (e) {
|
|
293
|
+
throw e.setData({ name, version })
|
|
294
|
+
}
|
|
295
|
+
if (!memo[name]) {
|
|
296
|
+
_cachePluginDeps(p, memo)
|
|
297
|
+
memo[name] = p
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
return memo
|
|
301
|
+
}
|
|
302
|
+
const enabled = (this.courseData.config.data._enabledPlugins || [])
|
|
303
|
+
.reduce((plugins, name) => {
|
|
304
|
+
const p = all[name]
|
|
305
|
+
return Object.assign(plugins, { [name]: p, ..._cachePluginDeps(p) })
|
|
306
|
+
}, {})
|
|
307
|
+
|
|
308
|
+
Object.entries(all).forEach(([name, p]) => (enabled[name] ? this.enabledPlugins : this.disabledPlugins).push(p))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Stores a map of friendlyId values to ObjectId _ids
|
|
313
|
+
*/
|
|
314
|
+
createIdMap (items) {
|
|
315
|
+
items.forEach(i => {
|
|
316
|
+
this.idMap[i._id] = i._friendlyId
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Sorts the course data into the types needed for each Adapt JSON file. Works by memoising items into an object using the relative sort order as a key used for sorting.
|
|
322
|
+
* @param {Array<Object>} items The list of content objects
|
|
323
|
+
*/
|
|
324
|
+
sortContentItems (items) {
|
|
325
|
+
const getSortOrderStr = co => (co._type === 'course' ? '1' : co._sortOrder.toString()).padStart(4, '0') // note we pad to allow 9999 children
|
|
326
|
+
const coMap = items.reduce((m, item) => Object.assign(m, { [item._id]: item }), {}) // object mapping items to their _id for easy lookup
|
|
327
|
+
const sorted = {}
|
|
328
|
+
items.forEach(i => {
|
|
329
|
+
const type = i._type === 'page' || i._type === 'menu' ? 'contentObject' : i._type
|
|
330
|
+
if (type === 'course' || type === 'config') {
|
|
331
|
+
this.courseData[type].data = i
|
|
332
|
+
return // don't sort the course or config items
|
|
333
|
+
}
|
|
334
|
+
if (!sorted[type]) sorted[type] = {}
|
|
335
|
+
// recursively calculate a sort order which is relative to the entire course for comparison
|
|
336
|
+
let sortOrder = ''
|
|
337
|
+
for (let item = i; item; sortOrder = getSortOrderStr(item) + sortOrder, item = coMap[item._parentId]);
|
|
338
|
+
sorted[type][sortOrder.padEnd(64, '0')] = i // pad the final string for comparison purposes
|
|
339
|
+
}) // finally populate this.courseData with the sorted items
|
|
340
|
+
Object.entries(sorted).forEach(([type, data]) => {
|
|
341
|
+
this.courseData[type].data = Object.keys(data).sort().map(key => data[key])
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Transforms content items into a format recognised by the Adapt framework
|
|
347
|
+
*/
|
|
348
|
+
async transformContentItems (items) {
|
|
349
|
+
items.forEach(i => {
|
|
350
|
+
// slot any _friendlyIds into the _id field
|
|
351
|
+
['_courseId', '_parentId'].forEach(k => {
|
|
352
|
+
i[k] = this.idMap[i[k]] || i[k]
|
|
353
|
+
})
|
|
354
|
+
if (i._friendlyId) {
|
|
355
|
+
i._id = i._friendlyId
|
|
356
|
+
}
|
|
357
|
+
// replace asset _ids with correct paths
|
|
358
|
+
const idMapEntries = Object.entries(this.assetData.idMap)
|
|
359
|
+
const itemString = idMapEntries.reduce((s, [_id, assetPath]) => {
|
|
360
|
+
const relPath = assetPath.replace(this.courseDir, 'course')
|
|
361
|
+
return s.replace(new RegExp(_id, 'g'), relPath)
|
|
362
|
+
}, JSON.stringify(i))
|
|
363
|
+
Object.assign(i, JSON.parse(itemString))
|
|
364
|
+
// insert expected _component values
|
|
365
|
+
if (i._component) {
|
|
366
|
+
i._component = this.enabledPlugins.find(p => p.name === i._component).targetAttribute.slice(1)
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
// move globals to a nested _extensions object as expected by the framework
|
|
370
|
+
this.enabledPlugins.forEach(({ targetAttribute, type }) => {
|
|
371
|
+
let key = `_${type}`
|
|
372
|
+
if (type === 'component' || type === 'extension') key += 's'
|
|
373
|
+
try {
|
|
374
|
+
_.merge(this.courseData.course.data._globals, {
|
|
375
|
+
[key]: { [targetAttribute]: this.courseData.course.data._globals[targetAttribute] }
|
|
376
|
+
})
|
|
377
|
+
delete this.courseData.course.data._globals[targetAttribute]
|
|
378
|
+
} catch (e) {}
|
|
379
|
+
})
|
|
380
|
+
// map course tag values (_id -> title)
|
|
381
|
+
const tags = await App.instance.waitForModule('tags')
|
|
382
|
+
const course = this.courseData.course.data
|
|
383
|
+
if (course?.tags?.length) {
|
|
384
|
+
course.tags = (await tags.find({ $or: course.tags.map(_id => Object.create({ _id })) }))
|
|
385
|
+
.map(t => t.title)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Copies the source code needed for this course. Plugin copy works as a whitelist any unknown plugin files aren't included erroneously.
|
|
391
|
+
* @return {Promise}
|
|
392
|
+
*/
|
|
393
|
+
async copySource () {
|
|
394
|
+
const { path: fwPath } = await App.instance.waitForModule('adaptframework')
|
|
395
|
+
const BLACKLIST = ['.git', '.DS_Store', 'thumbs.db', 'node_modules', 'course']
|
|
396
|
+
const ENABLED_PLUGINS = this.enabledPlugins.map(p => p.name)
|
|
397
|
+
const srcDir = path.join(fwPath, 'src')
|
|
398
|
+
await fs.copy(fwPath, this.dir, {
|
|
399
|
+
filter: f => {
|
|
400
|
+
f = path.normalize(f)
|
|
401
|
+
const [type, name] = path.relative(srcDir, f).split('/');
|
|
402
|
+
const isPlugin = f.startsWith(srcDir) && type && type !== 'core' && !!name
|
|
403
|
+
|
|
404
|
+
if(isPlugin && !ENABLED_PLUGINS.includes(name)) {
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
return !BLACKLIST.includes(path.basename(f))
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
if (!this.isExport) await fs.symlink(`${fwPath}/node_modules`, `${this.dir}/node_modules`, 'junction')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Deals with copying all assets used in this course
|
|
415
|
+
* @return {Promise}
|
|
416
|
+
*/
|
|
417
|
+
async copyAssets () {
|
|
418
|
+
const assets = await App.instance.waitForModule('assets')
|
|
419
|
+
return Promise.all(this.assetData.data.map(async a => {
|
|
420
|
+
if (a.url) {
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
await this.ensureDir(path.dirname(this.assetData.idMap[a._id]))
|
|
424
|
+
const inputStream = await assets.createFsWrapper(a).read(a)
|
|
425
|
+
const outputStream = createWriteStream(this.assetData.idMap[a._id])
|
|
426
|
+
inputStream.pipe(outputStream)
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
inputStream.on('end', () => resolve())
|
|
429
|
+
outputStream.on('error', e => reject(e))
|
|
430
|
+
})
|
|
431
|
+
}))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Outputs all course data to the required JSON files
|
|
436
|
+
* @return {Promise}
|
|
437
|
+
*/
|
|
438
|
+
async writeContentJson () {
|
|
439
|
+
const data = Object.values(this.courseData)
|
|
440
|
+
if (this.isExport && this.assetData.data.length) {
|
|
441
|
+
this.assetData.data = this.assetData.data.map(d => {
|
|
442
|
+
return {
|
|
443
|
+
title: d.title,
|
|
444
|
+
description: d.description,
|
|
445
|
+
filename: d.path,
|
|
446
|
+
tags: d.tags
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
data.push(this.assetData)
|
|
450
|
+
}
|
|
451
|
+
return Promise.all(data.map(async ({ dir, fileName, data }) => {
|
|
452
|
+
await this.ensureDir(dir)
|
|
453
|
+
const filepath = path.join(dir, fileName)
|
|
454
|
+
const returnData = await fs.writeJson(filepath, data, { spaces: 2 })
|
|
455
|
+
AdaptFrameworkUtils.log('debug', 'WRITE', filepath)
|
|
456
|
+
return returnData
|
|
457
|
+
}))
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Makes sure the output folder is structured to allow the files to be served statically for previewing
|
|
462
|
+
* @return {Promise}
|
|
463
|
+
*/
|
|
464
|
+
async createPreview () {
|
|
465
|
+
const tempName = `${this.dir}_temp`
|
|
466
|
+
await fs.move(path.join(this.dir, 'build'), tempName)
|
|
467
|
+
await fs.remove(this.dir)
|
|
468
|
+
await fs.move(tempName, this.dir)
|
|
469
|
+
this.location = this.dir
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Creates a zip file containing all files relevant to the type of build being performed
|
|
474
|
+
* @return {Promise}
|
|
475
|
+
*/
|
|
476
|
+
async createZip () {
|
|
477
|
+
const zipPath = path.join(this.dir, this.isPublish ? 'build' : '')
|
|
478
|
+
const outputPath = `${this.dir}.zip`
|
|
479
|
+
await zipper.zip(zipPath, outputPath, { removeSource: true })
|
|
480
|
+
await fs.remove(this.dir)
|
|
481
|
+
this.location = outputPath
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Stored metadata about a build attempt in the DB
|
|
486
|
+
* @return {Promise} Resolves with the DB document
|
|
487
|
+
*/
|
|
488
|
+
async recordBuildAttempt () {
|
|
489
|
+
const [framework, jsonschema, mongodb] = await App.instance.waitForModule('adaptframework', 'jsonschema', 'mongodb')
|
|
490
|
+
const schema = await jsonschema.getSchema('adaptbuild')
|
|
491
|
+
const validatedData = await schema.validate({
|
|
492
|
+
action: this.action,
|
|
493
|
+
courseId: this.courseId,
|
|
494
|
+
location: this.location,
|
|
495
|
+
expiresAt: this.expiresAt,
|
|
496
|
+
createdBy: this.userId,
|
|
497
|
+
versions: this.enabledPlugins.reduce((m, p) => {
|
|
498
|
+
return { ...m, [p.name]: p.version }
|
|
499
|
+
}, { adapt_framework: framework.version })
|
|
500
|
+
})
|
|
501
|
+
return mongodb.insert(this.collectionName, validatedData)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Removes all previous builds of this.action type
|
|
506
|
+
* @return {Promise}
|
|
507
|
+
*/
|
|
508
|
+
async removeOldBuilds () {
|
|
509
|
+
const mongodb = await App.instance.waitForModule('mongodb')
|
|
510
|
+
const query = { action: this.action, createdBy: this.userId }
|
|
511
|
+
const oldBuilds = await mongodb.find(this.collectionName, query)
|
|
512
|
+
await Promise.all(oldBuilds.map(b => fs.remove(b.location)))
|
|
513
|
+
return mongodb.deleteMany(this.collectionName, query)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export default AdaptFrameworkBuild
|