adapt-authoring-adaptframework 2.0.9 → 2.1.0
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/lib/AdaptFrameworkBuild.js +2 -2
- package/lib/AdaptFrameworkImport.js +6 -4
- package/lib/AdaptFrameworkModule.js +12 -40
- package/lib/handlers.js +7 -1
- package/lib/utils/copyFrameworkSource.js +1 -1
- package/lib/utils/log.js +1 -1
- package/package.json +3 -3
- package/routes.json +216 -0
- package/tests/AdaptFrameworkImport.spec.js +265 -2
- package/lib/apidefs.js +0 -153
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
import { App, Hook, ensureDir, writeJson } from 'adapt-authoring-core'
|
|
3
3
|
import { parseObjectId } from 'adapt-authoring-mongodb'
|
|
4
|
-
import { createWriteStream } from 'fs'
|
|
4
|
+
import { createWriteStream } from 'node:fs'
|
|
5
5
|
import AdaptCli from 'adapt-cli'
|
|
6
6
|
import { log, logDir, logMemory, copyFrameworkSource } from './utils.js'
|
|
7
|
-
import fs from 'fs/promises'
|
|
7
|
+
import fs from 'node:fs/promises'
|
|
8
8
|
import path from 'upath'
|
|
9
9
|
import semver from 'semver'
|
|
10
10
|
import zipper from 'zipper'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { App, Hook, spawn, readJson, writeJson } from 'adapt-authoring-core'
|
|
2
2
|
import { parseObjectId } from 'adapt-authoring-mongodb'
|
|
3
|
-
import fs from 'fs/promises'
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
4
|
import { glob } from 'glob'
|
|
5
5
|
import octopus from 'adapt-octopus'
|
|
6
6
|
import path from 'upath'
|
|
@@ -630,7 +630,8 @@ class AdaptFrameworkImport {
|
|
|
630
630
|
|
|
631
631
|
let managedPluginUpdateBlocked = false
|
|
632
632
|
Object.keys(this.usedContentPlugins).forEach(p => {
|
|
633
|
-
const
|
|
633
|
+
const { name: pluginName } = this.usedContentPlugins[p]
|
|
634
|
+
const installedP = this.installedPlugins[pluginName]
|
|
634
635
|
let { version: importVersion } = this.usedContentPlugins[p]
|
|
635
636
|
if (!semver.valid(importVersion)) {
|
|
636
637
|
if (!installedP) {
|
|
@@ -677,8 +678,9 @@ class AdaptFrameworkImport {
|
|
|
677
678
|
// pre-process: store update metadata and fix missing targetAttributes
|
|
678
679
|
for (const p of allPlugins) {
|
|
679
680
|
const isUpdate = pluginsToUpdate.includes(p)
|
|
680
|
-
|
|
681
|
-
|
|
681
|
+
const { name: pName } = this.usedContentPlugins[p]
|
|
682
|
+
if (isUpdate && this.installedPlugins[pName]) {
|
|
683
|
+
this.updatedContentPlugins[p] = this.installedPlugins[pName]
|
|
682
684
|
}
|
|
683
685
|
const pluginBowerPath = path.join(this.usedContentPlugins[p].path, 'bower.json')
|
|
684
686
|
const bowerJson = await readJson(pluginBowerPath)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AbstractModule, Hook, readJson } from 'adapt-authoring-core'
|
|
2
2
|
import AdaptFrameworkBuild from './AdaptFrameworkBuild.js'
|
|
3
3
|
import AdaptFrameworkImport from './AdaptFrameworkImport.js'
|
|
4
|
-
import
|
|
5
|
-
import fs from 'fs/promises'
|
|
4
|
+
import fs from 'node:fs/promises'
|
|
6
5
|
import { getHandler, postHandler, importHandler, postUpdateHandler, getUpdateHandler } from './handlers.js'
|
|
6
|
+
import { loadRouteConfig } from 'adapt-authoring-server'
|
|
7
7
|
import { runCliCommand } from './utils.js'
|
|
8
|
-
import path from 'path'
|
|
8
|
+
import path from 'node:path'
|
|
9
9
|
import semver from 'semver'
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -229,44 +229,16 @@ class AdaptFrameworkModule extends AbstractModule {
|
|
|
229
229
|
* Router for handling all API calls
|
|
230
230
|
* @type {Router}
|
|
231
231
|
*/
|
|
232
|
-
this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
{
|
|
240
|
-
route
|
|
241
|
-
handlers: { post: postHandler, get: getHandler },
|
|
242
|
-
meta: ApiDefs.publish
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
route: '/import',
|
|
246
|
-
handlers: { post: [importHandler] },
|
|
247
|
-
meta: ApiDefs.import
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
route: '/export/:id',
|
|
251
|
-
handlers: { post: postHandler, get: getHandler },
|
|
252
|
-
meta: ApiDefs.export
|
|
232
|
+
const config = await loadRouteConfig(this.rootDir, this, {
|
|
233
|
+
handlerAliases: { getHandler, postHandler, importHandler, postUpdateHandler, getUpdateHandler }
|
|
234
|
+
})
|
|
235
|
+
this.apiRouter = server.api.createChildRouter(config.root)
|
|
236
|
+
for (const r of config.routes) {
|
|
237
|
+
this.apiRouter.addRoute(r)
|
|
238
|
+
if (!r.permissions) continue
|
|
239
|
+
for (const [method, perms] of Object.entries(r.permissions)) {
|
|
240
|
+
auth.secureRoute(`${this.apiRouter.path}${r.route}`, method, perms)
|
|
253
241
|
}
|
|
254
|
-
)
|
|
255
|
-
auth.secureRoute(`${this.apiRouter.path}/preview/:id`, 'post', ['preview:adapt'])
|
|
256
|
-
auth.secureRoute(`${this.apiRouter.path}/publish/:id`, 'get', ['publish:adapt'])
|
|
257
|
-
auth.secureRoute(`${this.apiRouter.path}/publish/:id`, 'post', ['publish:adapt'])
|
|
258
|
-
auth.secureRoute(`${this.apiRouter.path}/import`, 'post', ['import:adapt'])
|
|
259
|
-
auth.secureRoute(`${this.apiRouter.path}/export/:id`, 'get', ['export:adapt'])
|
|
260
|
-
auth.secureRoute(`${this.apiRouter.path}/export/:id`, 'post', ['export:adapt'])
|
|
261
|
-
auth.secureRoute(`${this.apiRouter.path}/update`, 'post', ['update:adapt'])
|
|
262
|
-
|
|
263
|
-
if (this.getConfig('enableUpdateApi')) {
|
|
264
|
-
this.apiRouter.addRoute({
|
|
265
|
-
route: '/update',
|
|
266
|
-
handlers: { post: postUpdateHandler, get: getUpdateHandler },
|
|
267
|
-
meta: ApiDefs.update
|
|
268
|
-
})
|
|
269
|
-
auth.secureRoute(`${this.apiRouter.path}/update`, 'get', ['update:adapt'])
|
|
270
242
|
}
|
|
271
243
|
}
|
|
272
244
|
|
package/lib/handlers.js
CHANGED
|
@@ -127,8 +127,11 @@ export async function importHandler (req, res, next) {
|
|
|
127
127
|
*/
|
|
128
128
|
export async function postUpdateHandler (req, res, next) {
|
|
129
129
|
try {
|
|
130
|
-
log('info', 'running framework update')
|
|
131
130
|
const framework = await App.instance.waitForModule('adaptframework')
|
|
131
|
+
if (!framework.getConfig('enableUpdateApi')) {
|
|
132
|
+
return next(App.instance.errors.NOT_FOUND.setData({ type: 'route', id: req.originalUrl }))
|
|
133
|
+
}
|
|
134
|
+
log('info', 'running framework update')
|
|
132
135
|
const previousVersion = framework.version
|
|
133
136
|
await framework.updateFramework(req.body.version)
|
|
134
137
|
const currentVersion = framework.version !== previousVersion ? framework.version : undefined
|
|
@@ -151,6 +154,9 @@ export async function postUpdateHandler (req, res, next) {
|
|
|
151
154
|
export async function getUpdateHandler (req, res, next) {
|
|
152
155
|
try {
|
|
153
156
|
const framework = await App.instance.waitForModule('adaptframework')
|
|
157
|
+
if (!framework.getConfig('enableUpdateApi')) {
|
|
158
|
+
return next(App.instance.errors.NOT_FOUND.setData({ type: 'route', id: req.originalUrl }))
|
|
159
|
+
}
|
|
154
160
|
const current = framework.version
|
|
155
161
|
const latest = await framework.getLatestVersion()
|
|
156
162
|
res.json({
|
package/lib/utils/log.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-adaptframework",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Adapt framework integration for the Adapt authoring tool",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-adaptframework",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"repository": "github:adapt-security/adapt-authoring-adaptframework",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --test 'tests/**/*.spec.js'"
|
|
11
|
+
"test": "node --test --test-force-exit --experimental-test-module-mocks 'tests/**/*.spec.js'"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"adapt-authoring-browserslist": "^1.3.4",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"adapt-authoring-auth": "^2.0.0",
|
|
35
35
|
"adapt-authoring-jsonschema": "^1.1.5",
|
|
36
36
|
"adapt-authoring-middleware": "^1.0.1",
|
|
37
|
-
"adapt-authoring-server": "^2.
|
|
37
|
+
"adapt-authoring-server": "^2.1.0",
|
|
38
38
|
"adapt-authoring-tags": "^1.0.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/routes.json
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": "adapt",
|
|
3
|
+
"routes": [
|
|
4
|
+
{
|
|
5
|
+
"route": "/preview/:id",
|
|
6
|
+
"handlers": { "post": "postHandler" },
|
|
7
|
+
"permissions": { "post": ["preview:adapt"] },
|
|
8
|
+
"meta": {
|
|
9
|
+
"post": {
|
|
10
|
+
"summary": "Build a preview of an Adapt course",
|
|
11
|
+
"responses": {
|
|
12
|
+
"200": {
|
|
13
|
+
"description": "The Adapt build data",
|
|
14
|
+
"content": {
|
|
15
|
+
"application/json": {
|
|
16
|
+
"schema": { "$ref": "#components/schemas/adaptbuild" }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"route": "/publish/:id",
|
|
26
|
+
"handlers": { "post": "postHandler", "get": "getHandler" },
|
|
27
|
+
"permissions": { "post": ["publish:adapt"], "get": ["publish:adapt"] },
|
|
28
|
+
"meta": {
|
|
29
|
+
"post": {
|
|
30
|
+
"summary": "Create a publish zip of an Adapt course",
|
|
31
|
+
"responses": {
|
|
32
|
+
"200": {
|
|
33
|
+
"description": "The Adapt build data",
|
|
34
|
+
"content": {
|
|
35
|
+
"application/json": {
|
|
36
|
+
"schema": { "$ref": "#components/schemas/adaptbuild" }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"get": {
|
|
43
|
+
"summary": "Retrieve an Adapt course publish zip",
|
|
44
|
+
"responses": {
|
|
45
|
+
"200": {
|
|
46
|
+
"description": "Course build zip file",
|
|
47
|
+
"content": { "application/zip": {} }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"route": "/import",
|
|
55
|
+
"handlers": { "post": "importHandler" },
|
|
56
|
+
"permissions": { "post": ["import:adapt"] },
|
|
57
|
+
"meta": {
|
|
58
|
+
"post": {
|
|
59
|
+
"summary": "Import an Adapt course",
|
|
60
|
+
"requestBody": {
|
|
61
|
+
"content": {
|
|
62
|
+
"application/json": {
|
|
63
|
+
"schema": {
|
|
64
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
65
|
+
"type": "object",
|
|
66
|
+
"properties": {
|
|
67
|
+
"path": { "type": "String" },
|
|
68
|
+
"isDryRun": { "type": "Boolean", "default": false },
|
|
69
|
+
"importContent": { "type": "Boolean", "default": true },
|
|
70
|
+
"importPlugins": { "type": "Boolean", "default": true },
|
|
71
|
+
"updatePlugins": { "type": "Boolean", "default": false }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"responses": {
|
|
78
|
+
"200": {
|
|
79
|
+
"description": "",
|
|
80
|
+
"content": {
|
|
81
|
+
"application/json": {
|
|
82
|
+
"schema": {
|
|
83
|
+
"properties": {
|
|
84
|
+
"title": { "type": "string" },
|
|
85
|
+
"courseId": { "type": "string" },
|
|
86
|
+
"versions": { "type": "object" },
|
|
87
|
+
"content": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"course": { "type": "number" },
|
|
91
|
+
"config": { "type": "number" },
|
|
92
|
+
"menu": { "type": "number" },
|
|
93
|
+
"page": { "type": "number" },
|
|
94
|
+
"article": { "type": "number" },
|
|
95
|
+
"block": { "type": "number" },
|
|
96
|
+
"component": { "type": "number" }
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"statusReport": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"error": {
|
|
103
|
+
"type": "array",
|
|
104
|
+
"items": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"code": { "type": "string" },
|
|
108
|
+
"data": { "type": "string" }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"warn": {
|
|
113
|
+
"type": "array",
|
|
114
|
+
"items": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"code": { "type": "string" },
|
|
118
|
+
"data": { "type": "string" }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"info": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"items": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"code": { "type": "string" },
|
|
128
|
+
"data": { "type": "string" }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"route": "/export/:id",
|
|
145
|
+
"handlers": { "post": "postHandler", "get": "getHandler" },
|
|
146
|
+
"permissions": { "post": ["export:adapt"], "get": ["export:adapt"] },
|
|
147
|
+
"meta": {
|
|
148
|
+
"post": {
|
|
149
|
+
"summary": "Create an export zip of an Adapt course",
|
|
150
|
+
"responses": {
|
|
151
|
+
"200": {
|
|
152
|
+
"description": "The Adapt build data",
|
|
153
|
+
"content": {
|
|
154
|
+
"application/json": {
|
|
155
|
+
"schema": { "$ref": "#components/schemas/adaptbuild" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"get": {
|
|
162
|
+
"summary": "Retrieve an Adapt course export zip",
|
|
163
|
+
"responses": {
|
|
164
|
+
"200": {
|
|
165
|
+
"description": "Course build zip file",
|
|
166
|
+
"content": { "application/zip": {} }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"route": "/update",
|
|
174
|
+
"handlers": { "post": "postUpdateHandler", "get": "getUpdateHandler" },
|
|
175
|
+
"permissions": { "post": ["update:adapt"], "get": ["update:adapt"] },
|
|
176
|
+
"meta": {
|
|
177
|
+
"post": {
|
|
178
|
+
"summary": "Updates the installed framework",
|
|
179
|
+
"responses": {
|
|
180
|
+
"200": {
|
|
181
|
+
"description": "Describes the upgraded elements",
|
|
182
|
+
"content": {
|
|
183
|
+
"application/json": {
|
|
184
|
+
"schema": {
|
|
185
|
+
"properties": {
|
|
186
|
+
"from": { "type": "string" },
|
|
187
|
+
"to": { "type": "string" }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"get": {
|
|
196
|
+
"summary": "Retrieve framework update data",
|
|
197
|
+
"responses": {
|
|
198
|
+
"200": {
|
|
199
|
+
"content": {
|
|
200
|
+
"application/json": {
|
|
201
|
+
"schema": {
|
|
202
|
+
"properties": {
|
|
203
|
+
"canBeUpdated": { "type": "boolean" },
|
|
204
|
+
"currentVersion": { "type": "string" },
|
|
205
|
+
"latestCompatibleVersion": { "type": "string" }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
1
|
+
import { describe, it, mock } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// Prevent log() from triggering App.instance boot during tests
|
|
5
|
+
mock.module('../lib/utils/log.js', {
|
|
6
|
+
namedExports: {
|
|
7
|
+
log: async () => {},
|
|
8
|
+
logDir: () => {},
|
|
9
|
+
logMemory: () => {}
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { default: AdaptFrameworkImport } = await import('../lib/AdaptFrameworkImport.js')
|
|
4
14
|
|
|
5
15
|
describe('AdaptFrameworkImport', () => {
|
|
6
16
|
describe('.typeToSchema()', () => {
|
|
@@ -324,4 +334,257 @@ describe('AdaptFrameworkImport', () => {
|
|
|
324
334
|
assert.equal(ctx.contentJson.contentObjects.art1._type, 'article')
|
|
325
335
|
})
|
|
326
336
|
})
|
|
337
|
+
|
|
338
|
+
describe('#cleanUp()', () => {
|
|
339
|
+
const cleanUp = AdaptFrameworkImport.prototype.cleanUp
|
|
340
|
+
|
|
341
|
+
it('should call rollback when an error is passed', async () => {
|
|
342
|
+
let rollbackCalled = false
|
|
343
|
+
const ctx = {
|
|
344
|
+
settings: { removeSource: false },
|
|
345
|
+
rollback: async () => { rollbackCalled = true }
|
|
346
|
+
}
|
|
347
|
+
await cleanUp.call(ctx, new Error('test'))
|
|
348
|
+
assert.equal(rollbackCalled, true)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('should not call rollback when no error is passed', async () => {
|
|
352
|
+
let rollbackCalled = false
|
|
353
|
+
const ctx = {
|
|
354
|
+
settings: { removeSource: false },
|
|
355
|
+
rollback: async () => { rollbackCalled = true }
|
|
356
|
+
}
|
|
357
|
+
await cleanUp.call(ctx, undefined)
|
|
358
|
+
assert.equal(rollbackCalled, false)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('should run rollback even when removeSource is false', async () => {
|
|
362
|
+
let rollbackCalled = false
|
|
363
|
+
const ctx = {
|
|
364
|
+
settings: { removeSource: false },
|
|
365
|
+
rollback: async () => { rollbackCalled = true }
|
|
366
|
+
}
|
|
367
|
+
await cleanUp.call(ctx, new Error('test'))
|
|
368
|
+
assert.equal(rollbackCalled, true)
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
describe('#rollback()', () => {
|
|
373
|
+
const rollback = AdaptFrameworkImport.prototype.rollback
|
|
374
|
+
|
|
375
|
+
function makeRollbackCtx (overrides = {}) {
|
|
376
|
+
return {
|
|
377
|
+
newContentPlugins: {},
|
|
378
|
+
updatedContentPlugins: {},
|
|
379
|
+
assetMap: {},
|
|
380
|
+
newTagIds: [],
|
|
381
|
+
contentJson: { course: {} },
|
|
382
|
+
idMap: {},
|
|
383
|
+
contentplugin: null,
|
|
384
|
+
assets: null,
|
|
385
|
+
content: null,
|
|
386
|
+
courseassets: null,
|
|
387
|
+
...overrides
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
it('should uninstall newly installed plugins', async () => {
|
|
392
|
+
const uninstalled = []
|
|
393
|
+
const ctx = makeRollbackCtx({
|
|
394
|
+
contentplugin: {
|
|
395
|
+
uninstallPlugin: async (id) => uninstalled.push(id)
|
|
396
|
+
},
|
|
397
|
+
newContentPlugins: {
|
|
398
|
+
'adapt-contrib-text': { _id: 'p1', name: 'adapt-contrib-text' },
|
|
399
|
+
'adapt-contrib-gmcq': { _id: 'p2', name: 'adapt-contrib-gmcq' }
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
await rollback.call(ctx)
|
|
403
|
+
assert.deepEqual(uninstalled.sort(), ['p1', 'p2'])
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('should delete imported assets', async () => {
|
|
407
|
+
const deleted = []
|
|
408
|
+
const ctx = makeRollbackCtx({
|
|
409
|
+
assets: {
|
|
410
|
+
delete: async ({ _id }) => deleted.push(_id)
|
|
411
|
+
},
|
|
412
|
+
assetMap: {
|
|
413
|
+
'course/en/assets/logo.png': 'a1',
|
|
414
|
+
'course/en/assets/bg.jpg': 'a2'
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
await rollback.call(ctx)
|
|
418
|
+
assert.deepEqual(deleted.sort(), ['a1', 'a2'])
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('should delete course content and course assets', async () => {
|
|
422
|
+
const contentDeleted = []
|
|
423
|
+
const courseAssetsDeleted = []
|
|
424
|
+
const ctx = makeRollbackCtx({
|
|
425
|
+
content: {
|
|
426
|
+
deleteMany: async (query) => contentDeleted.push(query)
|
|
427
|
+
},
|
|
428
|
+
courseassets: {
|
|
429
|
+
deleteMany: async (query) => courseAssetsDeleted.push(query)
|
|
430
|
+
},
|
|
431
|
+
contentJson: { course: { _id: 'oldCourseId' } },
|
|
432
|
+
idMap: { oldCourseId: '507f1f77bcf86cd799439011' }
|
|
433
|
+
})
|
|
434
|
+
await rollback.call(ctx)
|
|
435
|
+
assert.equal(contentDeleted.length, 1)
|
|
436
|
+
assert.equal(courseAssetsDeleted.length, 1)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('should skip plugin uninstall when contentplugin is not available', async () => {
|
|
440
|
+
const ctx = makeRollbackCtx({
|
|
441
|
+
contentplugin: null,
|
|
442
|
+
newContentPlugins: { 'adapt-contrib-text': { _id: 'p1', name: 'adapt-contrib-text' } }
|
|
443
|
+
})
|
|
444
|
+
await rollback.call(ctx) // should not throw
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('should skip asset deletion when assets module is not available', async () => {
|
|
448
|
+
const ctx = makeRollbackCtx({
|
|
449
|
+
assets: null,
|
|
450
|
+
assetMap: { 'some/path.png': 'a1' }
|
|
451
|
+
})
|
|
452
|
+
await rollback.call(ctx) // should not throw
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('should skip content deletion when course ID is not in idMap', async () => {
|
|
456
|
+
const deleted = []
|
|
457
|
+
const ctx = makeRollbackCtx({
|
|
458
|
+
content: {
|
|
459
|
+
deleteMany: async (query) => deleted.push(query)
|
|
460
|
+
},
|
|
461
|
+
courseassets: {
|
|
462
|
+
deleteMany: async (query) => deleted.push(query)
|
|
463
|
+
},
|
|
464
|
+
contentJson: { course: { _id: 'oldCourseId' } },
|
|
465
|
+
idMap: {} // no mapping exists
|
|
466
|
+
})
|
|
467
|
+
await rollback.call(ctx)
|
|
468
|
+
assert.equal(deleted.length, 0)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('should continue cleaning up when an individual asset deletion fails', async () => {
|
|
472
|
+
const deleted = []
|
|
473
|
+
const ctx = makeRollbackCtx({
|
|
474
|
+
assets: {
|
|
475
|
+
delete: async ({ _id }) => {
|
|
476
|
+
if (_id === 'a1') throw new Error('delete failed')
|
|
477
|
+
deleted.push(_id)
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
assetMap: {
|
|
481
|
+
'path/a.png': 'a1',
|
|
482
|
+
'path/b.png': 'a2',
|
|
483
|
+
'path/c.png': 'a3'
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
await rollback.call(ctx)
|
|
487
|
+
assert.deepEqual(deleted.sort(), ['a2', 'a3'])
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('should continue cleaning up when an individual plugin uninstall fails', async () => {
|
|
491
|
+
const uninstalled = []
|
|
492
|
+
const ctx = makeRollbackCtx({
|
|
493
|
+
contentplugin: {
|
|
494
|
+
uninstallPlugin: async (id) => {
|
|
495
|
+
if (id === 'p1') throw new Error('uninstall failed')
|
|
496
|
+
uninstalled.push(id)
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
newContentPlugins: {
|
|
500
|
+
'plugin-a': { _id: 'p1', name: 'plugin-a' },
|
|
501
|
+
'plugin-b': { _id: 'p2', name: 'plugin-b' }
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
await rollback.call(ctx)
|
|
505
|
+
assert.deepEqual(uninstalled, ['p2'])
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
describe('#importCoursePlugins() - early missing plugin detection', () => {
|
|
510
|
+
const importCoursePlugins = AdaptFrameworkImport.prototype.importCoursePlugins
|
|
511
|
+
|
|
512
|
+
function makePluginCtx (overrides = {}) {
|
|
513
|
+
return {
|
|
514
|
+
configEnabledPlugins: [],
|
|
515
|
+
usedContentPlugins: {},
|
|
516
|
+
installedPlugins: {},
|
|
517
|
+
newContentPlugins: {},
|
|
518
|
+
updatedContentPlugins: {},
|
|
519
|
+
pluginsToMigrate: ['core'],
|
|
520
|
+
componentNameMap: {},
|
|
521
|
+
settings: { isDryRun: false, importPlugins: true, updatePlugins: false },
|
|
522
|
+
statusReport: { info: [], warn: [], error: [] },
|
|
523
|
+
contentplugin: {
|
|
524
|
+
find: async () => []
|
|
525
|
+
},
|
|
526
|
+
...overrides
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
it('should report missing plugins in statusReport during dry run', async () => {
|
|
531
|
+
const ctx = makePluginCtx({
|
|
532
|
+
configEnabledPlugins: ['adapt-contrib-text', 'adapt-contrib-missing'],
|
|
533
|
+
usedContentPlugins: { 'adapt-contrib-text': { version: '1.0.0' } },
|
|
534
|
+
settings: { isDryRun: true, importPlugins: true, updatePlugins: false },
|
|
535
|
+
contentplugin: { find: async () => [] }
|
|
536
|
+
})
|
|
537
|
+
await importCoursePlugins.call(ctx)
|
|
538
|
+
assert.equal(ctx.statusReport.error.length, 1)
|
|
539
|
+
assert.equal(ctx.statusReport.error[0].code, 'MISSING_PLUGINS')
|
|
540
|
+
assert.deepEqual(ctx.statusReport.error[0].data, ['adapt-contrib-missing'])
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('should not report error when config plugins exist in the import package', async () => {
|
|
544
|
+
const ctx = makePluginCtx({
|
|
545
|
+
configEnabledPlugins: ['adapt-contrib-text'],
|
|
546
|
+
usedContentPlugins: { 'adapt-contrib-text': { name: 'adapt-contrib-text', version: '1.0.0', type: 'component' } },
|
|
547
|
+
contentplugin: {
|
|
548
|
+
find: async () => [{ name: 'adapt-contrib-text', version: '1.0.0', targetAttribute: '_text', isLocalInstall: true }]
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
await importCoursePlugins.call(ctx)
|
|
552
|
+
assert.equal(ctx.statusReport.error.length, 0)
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('should not report error when config plugins are installed on the server', async () => {
|
|
556
|
+
const ctx = makePluginCtx({
|
|
557
|
+
configEnabledPlugins: ['adapt-contrib-text'],
|
|
558
|
+
usedContentPlugins: {},
|
|
559
|
+
contentplugin: {
|
|
560
|
+
find: async () => [{ name: 'adapt-contrib-text', version: '1.0.0', targetAttribute: '_text' }]
|
|
561
|
+
}
|
|
562
|
+
})
|
|
563
|
+
await importCoursePlugins.call(ctx)
|
|
564
|
+
assert.equal(ctx.statusReport.error.length, 0)
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('should not report error when configEnabledPlugins is empty', async () => {
|
|
568
|
+
const ctx = makePluginCtx({
|
|
569
|
+
configEnabledPlugins: [],
|
|
570
|
+
contentplugin: { find: async () => [] }
|
|
571
|
+
})
|
|
572
|
+
await importCoursePlugins.call(ctx)
|
|
573
|
+
assert.equal(ctx.statusReport.error.length, 0)
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('should only flag plugins missing from both package and server', async () => {
|
|
577
|
+
const ctx = makePluginCtx({
|
|
578
|
+
configEnabledPlugins: ['adapt-contrib-text', 'adapt-contrib-gmcq', 'adapt-contrib-missing'],
|
|
579
|
+
usedContentPlugins: { 'adapt-contrib-text': { version: '1.0.0' } },
|
|
580
|
+
settings: { isDryRun: true, importPlugins: true, updatePlugins: false },
|
|
581
|
+
contentplugin: {
|
|
582
|
+
find: async () => [{ name: 'adapt-contrib-gmcq', version: '2.0.0', targetAttribute: '_gmcq' }]
|
|
583
|
+
}
|
|
584
|
+
})
|
|
585
|
+
await importCoursePlugins.call(ctx)
|
|
586
|
+
assert.equal(ctx.statusReport.error.length, 1)
|
|
587
|
+
assert.deepEqual(ctx.statusReport.error[0].data, ['adapt-contrib-missing'])
|
|
588
|
+
})
|
|
589
|
+
})
|
|
327
590
|
})
|
package/lib/apidefs.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
const responseDataMeta = {
|
|
2
|
-
200: {
|
|
3
|
-
description: 'The Adapt build data',
|
|
4
|
-
content: {
|
|
5
|
-
'application/json': {
|
|
6
|
-
schema: { $ref: '#components/schemas/adaptbuild' }
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const responseZipMeta = {
|
|
13
|
-
200: {
|
|
14
|
-
description: 'Course build zip file',
|
|
15
|
-
content: { 'application/zip': {} }
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const statusReportItemSchema = {
|
|
20
|
-
type: 'array',
|
|
21
|
-
items: {
|
|
22
|
-
type: 'object',
|
|
23
|
-
properties: {
|
|
24
|
-
code: { type: 'string' },
|
|
25
|
-
data: { type: 'string' }
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default {
|
|
31
|
-
preview: {
|
|
32
|
-
post: {
|
|
33
|
-
summary: 'Build a preview of an Adapt course',
|
|
34
|
-
responses: responseDataMeta
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
publish: {
|
|
38
|
-
post: {
|
|
39
|
-
summary: 'Create a publish zip of an Adapt course',
|
|
40
|
-
responses: responseDataMeta
|
|
41
|
-
},
|
|
42
|
-
get: {
|
|
43
|
-
summary: 'Retrieve an Adapt course publish zip',
|
|
44
|
-
responses: responseZipMeta
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
import: {
|
|
48
|
-
post: {
|
|
49
|
-
summary: 'Import an Adapt course',
|
|
50
|
-
requestBody: {
|
|
51
|
-
content: {
|
|
52
|
-
'application/json': {
|
|
53
|
-
schema: {
|
|
54
|
-
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
55
|
-
type: 'object',
|
|
56
|
-
properties: {
|
|
57
|
-
path: { type: 'String' },
|
|
58
|
-
isDryRun: { type: 'Boolean', default: false },
|
|
59
|
-
importContent: { type: 'Boolean', default: true },
|
|
60
|
-
importPlugins: { type: 'Boolean', default: true },
|
|
61
|
-
updatePlugins: { type: 'Boolean', default: false }
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
responses: {
|
|
68
|
-
200: {
|
|
69
|
-
description: '',
|
|
70
|
-
content: {
|
|
71
|
-
'application/json': {
|
|
72
|
-
schema: {
|
|
73
|
-
properties: {
|
|
74
|
-
title: { type: 'string' },
|
|
75
|
-
courseId: { type: 'string' },
|
|
76
|
-
versions: { type: 'object' },
|
|
77
|
-
content: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {
|
|
80
|
-
course: { type: 'number' },
|
|
81
|
-
config: { type: 'number' },
|
|
82
|
-
menu: { type: 'number' },
|
|
83
|
-
page: { type: 'number' },
|
|
84
|
-
article: { type: 'number' },
|
|
85
|
-
block: { type: 'number' },
|
|
86
|
-
component: { type: 'number' }
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
statusReport: {
|
|
90
|
-
type: 'object',
|
|
91
|
-
properties: {
|
|
92
|
-
error: statusReportItemSchema,
|
|
93
|
-
warn: statusReportItemSchema,
|
|
94
|
-
info: statusReportItemSchema
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
export: {
|
|
106
|
-
post: {
|
|
107
|
-
summary: 'Create an export zip of an Adapt course',
|
|
108
|
-
responses: responseDataMeta
|
|
109
|
-
},
|
|
110
|
-
get: {
|
|
111
|
-
summary: 'Retrieve an Adapt course export zip',
|
|
112
|
-
responses: responseZipMeta
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
update: {
|
|
116
|
-
post: {
|
|
117
|
-
summary: 'Updates the installed framework',
|
|
118
|
-
responses: {
|
|
119
|
-
200: {
|
|
120
|
-
description: 'Describes the upgraded elements',
|
|
121
|
-
content: {
|
|
122
|
-
'application/json': {
|
|
123
|
-
schema: {
|
|
124
|
-
properties: {
|
|
125
|
-
from: { type: 'string' },
|
|
126
|
-
to: { type: 'string' }
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
get: {
|
|
135
|
-
summary: 'Retrieve framework update data',
|
|
136
|
-
responses: {
|
|
137
|
-
200: {
|
|
138
|
-
content: {
|
|
139
|
-
'application/json': {
|
|
140
|
-
schema: {
|
|
141
|
-
properties: {
|
|
142
|
-
canBeUpdated: { type: 'boolean' },
|
|
143
|
-
currentVersion: { type: 'string' },
|
|
144
|
-
latestCompatibleVersion: { type: 'string' }
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|