adapt-authoring-content 1.3.0 → 2.0.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,5 +1,6 @@
1
- import { AbstractApiModule, AbstractApiUtils } from 'adapt-authoring-api'
2
- import { Hook } from 'adapt-authoring-core'
1
+ import { AbstractApiModule } from 'adapt-authoring-api'
2
+ import { getDescendants } from './utils.js'
3
+ import { Hook, stringifyValues } from 'adapt-authoring-core'
3
4
  import apidefs from './apidefs.js'
4
5
  /**
5
6
  * Module which handles course content
@@ -139,7 +140,7 @@ class ContentModule extends AbstractApiModule {
139
140
  if (!targetDoc) {
140
141
  throw this.app.errors.NOT_FOUND.setData({ type: options.schemaName, id: JSON.stringify(query) })
141
142
  }
142
- const descendants = await this.getDescendants(targetDoc)
143
+ const descendants = await getDescendants(q => this.find(q), targetDoc)
143
144
 
144
145
  await Promise.all([...descendants, targetDoc].map(d => {
145
146
  return super.delete({ _id: d._id })
@@ -151,27 +152,6 @@ class ContentModule extends AbstractApiModule {
151
152
  return [targetDoc, ...descendants]
152
153
  }
153
154
 
154
- /**
155
- * Finds all descendant content items for a given root
156
- * @param {Object} rootItem The root item document
157
- * @returns {Array<Object>} Array of content items
158
- */
159
- async getDescendants (rootItem) {
160
- const courseItems = await this.find({ _courseId: rootItem._courseId })
161
- const descendants = []
162
- let items = [rootItem]
163
- do {
164
- items = items.reduce((m, i) => [...m, ...courseItems.filter(c => c._parentId?.toString() === i._id.toString())], [])
165
- descendants.push(...items)
166
- } while (items.length)
167
-
168
- if (rootItem._type === 'course') {
169
- const config = courseItems.find(c => c._type === 'config')
170
- if (config) descendants.push(config)
171
- }
172
- return descendants
173
- }
174
-
175
155
  /**
176
156
  * Creates a new parent content type, along with any necessary children
177
157
  * @param {external:ExpressRequest} req
@@ -248,7 +228,7 @@ class ContentModule extends AbstractApiModule {
248
228
  throw this.app.errors.INVALID_PARENT.setData({ parentId: _parentId })
249
229
  }
250
230
  const schemaName = originalDoc._type === 'menu' || originalDoc._type === 'page' ? 'contentobject' : originalDoc._type
251
- const payload = AbstractApiUtils.stringifyValues({
231
+ const payload = stringifyValues({
252
232
  ...originalDoc,
253
233
  _id: undefined,
254
234
  _trackingId: undefined,
@@ -261,13 +241,12 @@ class ContentModule extends AbstractApiModule {
261
241
 
262
242
  if (originalDoc._type === 'course') {
263
243
  const [config] = await this.find({ _type: 'config', _courseId: originalDoc._courseId })
264
- await this.clone(userId, config._id, undefined, { _courseId: newData._id.toString() })
265
- delete payload._id
266
- delete payload._courseId
267
- // the config did not exist when the new course object was created
268
- // schema validation will have therefore stripped the plugin configuration data
269
- // here we restore the configuraton data
270
- await this.update({ _id: newData._id }, payload)
244
+ if (config) {
245
+ await this.clone(userId, config._id, undefined, { _courseId: newData._id.toString() })
246
+ delete payload._id
247
+ delete payload._courseId
248
+ await this.update({ _id: newData._id }, payload)
249
+ }
271
250
  }
272
251
  const children = await this.find({ _parentId: _id })
273
252
  for (let i = 0; i < children.length; i++) {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Finds all descendant content items for a given root using BFS traversal
3
+ * @param {Function} findFn Function to query content items (receives query object, returns array)
4
+ * @param {Object} rootItem The root item document
5
+ * @returns {Promise<Array<Object>>} Array of descendant content items
6
+ * @memberof content
7
+ */
8
+ export async function getDescendants (findFn, rootItem) {
9
+ const courseItems = await findFn({ _courseId: rootItem._courseId })
10
+ const descendants = []
11
+ let items = [rootItem]
12
+ do {
13
+ items = items.reduce((m, i) => [...m, ...courseItems.filter(c => c._parentId?.toString() === i._id.toString())], [])
14
+ descendants.push(...items)
15
+ } while (items.length)
16
+
17
+ if (rootItem._type === 'course') {
18
+ const config = courseItems.find(c => c._type === 'config')
19
+ if (config) descendants.push(config)
20
+ }
21
+ return descendants
22
+ }
package/lib/utils.js ADDED
@@ -0,0 +1 @@
1
+ export { getDescendants } from './utils/getDescendants.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-content",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "Module for managing Adapt content",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-content",
6
6
  "license": "GPL-3.0",
