adapt-authoring-content 1.3.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.
@@ -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,
@@ -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.1",
3
+ "version": "2.0.1",
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,14 +11,14 @@
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",
19
19
  "adapt-authoring-contentplugin": "^1.0.6",
20
20
  "adapt-authoring-jsonschema": "^1.2.0",
21
- "adapt-authoring-mongodb": "^1.1.3",
21
+ "adapt-authoring-mongodb": "^2.0.0",
22
22
  "adapt-authoring-tags": "^1.0.2"
23
23
  },
24
24
  "peerDependenciesMeta": {
@@ -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'
@@ -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
+ })