adapt-authoring-adaptframework 2.1.0 → 2.3.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.
@@ -1,4 +1,4 @@
1
- name: Standard.js formatting check
1
+ name: Lint
2
2
  on: push
3
3
  jobs:
4
4
  default:
@@ -9,4 +9,4 @@ jobs:
9
9
  with:
10
10
  node-version: 'lts/*'
11
11
  - run: npm install
12
- - run: npx standard
12
+ - run: npx standard
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash'
2
- import { App, Hook, ensureDir, writeJson } from 'adapt-authoring-core'
2
+ import { App, ensureDir, writeJson } from 'adapt-authoring-core'
3
3
  import { parseObjectId } from 'adapt-authoring-mongodb'
4
4
  import { createWriteStream } from 'node:fs'
5
5
  import AdaptCli from 'adapt-cli'
@@ -140,16 +140,6 @@ class AdaptFrameworkBuild {
140
140
  * @type {Array<Object>}
141
141
  */
142
142
  this.disabledPlugins = []
143
- /**
144
- * Invoked prior to a course being built.
145
- * @type {Hook}
146
- */
147
- this.preBuildHook = new Hook({ mutable: true })
148
- /**
149
- * Invoked after a course has been built.
150
- * @type {Hook}
151
- */
152
- this.postBuildHook = new Hook({ mutable: true })
153
143
  }
154
144
 
155
145
  /**
@@ -190,8 +180,6 @@ class AdaptFrameworkBuild {
190
180
  linkNodeModules: !this.isExport
191
181
  })
192
182
  ])
193
- await this.preBuildHook.invoke(this)
194
-
195
183
  await this.writeContentJson()
196
184
 
197
185
  logDir('courseDir', this.courseDir)
@@ -218,8 +206,6 @@ class AdaptFrameworkBuild {
218
206
  } else {
219
207
  this.location = this.isPreview ? path.join(this.dir, 'build') : this.dir
220
208
  }
221
- await this.postBuildHook.invoke(this)
222
-
223
209
  this.buildData = await this.recordBuildAttempt()
224
210
 
225
211
  return this
@@ -1,4 +1,4 @@
1
- import { App, Hook, spawn, readJson, writeJson } from 'adapt-authoring-core'
1
+ import { App, spawn, readJson, writeJson } from 'adapt-authoring-core'
2
2
  import { parseObjectId } from 'adapt-authoring-mongodb'
3
3
  import fs from 'node:fs/promises'
4
4
  import { glob } from 'glob'
@@ -194,15 +194,6 @@ class AdaptFrameworkImport {
194
194
  updatePlugins,
195
195
  removeSource
196
196
  }
197
- /**
198
- * Invoked before the import process has started
199
- */
200
- this.preImportHook = new Hook()
201
- /**
202
- * Invoked once the import process has completed
203
- */
204
- this.postImportHook = new Hook()
205
-
206
197
  /**
207
198
  * plugins on import that are of a lower version than the installed version
208
199
  * @type {Array}
@@ -264,7 +255,6 @@ class AdaptFrameworkImport {
264
255
  [this.prepare],
265
256
  [this.loadAssetData],
266
257
  [this.loadPluginData],
267
- [() => this.preImportHook.invoke(this)],
268
258
  [this.importTags, importContent],
269
259
  [this.importCourseAssets, importContent],
270
260
  [this.importCoursePlugins, isDryRun && importPlugins],
@@ -278,7 +268,6 @@ class AdaptFrameworkImport {
278
268
  for (const task of tasks) {
279
269
  if (task.length < 2 || task[1]) await task[0].call(this)
280
270
  }
281
- await this.postImportHook.invoke(this)
282
271
  } catch (e) {
283
272
  error = e
284
273
  }
@@ -804,7 +793,7 @@ class AdaptFrameworkImport {
804
793
  const schemaName = AdaptFrameworkImport.typeToSchema(data)
805
794
  const schema = await this.content.getSchema(schemaName, insertData)
806
795
  try {
807
- this.extractAssets(schema.built.properties, insertData)
796
+ this.resolveAssets(schema, insertData)
808
797
  } catch (e) {
809
798
  log('error', `failed to extract asset data for attribute '${e.attribute}' of schema '${schemaName}', ${e}`)
810
799
  }
@@ -833,27 +822,16 @@ class AdaptFrameworkImport {
833
822
  }
834
823
 
835
824
  /**
836
- * Infers the presence of any assets in incoming JSON data
837
- * @param {Object} schema Schema for the passed data
825
+ * Replaces asset paths in incoming JSON data with the _id for the corresponding asset, as used internally
826
+ * @param {Object} schema Schema instance for the passed data
838
827
  * @param {Object} data Data to check
839
828
  */
