adapt-authoring-content 2.1.0 → 2.1.2
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/errors/errors.json +8 -0
- package/lib/ContentModule.js +24 -3
- package/package.json +1 -1
- package/tests/ContentModule.spec.js +138 -1
package/errors/errors.json
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
"description": "Specified item is not a valid content item Invalid parent itemparent",
|
|
7
7
|
"statusCode": 500
|
|
8
8
|
},
|
|
9
|
+
"DUPL_FRIENDLY_ID": {
|
|
10
|
+
"data": {
|
|
11
|
+
"_friendlyId": "The duplicate friendly ID",
|
|
12
|
+
"_courseId": "The course ID"
|
|
13
|
+
},
|
|
14
|
+
"description": "A content item with this _friendlyId already exists in this course",
|
|
15
|
+
"statusCode": 409
|
|
16
|
+
},
|
|
9
17
|
"UNKNOWN_SCHEMA_NAME": {
|
|
10
18
|
"data": {
|
|
11
19
|
"_id": "The database _id",
|
package/lib/ContentModule.js
CHANGED
|
@@ -50,6 +50,10 @@ class ContentModule extends AbstractApiModule {
|
|
|
50
50
|
await this.registerConfigSchemas()
|
|
51
51
|
|
|
52
52
|
await mongodb.setIndex(this.collectionName, { _courseId: 1, _parentId: 1, _type: 1 })
|
|
53
|
+
await mongodb.setIndex(this.collectionName, { _courseId: 1, _friendlyId: 1 }, {
|
|
54
|
+
unique: true,
|
|
55
|
+
partialFilterExpression: { _friendlyId: { $type: 'string' } }
|
|
56
|
+
})
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/** @override */
|
|
@@ -109,7 +113,15 @@ class ContentModule extends AbstractApiModule {
|
|
|
109
113
|
|
|
110
114
|
/** @override */
|
|
111
115
|
async insert (data, options = {}, mongoOptions = {}) {
|
|
112
|
-
|
|
116
|
+
let doc
|
|
117
|
+
try {
|
|
118
|
+
doc = await super.insert(data, options, mongoOptions)
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e.code === this.app.errors.MONGO_DUPL_INDEX?.code) {
|
|
121
|
+
throw this.app.errors.DUPL_FRIENDLY_ID.setData({ _friendlyId: data._friendlyId, _courseId: data._courseId })
|
|
122
|
+
}
|
|
123
|
+
throw e
|
|
124
|
+
}
|
|
113
125
|
|
|
114
126
|
if (doc._type === 'course') { // add the _courseId to a new course to make querying easier
|
|
115
127
|
return this.update({ _id: doc._id }, { _courseId: doc._id.toString() })
|
|
@@ -123,7 +135,15 @@ class ContentModule extends AbstractApiModule {
|
|
|
123
135
|
|
|
124
136
|
/** @override */
|
|
125
137
|
async update (query, data, options, mongoOptions) {
|
|
126
|
-
|
|
138
|
+
let doc
|
|
139
|
+
try {
|
|
140
|
+
doc = await super.update(query, data, options, mongoOptions)
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (e.code === this.app.errors.MONGO_DUPL_INDEX?.code) {
|
|
143
|
+
throw this.app.errors.DUPL_FRIENDLY_ID.setData({ _friendlyId: data._friendlyId, _courseId: data._courseId })
|
|
144
|
+
}
|
|
145
|
+
throw e
|
|
146
|
+
}
|
|
127
147
|
await Promise.all([
|
|
128
148
|
this.updateSortOrder(doc, data),
|
|
129
149
|
this.updateEnabledPlugins(doc, data._enabledPlugins ? { forceUpdate: true } : {})
|
|
@@ -232,6 +252,7 @@ class ContentModule extends AbstractApiModule {
|
|
|
232
252
|
...originalDoc,
|
|
233
253
|
_id: undefined,
|
|
234
254
|
_trackingId: undefined,
|
|
255
|
+
_friendlyId: originalDoc._type !== 'course' ? undefined : originalDoc._friendlyId,
|
|
235
256
|
_courseId: parent?._type === 'course' ? parent?._id : parent?._courseId,
|
|
236
257
|
_parentId,
|
|
237
258
|
createdBy: userId,
|
|
@@ -275,7 +296,7 @@ class ContentModule extends AbstractApiModule {
|
|
|
275
296
|
}
|
|
276
297
|
return Promise.all(siblings.map(async (s, i) => {
|
|
277
298
|
const _sortOrder = i + 1
|
|
278
|
-
if (s._sortOrder !== _sortOrder) super.update({ _id: s._id }, { _sortOrder })
|
|
299
|
+
if (s._sortOrder !== _sortOrder) return super.update({ _id: s._id }, { _sortOrder })
|
|
279
300
|
}))
|
|
280
301
|
}
|
|
281
302
|
|
package/package.json
CHANGED
|
@@ -30,7 +30,9 @@ function createMockApp () {
|
|
|
30
30
|
errors: {
|
|
31
31
|
NOT_FOUND: createMockError('NOT_FOUND'),
|
|
32
32
|
INVALID_PARENT: createMockError('INVALID_PARENT'),
|
|
33
|
-
UNKNOWN_SCHEMA_NAME: createMockError('UNKNOWN_SCHEMA_NAME')
|
|
33
|
+
UNKNOWN_SCHEMA_NAME: createMockError('UNKNOWN_SCHEMA_NAME'),
|
|
34
|
+
MONGO_DUPL_INDEX: createMockError('MONGO_DUPL_INDEX'),
|
|
35
|
+
DUPL_FRIENDLY_ID: createMockError('DUPL_FRIENDLY_ID')
|
|
34
36
|
},
|
|
35
37
|
waitForModule: mock.fn(async () => ({})),
|
|
36
38
|
config: { get: mock.fn(() => 10) }
|
|
@@ -1686,6 +1688,141 @@ describe('ContentModule', () => {
|
|
|
1686
1688
|
// Bug fixes
|
|
1687
1689
|
// -----------------------------------------------------------------------
|
|
1688
1690
|
describe('bug fixes', () => {
|
|
1691
|
+
it('insert should catch MONGO_DUPL_INDEX and throw DUPL_FRIENDLY_ID', async () => {
|
|
1692
|
+
const duplError = createMockError('MONGO_DUPL_INDEX')
|
|
1693
|
+
const inst = createInstance()
|
|
1694
|
+
const superInsert = mock.fn(async () => { throw duplError })
|
|
1695
|
+
|
|
1696
|
+
const origProto = Object.getPrototypeOf(ContentModule.prototype)
|
|
1697
|
+
const origInsert = origProto.insert
|
|
1698
|
+
origProto.insert = superInsert
|
|
1699
|
+
try {
|
|
1700
|
+
await assert.rejects(
|
|
1701
|
+
() => ContentModule.prototype.insert.call(inst, { _friendlyId: 'fid-1', _courseId: 'c1', _type: 'article' }),
|
|
1702
|
+
(err) => {
|
|
1703
|
+
assert.equal(err.code, 'DUPL_FRIENDLY_ID')
|
|
1704
|
+
assert.equal(err.data._friendlyId, 'fid-1')
|
|
1705
|
+
assert.equal(err.data._courseId, 'c1')
|
|
1706
|
+
return true
|
|
1707
|
+
}
|
|
1708
|
+
)
|
|
1709
|
+
} finally {
|
|
1710
|
+
origProto.insert = origInsert
|
|
1711
|
+
}
|
|
1712
|
+
})
|
|
1713
|
+
|
|
1714
|
+
it('insert should re-throw non-duplicate errors unchanged', async () => {
|
|
1715
|
+
const otherError = new Error('SOME_OTHER_ERROR')
|
|
1716
|
+
otherError.code = 'SOME_OTHER_ERROR'
|
|
1717
|
+
const inst = createInstance()
|
|
1718
|
+
|
|
1719
|
+
const origProto = Object.getPrototypeOf(ContentModule.prototype)
|
|
1720
|
+
const origInsert = origProto.insert
|
|
1721
|
+
origProto.insert = mock.fn(async () => { throw otherError })
|
|
1722
|
+
try {
|
|
1723
|
+
await assert.rejects(
|
|
1724
|
+
() => ContentModule.prototype.insert.call(inst, { _type: 'article' }),
|
|
1725
|
+
(err) => {
|
|
1726
|
+
assert.equal(err.code, 'SOME_OTHER_ERROR')
|
|
1727
|
+
return true
|
|
1728
|
+
}
|
|
1729
|
+
)
|
|
1730
|
+
} finally {
|
|
1731
|
+
origProto.insert = origInsert
|
|
1732
|
+
}
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
it('update should catch MONGO_DUPL_INDEX and throw DUPL_FRIENDLY_ID', async () => {
|
|
1736
|
+
const duplError = createMockError('MONGO_DUPL_INDEX')
|
|
1737
|
+
const inst = createInstance()
|
|
1738
|
+
|
|
1739
|
+
const origProto = Object.getPrototypeOf(ContentModule.prototype)
|
|
1740
|
+
const origUpdate = origProto.update
|
|
1741
|
+
origProto.update = mock.fn(async () => { throw duplError })
|
|
1742
|
+
try {
|
|
1743
|
+
await assert.rejects(
|
|
1744
|
+
() => ContentModule.prototype.update.call(inst, { _id: 'x' }, { _friendlyId: 'fid-1', _courseId: 'c1' }),
|
|
1745
|
+
(err) => {
|
|
1746
|
+
assert.equal(err.code, 'DUPL_FRIENDLY_ID')
|
|
1747
|
+
assert.equal(err.data._friendlyId, 'fid-1')
|
|
1748
|
+
assert.equal(err.data._courseId, 'c1')
|
|
1749
|
+
return true
|
|
1750
|
+
}
|
|
1751
|
+
)
|
|
1752
|
+
} finally {
|
|
1753
|
+
origProto.update = origUpdate
|
|
1754
|
+
}
|
|
1755
|
+
})
|
|
1756
|
+
|
|
1757
|
+
it('update should re-throw non-duplicate errors unchanged', async () => {
|
|
1758
|
+
const otherError = new Error('SOME_OTHER_ERROR')
|
|
1759
|
+
otherError.code = 'SOME_OTHER_ERROR'
|
|
1760
|
+
const inst = createInstance()
|
|
1761
|
+
|
|
1762
|
+
const origProto = Object.getPrototypeOf(ContentModule.prototype)
|
|
1763
|
+
const origUpdate = origProto.update
|
|
1764
|
+
origProto.update = mock.fn(async () => { throw otherError })
|
|
1765
|
+
try {
|
|
1766
|
+
await assert.rejects(
|
|
1767
|
+
() => ContentModule.prototype.update.call(inst, { _id: 'x' }, { title: 'Updated' }),
|
|
1768
|
+
(err) => {
|
|
1769
|
+
assert.equal(err.code, 'SOME_OTHER_ERROR')
|
|
1770
|
+
return true
|
|
1771
|
+
}
|
|
1772
|
+
)
|
|
1773
|
+
} finally {
|
|
1774
|
+
origProto.update = origUpdate
|
|
1775
|
+
}
|
|
1776
|
+
})
|
|
1777
|
+
|
|
1778
|
+
it('clone should clear _friendlyId for non-course types', async () => {
|
|
1779
|
+
let findCallCount = 0
|
|
1780
|
+
const insertFn = mock.fn(async (data, opts) => ({
|
|
1781
|
+
...data,
|
|
1782
|
+
_id: 'new-id'
|
|
1783
|
+
}))
|
|
1784
|
+
const inst = createInstance({
|
|
1785
|
+
find: mock.fn(async () => {
|
|
1786
|
+
findCallCount++
|
|
1787
|
+
if (findCallCount === 1) return [{ _id: 'orig', _type: 'article', _courseId: 'c1', _friendlyId: 'art-1' }]
|
|
1788
|
+
if (findCallCount === 2) return [{ _id: 'p', _type: 'page', _courseId: 'c1' }]
|
|
1789
|
+
return []
|
|
1790
|
+
}),
|
|
1791
|
+
insert: insertFn,
|
|
1792
|
+
preCloneHook: createMockHook(),
|
|
1793
|
+
postCloneHook: createMockHook()
|
|
1794
|
+
})
|
|
1795
|
+
|
|
1796
|
+
await ContentModule.prototype.clone.call(inst, 'user1', 'orig', 'p')
|
|
1797
|
+
|
|
1798
|
+
const payload = insertFn.mock.calls[0].arguments[0]
|
|
1799
|
+
assert.equal(payload._friendlyId, undefined)
|
|
1800
|
+
})
|
|
1801
|
+
|
|
1802
|
+
it('clone should preserve _friendlyId for course types', async () => {
|
|
1803
|
+
let findCallCount = 0
|
|
1804
|
+
const insertFn = mock.fn(async (data, opts) => ({
|
|
1805
|
+
...data,
|
|
1806
|
+
_id: 'new-course-id'
|
|
1807
|
+
}))
|
|
1808
|
+
const inst = createInstance({
|
|
1809
|
+
find: mock.fn(async () => {
|
|
1810
|
+
findCallCount++
|
|
1811
|
+
if (findCallCount === 1) return [{ _id: 'c1', _type: 'course', _courseId: 'c1', _friendlyId: 'course-1' }]
|
|
1812
|
+
return []
|
|
1813
|
+
}),
|
|
1814
|
+
insert: insertFn,
|
|
1815
|
+
update: mock.fn(async () => ({})),
|
|
1816
|
+
preCloneHook: createMockHook(),
|
|
1817
|
+
postCloneHook: createMockHook()
|
|
1818
|
+
})
|
|
1819
|
+
|
|
1820
|
+
await ContentModule.prototype.clone.call(inst, 'user1', 'c1', undefined)
|
|
1821
|
+
|
|
1822
|
+
const payload = insertFn.mock.calls[0].arguments[0]
|
|
1823
|
+
assert.equal(payload._friendlyId, 'course-1')
|
|
1824
|
+
})
|
|
1825
|
+
|
|
1689
1826
|
it('should handle clone of course when no config exists', async () => {
|
|
1690
1827
|
let findCallCount = 0
|
|
1691
1828
|
const inst = createInstance({
|