musora-content-services 2.102.2 → 2.103.3

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(rg:*)",
5
+ "Bash(npm run lint:*)"
6
+ ],
7
+ "deny": []
8
+ }
9
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
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.3](https://github.com/railroadmedia/musora-content-services/compare/v2.103.2...v2.103.3) (2025-12-11)
6
+
7
+ ### [2.103.2](https://github.com/railroadmedia/musora-content-services/compare/v2.103.1...v2.103.2) (2025-12-11)
8
+
9
+ ### [2.103.1](https://github.com/railroadmedia/musora-content-services/compare/v2.103.0...v2.103.1) (2025-12-11)
10
+
11
+ ## [2.103.0](https://github.com/railroadmedia/musora-content-services/compare/v2.102.3...v2.103.0) (2025-12-11)
12
+
13
+
14
+ ### Features
15
+
16
+ * 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))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * lp lesson upserting ([#645](https://github.com/railroadmedia/musora-content-services/issues/645)) ([1a3094d](https://github.com/railroadmedia/musora-content-services/commit/1a3094d569bb868d62844755b876cee8bc63a365))
22
+
23
+ ### [2.102.3](https://github.com/railroadmedia/musora-content-services/compare/v2.102.2...v2.102.3) (2025-12-11)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * **agi:** rollback total on fetchAGILessons ([a02be05](https://github.com/railroadmedia/musora-content-services/commit/a02be0592dd8b195da7971f13ea062674510c8bb))
29
+
5
30
  ### [2.102.2](https://github.com/railroadmedia/musora-content-services/compare/v2.102.1...v2.102.2) (2025-12-11)
6
31
 
7
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.102.2",
3
+ "version": "2.103.3",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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
- completion_data: {
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
 
@@ -66,6 +66,7 @@ export interface AwardCallbackPayload {
66
66
  name: string
67
67
  badge: string
68
68
  completed_at: string
69
+ isCompleted: boolean
69
70
  completion_data: AwardCompletionData
70
71
  }
71
72
 
@@ -149,7 +149,7 @@ export async function fetchGenreLessons(
149
149
  f.referencesIDWithFilter(f.combine(f.type('genre'), f.slug(slug)))
150
150
  )
151
151
 
152
- const q = query()
152
+ const data = query()
153
153
  .and(f.brand(brand))
154
154
  .and(f.searchMatch('title', searchTerm))
155
155
  .and(f.includedFields(includedFields))
@@ -160,5 +160,12 @@ export async function fetchGenreLessons(
160
160
  .select(getFieldsForContentType(contentType) as string)
161
161
  .build()
162
162
 
163
+ const total = query().and(restrictions).build()
164
+
165
+ const q = `{
166
+ "data": ${data},
167
+ "total": count(${total})
168
+ }`
169
+
163
170
  return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
164
171
  }
@@ -148,7 +148,7 @@ export async function fetchInstructorLessons(
148
148
  f.referencesIDWithFilter(f.combine(f.type('instructor'), f.slug(slug)))
149
149
  )
150
150
 
151
- const q = query()
151
+ const data = query()
152
152
  .and(f.brand(brand))
153
153
  .and(f.searchMatch('title', searchTerm))
154
154
  .and(f.includedFields(includedFields))
@@ -158,5 +158,12 @@ export async function fetchInstructorLessons(
158
158
  .select(getFieldsForContentType() as string)
159
159
  .build()
160
160
 
161
+ const total = query().and(restrictions).build()
162
+
163
+ const q = `{
164
+ "data": ${data},
165
+ "total": count(${total})
166
+ }`
167
+
161
168
  return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
162
169
  }
@@ -7,7 +7,7 @@ import { fetchByRailContentId, fetchByRailContentIds, fetchMethodV2Structure } f
7
7
  import { addContextToLearningPaths } from '../contentAggregator.js'
8
8
  import {
9
9
  contentStatusCompleted,
10
- contentsStatusCompleted,
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 contentsStatusCompleted(lessonsToImport, collection)
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.getSomeProgressByContentIdsAndCollection(tuples).then(r => {
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.getSomeProgressByContentIdsAndCollection(tuples).then(r => {
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 contentsStatusCompleted(contentIds, collection = null) {
445
- return setStartedOrCompletedStatuses(
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
- await db.contentProgress.recordProgressesTentative(bubbledProgresses, collection)
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 ids = {
503
+ let progresses = {
503
504
  ...trickleProgress(hierarchy, contentId, collection, progress),
504
505
  ...await bubbleProgress(hierarchy, contentId, collection)
505
506
  }
506
- await db.contentProgress.recordProgressesTentative(ids, collection)
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 exportIds = ids
510
- exportIds[contentId] = progress
511
- await duplicateLearningPathProgressToExternalContents(exportIds, collection, hierarchy)
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(ids)) {
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 setStartedOrCompletedStatuses(contentIds, collection, isCompleted) {
559
+ async function setStartedOrCompletedStatusMany(contentIds, collection, isCompleted) {
556
560
  const progress = isCompleted ? 100 : 0
557
- const response = await db.contentProgress.recordProgresses(contentIds, collection, progress)
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 ids = {}
567
+ let progresses = {}
563
568
  for (const contentId of contentIds) {
564
- ids = {
565
- ...ids,
569
+ progresses = {
570
+ ...progresses,
566
571
  ...trickleProgress(hierarchy, contentId, collection, progress),
567
572
  ...(await bubbleProgress(hierarchy, contentId, collection)),
568
573
  }
569
574
  }
570
- await db.contentProgress.recordProgressesTentative(ids, collection)
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 exportIds = ids
579
+ let exportProgresses = progresses
574
580
  for (const contentId of contentIds){
575
- exportIds[contentId] = progress
581
+ exportProgresses[contentId] = progress
576
582
  }
577
- await duplicateLearningPathProgressToExternalContents(exportIds, collection, hierarchy)
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 ids = {
594
+ let progresses = {
589
595
  ...trickleProgress(hierarchy, contentId, collection, progress),
590
596
  ...await bubbleProgress(hierarchy, contentId, collection)
591
597
  }
592
- await db.contentProgress.recordProgressesTentative(ids, collection)
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
- ids[contentId] = progress
596
- await duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy)
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 getSomeProgressByContentIdsAndCollection(tuples: ContentIdCollectionTuple[]) {
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
- recordProgresses(
183
- contentIds: number[],
182
+ recordProgressMany(
183
+ contentProgresses: Record<string, number>, // Accept plain object
184
184
  collection: CollectionParameter | null,
185
- progressPct: number
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 this.upsertSomeTentative(data)
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) {