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.
- package/lib/ContentModule.js +11 -32
- package/lib/utils/getDescendants.js +22 -0
- package/lib/utils.js +1 -0
- package/package.json +3 -3
- package/tests/ContentModule.spec.js +14 -26
- package/tests/utils-getDescendants.spec.js +117 -0
package/lib/ContentModule.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { AbstractApiModule
|
|
2
|
-
import {
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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": "
|
|
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": "^
|
|
15
|
-
"adapt-authoring-core": "^
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1686
|
+
// Bug fixes
|
|
1686
1687
|
// -----------------------------------------------------------------------
|
|
1687
|
-
describe('
|
|
1688
|
-
it('
|
|
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
|
-
|
|
1710
|
-
|
|
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
|
+
})
|