@@ -11,8 +11,8 @@
11
11
  "test": "node --test 'tests/**/*.spec.js'"
12
12
  },
13
13
  "dependencies": {
14
- "adapt-authoring-api": "^1.3.2",
15
- "adapt-authoring-core": "^1.7.0"
14
+ "adapt-authoring-api": "^2.0.0",
15
+ "adapt-authoring-core": "^2.0.0"
16
16
  },
17
17
  "peerDependencies": {
18
18
  "adapt-authoring-authored": "^1.1.1",
@@ -1,5 +1,6 @@
1
1
  import { describe, it, mock, beforeEach } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
+ import { getDescendants } from '../lib/utils.js'
3
4
 
4
5
  /**
5
6
  * ContentModule extends AbstractApiModule (which extends AbstractModule).
@@ -338,7 +339,7 @@ describe('ContentModule', () => {
338
339
  ])
339
340
  })
340
341
 
341
- const result = await ContentModule.prototype.getDescendants.call(inst, {
342
+ const result = await getDescendants(q => inst.find(q), {
342
343
  _id: 'root',
343
344
  _courseId: 'c1',
344
345
  _type: 'page'
@@ -356,7 +357,7 @@ describe('ContentModule', () => {
356
357
  ])
357
358
  })
358
359
 
359
- const result = await ContentModule.prototype.getDescendants.call(inst, {
360
+ const result = await getDescendants(q => inst.find(q), {
360
361
  _id: 'root',
361
362
  _courseId: 'c1',
362
363
  _type: 'page'
@@ -379,7 +380,7 @@ describe('ContentModule', () => {
379
380
  ])
380
381
  })
381
382
 
382
- const result = await ContentModule.prototype.getDescendants.call(inst, {
383
+ const result = await getDescendants(q => inst.find(q), {
383
384
  _id: 'root',
384
385
  _courseId: 'c1',
385
386
  _type: 'page'
@@ -401,7 +402,7 @@ describe('ContentModule', () => {
401
402
  ])
402
403
  })
403
404
 
404
- const result = await ContentModule.prototype.getDescendants.call(inst, {
405
+ const result = await getDescendants(q => inst.find(q), {
405
406
  _id: 'c1',
406
407
  _courseId: 'c1',
407
408
  _type: 'course'
@@ -419,7 +420,7 @@ describe('ContentModule', () => {
419
420
  ])
420
421
  })
421
422
 
422
- const result = await ContentModule.prototype.getDescendants.call(inst, {
423
+ const result = await getDescendants(q => inst.find(q), {
423
424
  _id: 'page1',
424
425
  _courseId: 'c1',
425
426
  _type: 'page'
@@ -445,7 +446,7 @@ describe('ContentModule', () => {
445
446
  find: mock.fn(async () => [parent, child])
446
447
  })
447
448
 
448
- const result = await ContentModule.prototype.getDescendants.call(inst, parent)
449
+ const result = await getDescendants(q => inst.find(q), parent)
449
450
 
450
451
  assert.equal(result.length, 1)
451
452
  assert.equal(result[0]._id, 'child1')
@@ -462,7 +463,7 @@ describe('ContentModule', () => {
462
463
  ])
463
464
  })
464
465
 
465
- const result = await ContentModule.prototype.getDescendants.call(inst, {
466
+ const result = await getDescendants(q => inst.find(q), {
466
467
  _id: 'root',
467
468
  _courseId: 'x',
468
469
  _type: 'page'
@@ -1607,7 +1608,7 @@ describe('ContentModule', () => {
1607
1608
  ])
1608
1609
  })
1609
1610
 
1610
- const result = await ContentModule.prototype.getDescendants.call(inst, {
1611
+ const result = await getDescendants(q => inst.find(q), {
1611
1612
  _id: 'root',
1612
1613
  _courseId: 'c1',
1613
1614
  _type: 'course'
@@ -1682,17 +1683,10 @@ describe('ContentModule', () => {
1682
1683
  })
1683
1684
 
1684
1685
  // -----------------------------------------------------------------------
1685
- // TODO: Potential bugs
1686
+ // Bug fixes
1686
1687
  // -----------------------------------------------------------------------
1687
- describe('potential bugs', () => {
1688
- it('TODO: clone of course crashes if no config exists for the course', async () => {
1689
- // BUG: In clone() line 263, when cloning a course:
1690
- // const [config] = await this.find({ _type: 'config', _courseId: originalDoc._courseId })
1691
- // await this.clone(userId, config._id, ...)
1692
- // If no config exists for the course, config is undefined, and
1693
- // config._id throws TypeError: Cannot read properties of undefined
1694
- //
1695
- // The code should check whether config exists before trying to clone it.
1688
+ describe('bug fixes', () => {
1689
+ it('should handle clone of course when no config exists', async () => {
1696
1690
  let findCallCount = 0
1697
1691
  const inst = createInstance({
1698
1692
  find: mock.fn(async () => {
@@ -1706,14 +1700,8 @@ describe('ContentModule', () => {
1706
1700
  postCloneHook: createMockHook()
1707
1701
  })
1708
1702
 
1709
- // This SHOULD work but crashes due to missing null-check on config
1710
- await assert.rejects(
1711
- () => ContentModule.prototype.clone.call(inst, 'user1', 'c1', undefined),
1712
- (err) => {
1713
- assert.match(err.message, /Cannot read properties of undefined/)
1714
- return true
1715
- }
1716
- )
1703
+ const result = await ContentModule.prototype.clone.call(inst, 'user1', 'c1', undefined)
1704
+ assert.strictEqual(result._id, 'new-c1')
1717
1705
  })
1718
1706
  })
1719
1707
  })
@@ -0,0 +1,117 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { getDescendants } from '../lib/utils/getDescendants.js'
4
+
5
+ describe('getDescendants()', () => {
6
+ const makeId = (n) => ({ toString: () => `id${n}` })
7
+
8
+ const courseItems = [
9
+ { _id: makeId(1), _courseId: 'course1', _type: 'course' },
10
+ { _id: makeId(2), _courseId: 'course1', _parentId: makeId(1), _type: 'page' },
11
+ { _id: makeId(3), _courseId: 'course1', _parentId: makeId(2), _type: 'article' },
12
+ { _id: makeId(4), _courseId: 'course1', _parentId: makeId(3), _type: 'block' },
13
+ { _id: makeId(5), _courseId: 'course1', _parentId: makeId(4), _type: 'component' },
14
+ { _id: makeId(6), _courseId: 'course1', _type: 'config' }
15
+ ]
16
+
17
+ const findFn = async (query) => {
18
+ return courseItems.filter(c => {
19
+ return Object.entries(query).every(([k, v]) => {
20
+ const itemVal = c[k]
21
+ if (itemVal && typeof itemVal.toString === 'function' && typeof v === 'string') {
22
+ return itemVal.toString() === v
23
+ }
24
+ return itemVal === v
25
+ })
26
+ })
27
+ }
28
+
29
+ it('should return all descendants of the course (including config)', async () => {
30
+ const root = courseItems[0]
31
+ const result = await getDescendants(findFn, root)
32
+ // Should include page, article, block, component, config = 5 items
33
+ assert.equal(result.length, 5)
34
+ assert.ok(result.some(r => r._type === 'page'))
35
+ assert.ok(result.some(r => r._type === 'article'))
36
+ assert.ok(result.some(r => r._type === 'block'))
37
+ assert.ok(result.some(r => r._type === 'component'))
38
+ assert.ok(result.some(r => r._type === 'config'))
39
+ })
40
+
41
+ it('should return descendants of a page', async () => {
42
+ const root = courseItems[1] // page
43
+ const result = await getDescendants(findFn, root)
44
+ // article, block, component = 3
45
+ assert.equal(result.length, 3)
46
+ assert.ok(result.some(r => r._type === 'article'))
47
+ assert.ok(result.some(r => r._type === 'block'))
48
+ assert.ok(result.some(r => r._type === 'component'))
49
+ })
50
+
51
+ it('should return descendants of an article', async () => {
52
+ const root = courseItems[2] // article
53
+ const result = await getDescendants(findFn, root)
54
+ // block, component = 2
55
+ assert.equal(result.length, 2)
56
+ assert.ok(result.some(r => r._type === 'block'))
57
+ assert.ok(result.some(r => r._type === 'component'))
58
+ })
59
+
60
+ it('should return empty array for a leaf node', async () => {
61
+ const root = courseItems[4] // component (leaf)
62
+ const result = await getDescendants(findFn, root)
63
+ assert.equal(result.length, 0)
64
+ })
65
+
66
+ it('should not include config for non-course roots', async () => {
67
+ const root = courseItems[1] // page
68
+ const result = await getDescendants(findFn, root)
69
+ assert.ok(!result.some(r => r._type === 'config'))
70
+ })
71
+
72
+ it('should include config for course root', async () => {
73
+ const root = courseItems[0]
74
+ const result = await getDescendants(findFn, root)
75
+ assert.ok(result.some(r => r._type === 'config'))
76
+ })
77
+
78
+ it('should handle course with no config', async () => {
79
+ const itemsNoConfig = courseItems.filter(c => c._type !== 'config')
80
+ const findNoConfig = async (query) => {
81
+ return itemsNoConfig.filter(c => {
82
+ return Object.entries(query).every(([k, v]) => {
83
+ const itemVal = c[k]
84
+ if (itemVal && typeof itemVal.toString === 'function' && typeof v === 'string') {
85
+ return itemVal.toString() === v
86
+ }
87
+ return itemVal === v
88
+ })
89
+ })
90
+ }
91
+ const root = itemsNoConfig[0] // course
92
+ const result = await getDescendants(findNoConfig, root)
93
+ // page, article, block, component = 4 (no config)
94
+ assert.equal(result.length, 4)
95
+ assert.ok(!result.some(r => r._type === 'config'))
96
+ })
97
+
98
+ it('should handle empty course (no children)', async () => {
99
+ const emptyItems = [{ _id: makeId(10), _courseId: 'course10', _type: 'course' }]
100
+ const emptyFind = async () => emptyItems
101
+ const root = emptyItems[0]
102
+ const result = await getDescendants(emptyFind, root)
103
+ assert.equal(result.length, 0)
104
+ })
105
+
106
+ it('should use findFn with correct query', async () => {
107
+ const queries = []
108
+ const trackingFind = async (query) => {
109
+ queries.push(query)
110
+ return []
111
+ }
112
+ const root = { _id: makeId(99), _courseId: 'courseX', _type: 'page' }
113
+ await getDescendants(trackingFind, root)
114
+ assert.equal(queries.length, 1)
115
+ assert.deepEqual(queries[0], { _courseId: 'courseX' })
116
+ })
117
+ })