840
- extractAssets (schema, data) {
841
- if (!schema) {
842
- return
843
- }
844
- Object.entries(schema).forEach(([key, val]) => {
845
- if (data[key] === undefined) {
846
- return
847
- }
848
- if (val.properties) {
849
- this.extractAssets(val.properties, data[key])
850
- } else if (val?.items?.properties) {
851
- data[key].forEach(d => this.extractAssets(val.items.properties, d))
852
- } else if (val?._backboneForms?.type === 'Asset' || val?._backboneForms === 'Asset') {
853
- data[key] !== ''
854
- ? data[key] = this.assetMap[data[key]] ?? data[key]
855
- : delete data[key]
856
- }
829
+ resolveAssets (schema, data) {
830
+ if (!schema) return
831
+ schema.walk(data, field =>
832
+ field?._backboneForms?.type === 'Asset' || field?._backboneForms === 'Asset'
833
+ ).forEach(({ data: parent, key, value }) => {
834
+ value ? parent[key] = this.assetMap[value] ?? value : delete parent[key]
857
835
  })
858
836
  }
859
837
 
@@ -3,7 +3,7 @@ import AdaptFrameworkBuild from './AdaptFrameworkBuild.js'
3
3
  import AdaptFrameworkImport from './AdaptFrameworkImport.js'
4
4
  import fs from 'node:fs/promises'
5
5
  import { getHandler, postHandler, importHandler, postUpdateHandler, getUpdateHandler } from './handlers.js'
6
- import { loadRouteConfig } from 'adapt-authoring-server'
6
+ import { loadRouteConfig, registerRoutes } from 'adapt-authoring-server'
7
7
  import { runCliCommand } from './utils.js'
8
8
  import path from 'node:path'
9
9
  import semver from 'semver'
@@ -52,6 +52,18 @@ class AdaptFrameworkModule extends AbstractModule {
52
52
  * @type {Hook}
53
53
  */
54
54
  this.postBuildHook = new Hook({ mutable: true })
55
+ /**
56
+ * Middleware hook wrapping the full import lifecycle (pre → import → post).
57
+ * Observers receive (next, importer) and must call next() to proceed.
58
+ * @type {Hook}
59
+ */
60
+ this.importHook = new Hook({ type: Hook.Types.Middleware })
61
+ /**
62
+ * Middleware hook wrapping the full build lifecycle (pre → build → post).
63
+ * Observers receive (next, builder) and must call next() to proceed.
64
+ * @type {Hook}
65
+ */
66
+ this.buildHook = new Hook({ type: Hook.Types.Middleware })
55
67
  /**
56
68
  * Content migration functions to be run on import
57
69
  * @type {Array}
@@ -233,13 +245,7 @@ class AdaptFrameworkModule extends AbstractModule {
233
245
  handlerAliases: { getHandler, postHandler, importHandler, postUpdateHandler, getUpdateHandler }
234
246
  })
235
247
  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)
241
- }
242
- }
248
+ registerRoutes(this.apiRouter, config.routes, auth)
243
249
  }
244
250
 
245
251
  registerImportContentMigration (migration) {
@@ -256,9 +262,16 @@ class AdaptFrameworkModule extends AbstractModule {
256
262
  */
257
263
  async buildCourse (options) {
258
264
  const builder = new AdaptFrameworkBuild(options)
259
- builder.preBuildHook.tap(() => this.preBuildHook.invoke(builder))
260
- builder.postBuildHook.tap(() => this.postBuildHook.invoke(builder))
261
- return builder.build()
265
+ const core = async () => {
266
+ await this.preBuildHook.invoke(builder)
267
+ await builder.build()
268
+ await this.postBuildHook.invoke(builder)
269
+ return builder
270
+ }
271
+ if (this.buildHook.hasObservers) {
272
+ return this.buildHook.invoke(core, builder)
273
+ }
274
+ return core()
262
275
  }
263
276
 
264
277
  /**
@@ -268,9 +281,16 @@ class AdaptFrameworkModule extends AbstractModule {
268
281
  */
269
282
  async importCourse (options) {
270
283
  const importer = new AdaptFrameworkImport(options)
271
- importer.preImportHook.tap(() => this.preImportHook.invoke(importer))
272
- importer.postImportHook.tap(() => this.postImportHook.invoke(importer))
273
- return importer.import()
284
+ const core = async () => {
285
+ await this.preImportHook.invoke(importer)
286
+ await importer.import()
287
+ await this.postImportHook.invoke(importer)
288
+ return importer
289
+ }
290
+ if (this.importHook.hasObservers) {
291
+ return this.importHook.invoke(core, importer)
292
+ }
293
+ return core()
274
294
  }
275
295
  }
276
296
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-adaptframework",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",
@@ -32,7 +32,7 @@
32
32
  "peerDependencies": {
33
33
  "adapt-authoring-assets": "^1.4.3",
34
34
  "adapt-authoring-auth": "^2.0.0",
35
- "adapt-authoring-jsonschema": "^1.1.5",
35
+ "adapt-authoring-jsonschema": "^1.4.0",
36
36
  "adapt-authoring-middleware": "^1.0.1",
37
37
  "adapt-authoring-server": "^2.1.0",
38
38
  "adapt-authoring-tags": "^1.0.1"
@@ -148,54 +148,78 @@ describe('AdaptFrameworkImport', () => {
148
148
  })
149
149
  })
150
150
 
151
- describe('#extractAssets()', () => {
151
+ describe('#resolveAssets()', () => {
152
152
  function makeCtx (assetMap) {
153
153
  const ctx = { assetMap }
154
- ctx.extractAssets = AdaptFrameworkImport.prototype.extractAssets.bind(ctx)
154
+ ctx.resolveAssets = AdaptFrameworkImport.prototype.resolveAssets.bind(ctx)
155
155
  return ctx
156
156
  }
157
157
 
158
+ function makeSchema (properties) {
159
+ return {
160
+ built: { properties },
161
+ walk (data, predicate, props, parentPath = '') {
162
+ props = props ?? this.built.properties
163
+ const matches = []
164
+ for (const [key, val] of Object.entries(props)) {
165
+ if (data[key] === undefined) continue
166
+ const currentPath = parentPath ? `${parentPath}/${key}` : key
167
+ if (val.properties) {
168
+ matches.push(...this.walk(data[key], predicate, val.properties, currentPath))
169
+ } else if (val?.items?.properties) {
170
+ data[key].forEach((item, i) => {
171
+ matches.push(...this.walk(item, predicate, val.items.properties, `${currentPath}/${i}`))
172
+ })
173
+ } else if (predicate(val)) {
174
+ matches.push({ path: currentPath, key, data, value: data[key] })
175
+ }
176
+ }
177
+ return matches
178
+ }
179
+ }
180
+ }
181
+
158
182
  it('should replace asset paths with mapped IDs', () => {
159
183
  const ctx = makeCtx({ 'course/en/assets/logo.png': 'asset123' })
160
- const schema = {
184
+ const schema = makeSchema({
161
185
  _graphic: {
162
186
  properties: {
163
187
  src: { _backboneForms: 'Asset' }
164
188
  }
165
189
  }
166
- }
190
+ })
167
191
  const data = { _graphic: { src: 'course/en/assets/logo.png' } }
168
- ctx.extractAssets(schema, data)
192
+ ctx.resolveAssets(schema, data)
169
193
  assert.equal(data._graphic.src, 'asset123')
170
194
  })
171
195
 
172
196
  it('should delete empty string asset values', () => {
173
197
  const ctx = makeCtx({})
174
- const schema = {
198
+ const schema = makeSchema({
175
199
  _graphic: {
176
200
  properties: {
177
201
  src: { _backboneForms: 'Asset' }
178
202
  }
179
203
  }
180
- }
204
+ })
181
205
  const data = { _graphic: { src: '' } }
182
- ctx.extractAssets(schema, data)
206
+ ctx.resolveAssets(schema, data)
183
207
  assert.equal('src' in data._graphic, false)
184
208
  })
185
209
 
186
210
  it('should keep value when not in assetMap', () => {
187
211
  const ctx = makeCtx({})
188
- const schema = {
212
+ const schema = makeSchema({
189
213
  img: { _backboneForms: { type: 'Asset' } }
190
- }
214
+ })
191
215
  const data = { img: 'unknown/path.png' }
192
- ctx.extractAssets(schema, data)
216
+ ctx.resolveAssets(schema, data)
193
217
  assert.equal(data.img, 'unknown/path.png')
194
218
  })
195
219
 
196
220
  it('should recurse into nested properties', () => {
197
221
  const ctx = makeCtx({ 'assets/bg.jpg': 'asset456' })
198
- const schema = {
222
+ const schema = makeSchema({
199
223
  _settings: {
200
224
  properties: {
201
225
  _background: {
@@ -205,15 +229,15 @@ describe('AdaptFrameworkImport', () => {
205
229
  }
206
230
  }
207
231
  }
208
- }
232
+ })
209
233
  const data = { _settings: { _background: { src: 'assets/bg.jpg' } } }
210
- ctx.extractAssets(schema, data)
234
+ ctx.resolveAssets(schema, data)
211
235
  assert.equal(data._settings._background.src, 'asset456')
212
236
  })
213
237
 
214
238
  it('should recurse into array items', () => {
215
239
  const ctx = makeCtx({ 'assets/a.png': 'id1', 'assets/b.png': 'id2' })
216
- const schema = {
240
+ const schema = makeSchema({
217
241
  _items: {
218
242
  items: {
219
243
  properties: {
@@ -221,40 +245,40 @@ describe('AdaptFrameworkImport', () => {
221
245
  }
222
246
  }
223
247
  }
224
- }
248
+ })
225
249
  const data = {
226
250
  _items: [
227
251
  { src: 'assets/a.png' },
228
252
  { src: 'assets/b.png' }
229
253
  ]
230
254
  }
231
- ctx.extractAssets(schema, data)
255
+ ctx.resolveAssets(schema, data)
232
256
  assert.equal(data._items[0].src, 'id1')
233
257
  assert.equal(data._items[1].src, 'id2')
234
258
  })
235
259
 
236
260
  it('should skip undefined data keys', () => {
237
261
  const ctx = makeCtx({})
238
- const schema = {
262
+ const schema = makeSchema({
239
263
  _graphic: { _backboneForms: 'Asset' }
240
- }
264
+ })
241
265
  const data = {}
242
- ctx.extractAssets(schema, data)
266
+ ctx.resolveAssets(schema, data)
243
267
  assert.equal('_graphic' in data, false)
244
268
  })
245
269
 
246
270
  it('should handle null schema gracefully', () => {
247
271
  const ctx = makeCtx({})
248
- ctx.extractAssets(null, { a: 1 })
272
+ ctx.resolveAssets(null, { a: 1 })
249
273
  })
250
274
 
251
275
  it('should handle _backboneForms as object with type', () => {
252
276
  const ctx = makeCtx({ 'path/img.png': 'mapped' })
253
- const schema = {
277
+ const schema = makeSchema({
254
278
  hero: { _backboneForms: { type: 'Asset' } }
255
- }
279
+ })
256
280
  const data = { hero: 'path/img.png' }
257
- ctx.extractAssets(schema, data)
281
+ ctx.resolveAssets(schema, data)
258
282
  assert.equal(data.hero, 'mapped')
259
283
  })
260
284
  })