musora-content-services 2.102.3 → 2.103.6
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/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/src/index.d.ts +4 -2
- package/src/index.js +4 -2
- package/src/services/awards/award-callbacks.js +6 -4
- package/src/services/awards/types.d.ts +1 -0
- package/src/services/content-org/learning-paths.ts +2 -2
- package/src/services/contentProgress.js +31 -24
- package/src/services/sync/repositories/content-progress.ts +9 -25
- package/src/services/user/sessions.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.103.6](https://github.com/railroadmedia/musora-content-services/compare/v2.103.5...v2.103.6) (2025-12-12)
|
|
6
|
+
|
|
7
|
+
### [2.103.5](https://github.com/railroadmedia/musora-content-services/compare/v2.103.4...v2.103.5) (2025-12-12)
|
|
8
|
+
|
|
9
|
+
### [2.103.4](https://github.com/railroadmedia/musora-content-services/compare/v2.103.3...v2.103.4) (2025-12-12)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* mobile login broken ([785e0fc](https://github.com/railroadmedia/musora-content-services/commit/785e0fc1fc1ce58840d0e85874f68b9c77342290))
|
|
15
|
+
|
|
16
|
+
### [2.103.3](https://github.com/railroadmedia/musora-content-services/compare/v2.103.2...v2.103.3) (2025-12-11)
|
|
17
|
+
|
|
18
|
+
### [2.103.2](https://github.com/railroadmedia/musora-content-services/compare/v2.103.1...v2.103.2) (2025-12-11)
|
|
19
|
+
|
|
20
|
+
### [2.103.1](https://github.com/railroadmedia/musora-content-services/compare/v2.103.0...v2.103.1) (2025-12-11)
|
|
21
|
+
|
|
22
|
+
## [2.103.0](https://github.com/railroadmedia/musora-content-services/compare/v2.102.3...v2.103.0) (2025-12-11)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* add isCompleted and field name updates in award callback([#648](https://github.com/railroadmedia/musora-content-services/issues/648)) ([c250d61](https://github.com/railroadmedia/musora-content-services/commit/c250d61b90bc29504827e8fa368a78b34a3f0c08))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* lp lesson upserting ([#645](https://github.com/railroadmedia/musora-content-services/issues/645)) ([1a3094d](https://github.com/railroadmedia/musora-content-services/commit/1a3094d569bb868d62844755b876cee8bc63a365))
|
|
33
|
+
|
|
5
34
|
### [2.102.3](https://github.com/railroadmedia/musora-content-services/compare/v2.102.2...v2.102.3) (2025-12-11)
|
|
6
35
|
|
|
7
36
|
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -104,9 +104,9 @@ import {
|
|
|
104
104
|
|
|
105
105
|
import {
|
|
106
106
|
contentStatusCompleted,
|
|
107
|
+
contentStatusCompletedMany,
|
|
107
108
|
contentStatusReset,
|
|
108
109
|
contentStatusStarted,
|
|
109
|
-
contentsStatusCompleted,
|
|
110
110
|
getAllCompleted,
|
|
111
111
|
getAllCompletedByIds,
|
|
112
112
|
getAllStarted,
|
|
@@ -396,6 +396,7 @@ import {
|
|
|
396
396
|
|
|
397
397
|
import {
|
|
398
398
|
login,
|
|
399
|
+
loginWithAuthKey,
|
|
399
400
|
logout
|
|
400
401
|
} from './services/user/sessions.js';
|
|
401
402
|
|
|
@@ -447,9 +448,9 @@ declare module 'musora-content-services' {
|
|
|
447
448
|
completeMethodIntroVideo,
|
|
448
449
|
confirmEmailChange,
|
|
449
450
|
contentStatusCompleted,
|
|
451
|
+
contentStatusCompletedMany,
|
|
450
452
|
contentStatusReset,
|
|
451
453
|
contentStatusStarted,
|
|
452
|
-
contentsStatusCompleted,
|
|
453
454
|
convertToTimeZone,
|
|
454
455
|
createComment,
|
|
455
456
|
createForumCategory,
|
|
@@ -646,6 +647,7 @@ declare module 'musora-content-services' {
|
|
|
646
647
|
likePost,
|
|
647
648
|
lockThread,
|
|
648
649
|
login,
|
|
650
|
+
loginWithAuthKey,
|
|
649
651
|
logout,
|
|
650
652
|
mapContentToParent,
|
|
651
653
|
markAllNotificationsAsRead,
|
package/src/index.js
CHANGED
|
@@ -108,9 +108,9 @@ import {
|
|
|
108
108
|
|
|
109
109
|
import {
|
|
110
110
|
contentStatusCompleted,
|
|
111
|
+
contentStatusCompletedMany,
|
|
111
112
|
contentStatusReset,
|
|
112
113
|
contentStatusStarted,
|
|
113
|
-
contentsStatusCompleted,
|
|
114
114
|
getAllCompleted,
|
|
115
115
|
getAllCompletedByIds,
|
|
116
116
|
getAllStarted,
|
|
@@ -400,6 +400,7 @@ import {
|
|
|
400
400
|
|
|
401
401
|
import {
|
|
402
402
|
login,
|
|
403
|
+
loginWithAuthKey,
|
|
403
404
|
logout
|
|
404
405
|
} from './services/user/sessions.js';
|
|
405
406
|
|
|
@@ -446,9 +447,9 @@ export {
|
|
|
446
447
|
completeMethodIntroVideo,
|
|
447
448
|
confirmEmailChange,
|
|
448
449
|
contentStatusCompleted,
|
|
450
|
+
contentStatusCompletedMany,
|
|
449
451
|
contentStatusReset,
|
|
450
452
|
contentStatusStarted,
|
|
451
|
-
contentsStatusCompleted,
|
|
452
453
|
convertToTimeZone,
|
|
453
454
|
createComment,
|
|
454
455
|
createForumCategory,
|
|
@@ -645,6 +646,7 @@ export {
|
|
|
645
646
|
likePost,
|
|
646
647
|
lockThread,
|
|
647
648
|
login,
|
|
649
|
+
loginWithAuthKey,
|
|
648
650
|
logout,
|
|
649
651
|
mapContentToParent,
|
|
650
652
|
markAllNotificationsAsRead,
|
|
@@ -22,6 +22,7 @@ let progressUpdateCallback = null
|
|
|
22
22
|
* - `name` - Display name of the award
|
|
23
23
|
* - `badge` - URL to badge image
|
|
24
24
|
* - `completed_at` - ISO timestamp
|
|
25
|
+
* - `isCompleted` - Boolean indicating the award is completed (always true for granted awards)
|
|
25
26
|
* - `completion_data.message` - Pre-generated congratulations message
|
|
26
27
|
* - `completion_data.practice_minutes` - Total practice time
|
|
27
28
|
* - `completion_data.days_user_practiced` - Days spent practicing
|
|
@@ -72,13 +73,14 @@ export function registerAwardCallback(callback) {
|
|
|
72
73
|
name: definition.name,
|
|
73
74
|
badge: definition.badge,
|
|
74
75
|
completed_at: completionData.completed_at,
|
|
75
|
-
|
|
76
|
+
isCompleted: true,
|
|
77
|
+
completionData: {
|
|
76
78
|
completed_at: completionData.completed_at,
|
|
77
79
|
days_user_practiced: completionData.days_user_practiced,
|
|
78
80
|
message: popupMessage,
|
|
79
81
|
practice_minutes: completionData.practice_minutes,
|
|
80
|
-
content_title: completionData.content_title
|
|
81
|
-
}
|
|
82
|
+
content_title: completionData.content_title,
|
|
83
|
+
},
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
callback(award)
|
|
@@ -148,7 +150,7 @@ export function registerProgressCallback(callback) {
|
|
|
148
150
|
progressUpdateCallback = (payload) => {
|
|
149
151
|
callback({
|
|
150
152
|
awardId: payload.awardId,
|
|
151
|
-
progressPercentage: payload.progressPercentage
|
|
153
|
+
progressPercentage: payload.progressPercentage,
|
|
152
154
|
})
|
|
153
155
|
}
|
|
154
156
|
|
|
@@ -7,7 +7,7 @@ import { fetchByRailContentId, fetchByRailContentIds, fetchMethodV2Structure } f
|
|
|
7
7
|
import { addContextToLearningPaths } from '../contentAggregator.js'
|
|
8
8
|
import {
|
|
9
9
|
contentStatusCompleted,
|
|
10
|
-
|
|
10
|
+
contentStatusCompletedMany,
|
|
11
11
|
contentStatusReset,
|
|
12
12
|
getAllCompletedByIds,
|
|
13
13
|
getProgressState,
|
|
@@ -367,7 +367,7 @@ export async function completeLearningPathIntroVideo(
|
|
|
367
367
|
response.learning_path_reset_response = await resetIfPossible(learningPathId, collection)
|
|
368
368
|
|
|
369
369
|
} else {
|
|
370
|
-
response.lesson_import_response = await
|
|
370
|
+
response.lesson_import_response = await contentStatusCompletedMany(lessonsToImport, collection)
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
return response
|
|
@@ -270,7 +270,7 @@ export async function getProgressDataByIdsAndCollections(tuples) {
|
|
|
270
270
|
collection: {},
|
|
271
271
|
}]))
|
|
272
272
|
|
|
273
|
-
await db.contentProgress.
|
|
273
|
+
await db.contentProgress.getSomeProgressByContentIdsAndCollections(tuples).then(r => {
|
|
274
274
|
r.data.forEach(p => {
|
|
275
275
|
progress[p.content_id] = {
|
|
276
276
|
last_update: p.updated_at,
|
|
@@ -307,7 +307,7 @@ async function getByIdsAndCollections(tuples, dataKey, defaultValue) {
|
|
|
307
307
|
tuples = tuples.map(t => ({contentId: normalizeContentId(t.contentId), collection: normalizeCollection(t.collection)}))
|
|
308
308
|
const progress = Object.fromEntries(tuples.map(tuple => [tuple.contentId, defaultValue]))
|
|
309
309
|
|
|
310
|
-
await db.contentProgress.
|
|
310
|
+
await db.contentProgress.getSomeProgressByContentIdsAndCollections(tuples).then(r => {
|
|
311
311
|
r.data.forEach(p => {
|
|
312
312
|
progress[p.content_id] = p[dataKey] ?? defaultValue
|
|
313
313
|
})
|
|
@@ -441,8 +441,8 @@ export async function contentStatusCompleted(contentId, collection = null) {
|
|
|
441
441
|
)
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
export async function
|
|
445
|
-
return
|
|
444
|
+
export async function contentStatusCompletedMany(contentIds, collection = null) {
|
|
445
|
+
return setStartedOrCompletedStatusMany(
|
|
446
446
|
normalizeContentIds(contentIds),
|
|
447
447
|
normalizeCollection(collection),
|
|
448
448
|
true
|
|
@@ -475,7 +475,8 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
|
|
|
475
475
|
const hierarchy = await getHierarchy(contentId, collection)
|
|
476
476
|
|
|
477
477
|
const bubbledProgresses = await bubbleProgress(hierarchy, contentId, collection)
|
|
478
|
-
|
|
478
|
+
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
479
|
+
await db.contentProgress.recordProgressMany(bubbledProgresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
|
|
479
480
|
|
|
480
481
|
if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
481
482
|
let exportIds = bubbledProgresses
|
|
@@ -499,19 +500,20 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
|
|
|
499
500
|
|
|
500
501
|
const hierarchy = await getHierarchy(contentId, collection)
|
|
501
502
|
|
|
502
|
-
let
|
|
503
|
+
let progresses = {
|
|
503
504
|
...trickleProgress(hierarchy, contentId, collection, progress),
|
|
504
505
|
...await bubbleProgress(hierarchy, contentId, collection)
|
|
505
506
|
}
|
|
506
|
-
|
|
507
|
+
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
508
|
+
await db.contentProgress.recordProgressMany(progresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
|
|
507
509
|
|
|
508
510
|
if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
509
|
-
let
|
|
510
|
-
|
|
511
|
-
await duplicateLearningPathProgressToExternalContents(
|
|
511
|
+
let exportProgresses = progresses
|
|
512
|
+
exportProgresses[contentId] = progress
|
|
513
|
+
await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy)
|
|
512
514
|
}
|
|
513
515
|
|
|
514
|
-
for (const [id, progress] of Object.entries(
|
|
516
|
+
for (const [id, progress] of Object.entries(progresses)) {
|
|
515
517
|
if (progress === 100) {
|
|
516
518
|
emitContentCompleted(Number(id), collection)
|
|
517
519
|
}
|
|
@@ -520,6 +522,8 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
|
|
|
520
522
|
return response
|
|
521
523
|
}
|
|
522
524
|
|
|
525
|
+
// we cannot simply pass LP id with self collection, because we do not have a-la-carte LP's set up yet,
|
|
526
|
+
// and we need each lesson to bubble to its parent outside of LP
|
|
523
527
|
async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy) {
|
|
524
528
|
// filter out LPs. we dont want to duplicate to LP's while we dont have a-la-cart LP's set up.
|
|
525
529
|
let filteredIds = Object.fromEntries(
|
|
@@ -552,29 +556,31 @@ async function getHierarchy(contentId, collection) {
|
|
|
552
556
|
}
|
|
553
557
|
}
|
|
554
558
|
|
|
555
|
-
async function
|
|
559
|
+
async function setStartedOrCompletedStatusMany(contentIds, collection, isCompleted) {
|
|
556
560
|
const progress = isCompleted ? 100 : 0
|
|
557
|
-
const
|
|
561
|
+
const contents = Object.fromEntries(contentIds.map((id) => [id, progress]))
|
|
562
|
+
const response = await db.contentProgress.recordProgressMany(contents, collection, true)
|
|
558
563
|
|
|
559
564
|
// we assume this is used only for contents within the same hierarchy
|
|
560
565
|
const hierarchy = await getHierarchy(collection.id, collection)
|
|
561
566
|
|
|
562
|
-
let
|
|
567
|
+
let progresses = {}
|
|
563
568
|
for (const contentId of contentIds) {
|
|
564
|
-
|
|
565
|
-
...
|
|
569
|
+
progresses = {
|
|
570
|
+
...progresses,
|
|
566
571
|
...trickleProgress(hierarchy, contentId, collection, progress),
|
|
567
572
|
...(await bubbleProgress(hierarchy, contentId, collection)),
|
|
568
573
|
}
|
|
569
574
|
}
|
|
570
|
-
|
|
575
|
+
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
576
|
+
await db.contentProgress.recordProgressMany(progresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
|
|
571
577
|
|
|
572
578
|
if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
573
|
-
let
|
|
579
|
+
let exportProgresses = progresses
|
|
574
580
|
for (const contentId of contentIds){
|
|
575
|
-
|
|
581
|
+
exportProgresses[contentId] = progress
|
|
576
582
|
}
|
|
577
|
-
await duplicateLearningPathProgressToExternalContents(
|
|
583
|
+
await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy)
|
|
578
584
|
}
|
|
579
585
|
|
|
580
586
|
return response
|
|
@@ -585,15 +591,16 @@ async function resetStatus(contentId, collection = null) {
|
|
|
585
591
|
const response = await db.contentProgress.eraseProgress(contentId, collection)
|
|
586
592
|
const hierarchy = await getHierarchy(contentId, collection)
|
|
587
593
|
|
|
588
|
-
let
|
|
594
|
+
let progresses = {
|
|
589
595
|
...trickleProgress(hierarchy, contentId, collection, progress),
|
|
590
596
|
...await bubbleProgress(hierarchy, contentId, collection)
|
|
591
597
|
}
|
|
592
|
-
|
|
598
|
+
// BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
|
|
599
|
+
await db.contentProgress.recordProgressMany(progresses, collection, collection?.type !== COLLECTION_TYPE.LEARNING_PATH)
|
|
593
600
|
|
|
594
601
|
if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
595
|
-
|
|
596
|
-
await duplicateLearningPathProgressToExternalContents(
|
|
602
|
+
progresses[contentId] = progress
|
|
603
|
+
await duplicateLearningPathProgressToExternalContents(progresses, collection, hierarchy)
|
|
597
604
|
}
|
|
598
605
|
|
|
599
606
|
return response
|
|
@@ -122,7 +122,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
122
122
|
return await this.queryAll(...clauses)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async
|
|
125
|
+
async getSomeProgressByContentIdsAndCollections(tuples: ContentIdCollectionTuple[]) {
|
|
126
126
|
const clauses = []
|
|
127
127
|
|
|
128
128
|
clauses.push(...tuples.map(tuple => Q.and(...tupleClauses(tuple))))
|
|
@@ -179,32 +179,12 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
179
179
|
return result
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
recordProgressMany(
|
|
183
|
+
contentProgresses: Record<string, number>, // Accept plain object
|
|
184
184
|
collection: CollectionParameter | null,
|
|
185
|
-
|
|
185
|
+
tentative: boolean
|
|
186
186
|
) {
|
|
187
|
-
return this.upsertSome(
|
|
188
|
-
Object.fromEntries(
|
|
189
|
-
contentIds.map((contentId) => [
|
|
190
|
-
ProgressRepository.generateId(contentId, collection),
|
|
191
|
-
(r: ContentProgress) => {
|
|
192
|
-
r.content_id = contentId
|
|
193
|
-
r.collection_type = collection?.type ?? COLLECTION_TYPE.SELF
|
|
194
|
-
r.collection_id = collection?.id ?? COLLECTION_ID_SELF
|
|
195
|
-
|
|
196
|
-
r.state = progressPct === 100 ? STATE.COMPLETED : STATE.STARTED
|
|
197
|
-
r.progress_percent = progressPct
|
|
198
|
-
},
|
|
199
|
-
])
|
|
200
|
-
)
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
187
|
|
|
204
|
-
recordProgressesTentative(
|
|
205
|
-
contentProgresses: Record<string, number>, // Accept plain object
|
|
206
|
-
collection: CollectionParameter | null
|
|
207
|
-
) {
|
|
208
188
|
const data = Object.fromEntries(
|
|
209
189
|
Object.entries(contentProgresses).map(([contentId, progressPct]) => [
|
|
210
190
|
ProgressRepository.generateId(+contentId, collection),
|
|
@@ -218,7 +198,11 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
218
198
|
},
|
|
219
199
|
])
|
|
220
200
|
)
|
|
221
|
-
return
|
|
201
|
+
return tentative
|
|
202
|
+
? this.upsertSomeTentative(data)
|
|
203
|
+
: this.upsertSome(data)
|
|
204
|
+
|
|
205
|
+
//todo add event emitting for bulk updates?
|
|
222
206
|
}
|
|
223
207
|
|
|
224
208
|
eraseProgress(contentId: number, collection: CollectionParameter | null) {
|