musora-content-services 2.77.1 → 2.78.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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(rg:*)"
5
+ ],
6
+ "deny": []
7
+ }
8
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
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.78.0](https://github.com/railroadmedia/musora-content-services/compare/v2.77.2...v2.78.0) (2025-11-15)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEH-1409:** complete intro video ([#566](https://github.com/railroadmedia/musora-content-services/issues/566)) ([fb2fa96](https://github.com/railroadmedia/musora-content-services/commit/fb2fa96f408948cf4c3501709dc2ec2c17f23163))
11
+
12
+ ### [2.77.2](https://github.com/railroadmedia/musora-content-services/compare/v2.77.1...v2.77.2) (2025-11-14)
13
+
5
14
  ### [2.77.1](https://github.com/railroadmedia/musora-content-services/compare/v2.77.0...v2.77.1) (2025-11-13)
6
15
 
7
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.77.1",
3
+ "version": "2.78.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -299,8 +299,7 @@ export const progressTypesMapping = {
299
299
  'play along': playAlongLessonTypes,
300
300
  'guided course': ['guided-course'],
301
301
  pack: ['pack', 'semester-pack'],
302
- method: ['method-card', 'method-intro'],
303
- 'learning path': ['learning-path'],
302
+ 'learning path': ['learning-path-v2'],
304
303
  'jam track': jamTrackLessonTypes,
305
304
  'course video': ['course-part'],
306
305
  }
@@ -346,7 +345,7 @@ export const recentTypes = {
346
345
  ...transcriptionsLessonTypes,
347
346
  ...playAlongLessonTypes,
348
347
  'guided-course',
349
- 'learning-path',
348
+ 'learning-path-v2',
350
349
  'live',
351
350
  'course',
352
351
  'pack',
@@ -460,28 +459,9 @@ export let contentTypeConfig = {
460
459
  )`,
461
460
  ],
462
461
  },
463
- method: {
464
- fields: [
465
- `"description": ${descriptionField}`,
466
- 'hide_from_recsys',
467
- '"image": thumbnail.asset->url',
468
- '"instructors":instructor[]->name',
469
- '"lesson_count": child_count',
470
- 'length_in_seconds',
471
- 'permission',
472
- 'popularity',
473
- 'published_on',
474
- 'railcontent_id',
475
- '"thumbnail_logo": logo_image_url.asset->url',
476
- 'title',
477
- 'total_xp',
478
- '"type": _type',
479
- 'xp',
480
- ],
481
- },
482
462
  'learning-path-v2': {
483
463
  fields: [
484
- `"intro_video": intro_video->{ ${getIntroVideoFields('learning-path').join(', ')} }`,
464
+ `"intro_video": intro_video->{ ${getIntroVideoFields('learning-path-v2').join(', ')} }`,
485
465
  'total_skills',
486
466
  `"resource": ${resourcesField}`,
487
467
  `"badge": *[
@@ -491,40 +471,6 @@ export let contentTypeConfig = {
491
471
  ],
492
472
  includeChildFields: true,
493
473
  },
494
- 'learning-path-course': {
495
- fields: [
496
- '"lesson_count": child_count',
497
- '"instructors": instructor[]->name',
498
- `"description": ${descriptionField}`,
499
- `"resource": ${resourcesField}`,
500
- 'xp',
501
- 'total_xp',
502
- `"lessons": child[]->{
503
- "id": railcontent_id,
504
- title,
505
- "image": thumbnail.asset->url,
506
- "instructors": instructor[]->name,
507
- length_in_seconds,
508
- }`,
509
- ],
510
- },
511
- 'learning-path-level': {
512
- fields: [
513
- '"lesson_count": child_count',
514
- '"instructors": instructor[]->name',
515
- `"description": ${descriptionField}`,
516
- `"resource": ${resourcesField}`,
517
- 'xp',
518
- 'total_xp',
519
- `"lessons": child[]->{
520
- "id": railcontent_id,
521
- title,
522
- "image": thumbnail.asset->url,
523
- "instructors": instructor[]->name,
524
- length_in_seconds,
525
- }`,
526
- ],
527
- },
528
474
  workout: {
529
475
  fields: [artistOrInstructorNameAsArray()],
530
476
  slug: 'workouts',
@@ -680,7 +626,7 @@ export let contentTypeConfig = {
680
626
  `"type":_type`,
681
627
  'title',
682
628
  'brand',
683
- `"intro_video": intro_video->{ ${getIntroVideoFields('method').join(', ')} }`,
629
+ `"intro_video": intro_video->{ ${getIntroVideoFields('method-v2').join(', ')} }`,
684
630
  `child[]->{
685
631
  "resource": ${resourcesField},
686
632
  total_skills,
@@ -740,7 +686,6 @@ export function getNewReleasesTypes(brand) {
740
686
  'podcasts',
741
687
  'pack',
742
688
  'song',
743
- 'learning-path-level',
744
689
  'play-along',
745
690
  'course',
746
691
  'unit',
package/src/index.d.ts CHANGED
@@ -13,10 +13,12 @@ import {
13
13
  } from './services/content-org/guided-courses.ts';
14
14
 
15
15
  import {
16
+ completeLearningPathIntroVideo,
17
+ completeMethodIntroVideo,
16
18
  fetchLearningPathLessons,
17
19
  getActivePath,
18
20
  getDailySession,
19
- getLearningPath,
21
+ getEnrichedLearningPath,
20
22
  getLearningPathLessonsByIds,
21
23
  mapContentToParent,
22
24
  resetAllLearningPaths,
@@ -132,6 +134,7 @@ import {
132
134
  fetchThreads,
133
135
  followThread,
134
136
  lockThread,
137
+ markThreadAsRead,
135
138
  pinThread,
136
139
  unfollowThread,
137
140
  unlockThread,
@@ -406,6 +409,8 @@ declare module 'musora-content-services' {
406
409
  buildImageSRC,
407
410
  calculateLongestStreaks,
408
411
  closeComment,
412
+ completeLearningPathIntroVideo,
413
+ completeMethodIntroVideo,
409
414
  confirmEmailChange,
410
415
  contentStatusCompleted,
411
416
  contentStatusReset,
@@ -542,8 +547,8 @@ declare module 'musora-content-services' {
542
547
  getAwardDataForGuidedContent,
543
548
  getContentRows,
544
549
  getDailySession,
550
+ getEnrichedLearningPath,
545
551
  getLastInteractedOf,
546
- getLearningPath,
547
552
  getLearningPathLessonsByIds,
548
553
  getLegacyMethods,
549
554
  getLessonContentRows,
@@ -604,6 +609,7 @@ declare module 'musora-content-services' {
604
609
  markContentAsNotInterested,
605
610
  markNotificationAsRead,
606
611
  markNotificationAsUnread,
612
+ markThreadAsRead,
607
613
  numberOfActiveUsers,
608
614
  openComment,
609
615
  otherStats,
package/src/index.js CHANGED
@@ -13,10 +13,12 @@ import {
13
13
  } from './services/content-org/guided-courses.ts';
14
14
 
15
15
  import {
16
+ completeLearningPathIntroVideo,
17
+ completeMethodIntroVideo,
16
18
  fetchLearningPathLessons,
17
19
  getActivePath,
18
20
  getDailySession,
19
- getLearningPath,
21
+ getEnrichedLearningPath,
20
22
  getLearningPathLessonsByIds,
21
23
  mapContentToParent,
22
24
  resetAllLearningPaths,
@@ -132,6 +134,7 @@ import {
132
134
  fetchThreads,
133
135
  followThread,
134
136
  lockThread,
137
+ markThreadAsRead,
135
138
  pinThread,
136
139
  unfollowThread,
137
140
  unlockThread,
@@ -405,6 +408,8 @@ export {
405
408
  buildImageSRC,
406
409
  calculateLongestStreaks,
407
410
  closeComment,
411
+ completeLearningPathIntroVideo,
412
+ completeMethodIntroVideo,
408
413
  confirmEmailChange,
409
414
  contentStatusCompleted,
410
415
  contentStatusReset,
@@ -541,8 +546,8 @@ export {
541
546
  getAwardDataForGuidedContent,
542
547
  getContentRows,
543
548
  getDailySession,
549
+ getEnrichedLearningPath,
544
550
  getLastInteractedOf,
545
- getLearningPath,
546
551
  getLearningPathLessonsByIds,
547
552
  getLegacyMethods,
548
553
  getLessonContentRows,
@@ -603,6 +608,7 @@ export {
603
608
  markContentAsNotInterested,
604
609
  markNotificationAsRead,
605
610
  markNotificationAsUnread,
611
+ markThreadAsRead,
606
612
  numberOfActiveUsers,
607
613
  openComment,
608
614
  otherStats,
@@ -5,9 +5,25 @@
5
5
  import { fetchHandler } from '../railcontent.js'
6
6
  import { fetchByRailContentId, fetchByRailContentIds, fetchMethodV2Structure } from '../sanity.js'
7
7
  import { addContextToContent } from '../contentAggregator.js'
8
- import { getProgressStateByIds } from '../contentProgress.js'
8
+ import {
9
+ contentStatusCompleted,
10
+ contentStatusReset,
11
+ getProgressState,
12
+ getProgressStateByIds
13
+ } from '../contentProgress.js'
9
14
 
10
15
  const BASE_PATH: string = `/api/content-org`
16
+ const LEARNING_PATHS_PATH = `${BASE_PATH}/v1/user/learning-paths`
17
+
18
+ interface ActiveLearningPathResponse {
19
+ user_id: number,
20
+ brand: string,
21
+ active_learning_path_id: number,
22
+ }
23
+
24
+
25
+
26
+
11
27
 
12
28
  /**
13
29
  * Gets today's daily session for the user.
@@ -16,7 +32,7 @@ const BASE_PATH: string = `/api/content-org`
16
32
  */
17
33
  export async function getDailySession(brand: string, userDate: Date) {
18
34
  const stringDate = userDate.toISOString().split('T')[0]
19
- const url: string = `${BASE_PATH}/v1/user/learning-paths/daily-session/get-or-create`
35
+ const url: string = `${LEARNING_PATHS_PATH}/daily-session/get-or-create`
20
36
  const body = { brand: brand, userDate: stringDate }
21
37
  return await fetchHandler(url, 'POST', null, body)
22
38
  }
@@ -33,7 +49,7 @@ export async function updateDailySession(
33
49
  keepFirstLearningPath: boolean
34
50
  ) {
35
51
  const stringDate = userDate.toISOString().split('T')[0]
36
- const url: string = `${BASE_PATH}/v1/user/learning-paths/daily-session/update`
52
+ const url: string = `${LEARNING_PATHS_PATH}/daily-session/update`
37
53
  const body = { brand: brand, userDate: stringDate, keepFirstLearningPath: keepFirstLearningPath }
38
54
  return await fetchHandler(url, 'POST', null, body)
39
55
  }
@@ -43,17 +59,19 @@ export async function updateDailySession(
43
59
  * @param brand
44
60
  */
45
61
  export async function getActivePath(brand: string) {
46
- const url: string = `${BASE_PATH}/v1/user/learning-paths/active-path/get-or-create`
62
+ const url: string = `${LEARNING_PATHS_PATH}/active-path/get-or-create`
47
63
  const body = { brand: brand }
48
64
  return await fetchHandler(url, 'POST', null, body)
49
65
  }
50
66
 
67
+ // todo this should be removed once we handle active path gen only through
68
+ // finish method intro or complete current active path
51
69
  /**
52
70
  * Updates user's active learning path.
53
71
  * @param brand
54
72
  */
55
73
  export async function updateActivePath(brand: string) {
56
- const url: string = `${BASE_PATH}/v1/user/learning-paths/active-path/update`
74
+ const url: string = `${LEARNING_PATHS_PATH}/active-path/update`
57
75
  const body = { brand: brand }
58
76
  return await fetchHandler(url, 'POST', null, body)
59
77
  }
@@ -64,7 +82,7 @@ export async function updateActivePath(brand: string) {
64
82
  * @param learningPathId
65
83
  */
66
84
  export async function startLearningPath(brand: string, learningPathId: number) {
67
- const url: string = `${BASE_PATH}/v1/user/learning-paths/start`
85
+ const url: string = `${LEARNING_PATHS_PATH}/start`
68
86
  const body = { brand: brand, learning_path_id: learningPathId }
69
87
  return await fetchHandler(url, 'POST', null, body)
70
88
  }
@@ -73,7 +91,7 @@ export async function startLearningPath(brand: string, learningPathId: number) {
73
91
  * Resets the user's learning path.
74
92
  */
75
93
  export async function resetAllLearningPaths() {
76
- const url: string = `${BASE_PATH}/v1/user/learning-paths/reset`
94
+ const url: string = `${LEARNING_PATHS_PATH}/reset`
77
95
  return await fetchHandler(url, 'POST', null, {})
78
96
  }
79
97
 
@@ -82,8 +100,8 @@ export async function resetAllLearningPaths() {
82
100
  * @param {number} learningPathId - The learning path ID
83
101
  * @returns {Promise<Object>} Learning path with enriched lesson data
84
102
  */
85
- export async function getLearningPath(learningPathId) {
86
- //TODO: must be a cleaner way to do this
103
+ export async function getEnrichedLearningPath(learningPathId) {
104
+ //TODO BEH-1410: refactor/cleanup
87
105
  let learningPath = await fetchByRailContentId(learningPathId, 'learning-path-v2')
88
106
  learningPath.children = mapContentToParent(
89
107
  learningPath.children,
@@ -109,10 +127,16 @@ export async function getLearningPath(learningPathId) {
109
127
  export async function getLearningPathLessonsByIds(contentIds, learningPathId) {
110
128
  // It is more efficient to load the entire learning path than individual lessons
111
129
  // Also adds reliability check whether content is actually in the learning path
112
- const learningPath = await getLearningPath(learningPathId)
130
+ const learningPath = await getEnrichedLearningPath(learningPathId)
113
131
  return learningPath.children.filter((lesson) => contentIds.includes(lesson.id))
114
132
  }
115
133
 
134
+ /**
135
+ * Maps content to its parent learning path - fixes multi-parent problems for cta when lessons have a special collection.
136
+ * @param lessons
137
+ * @param parentContentType
138
+ * @param parentContentId
139
+ */
116
140
  export function mapContentToParent(lessons, parentContentType, parentContentId) {
117
141
  return lessons.map((lesson: any) => {
118
142
  return { ...lesson, type: parentContentType, parent_id: parentContentId }
@@ -142,7 +166,7 @@ export async function fetchLearningPathLessons(
142
166
  userDate: Date
143
167
  ) {
144
168
  const [learningPath, dailySession] = await Promise.all([
145
- getLearningPath(learningPathId),
169
+ getEnrichedLearningPath(learningPathId),
146
170
  getDailySession(brand, userDate),
147
171
  ])
148
172
 
@@ -198,7 +222,69 @@ export async function fetchLearningPathLessons(
198
222
  upcoming_lessons: upcomingLessons,
199
223
  todays_lessons: todaysLessons,
200
224
  next_learning_path_lessons: nextLPLessons,
225
+ next_learning_path_id: nextLearningPathId,
201
226
  completed_lessons: completedLessons,
202
227
  previous_learning_path_todays: previousLearningPathTodays,
203
228
  }
204
229
  }
230
+
231
+ interface completeMethodIntroVideo {
232
+ intro_video_response: Object | null,
233
+ active_path_response: ActiveLearningPathResponse
234
+ }
235
+ /**
236
+ * Handles completion of method intro video and other related actions.
237
+ * @param introVideoId - The intro video content ID.
238
+ * @param brand
239
+ * @returns {Promise<Array>} response - The response object.
240
+ * @returns {Promise<Object|null>} response.intro_video_response - The intro video completion response or null if already completed.
241
+ * @returns {Promise<Object>} response.active_path_response - The set active learning path response.
242
+ */
243
+ export async function completeMethodIntroVideo(introVideoId: number, brand: string): Promise<completeMethodIntroVideo> {
244
+ let response = {} as completeMethodIntroVideo
245
+
246
+ response.intro_video_response = await completeIfNotCompleted(introVideoId)
247
+
248
+ const url: string = `${LEARNING_PATHS_PATH}/start`
249
+ const body = { brand: brand }
250
+ response.active_path_response = await fetchHandler(url, 'POST', null, body)
251
+
252
+ return response
253
+ }
254
+
255
+ interface completeLearningPathIntroVideo {
256
+ intro_video_response: Object | null,
257
+ learning_path_reset_response: void | Object[] | Object
258
+ }
259
+ /**
260
+ * Handles completion of learning path intro video and other related actions.
261
+ * @param introVideoId
262
+ * @param learningPathId
263
+ * @param lessonsToImport
264
+ */
265
+ export async function completeLearningPathIntroVideo(introVideoId: number, learningPathId: number, lessonsToImport: number[] | null) {
266
+ let response = {} as completeLearningPathIntroVideo
267
+
268
+ response.intro_video_response = await completeIfNotCompleted(introVideoId)
269
+
270
+ if (!lessonsToImport) {
271
+ // reset progress within the learning path
272
+ response.learning_path_reset_response = await contentStatusReset(learningPathId)
273
+ } else {
274
+ response.learning_path_reset_response = null
275
+
276
+ // todo: add collection context + optimize with bulk calls with watermelon
277
+ for (const contentId of lessonsToImport) {
278
+ response.learning_path_reset_response[contentId] = await contentStatusCompleted(contentId)
279
+ }
280
+ }
281
+
282
+ return response
283
+ }
284
+
285
+
286
+ async function completeIfNotCompleted(contentId: number): Promise<Object | null> {
287
+ const introVideoStatus = await getProgressState(contentId)
288
+
289
+ return introVideoStatus !== 'completed' ? await contentStatusCompleted(contentId) : null
290
+ }
@@ -878,6 +878,7 @@ export async function fetchAllFilterOptions(
878
878
  return includeTabs ? { ...results, tabs, catalogName } : results
879
879
  }
880
880
 
881
+ //Daniel Nov 14 2025 note - keeping this for when we migrate foundations to packs, so we know what fields to use.
881
882
  /**
882
883
  * Fetch the Foundations 2019.
883
884
  * @param {string} slug - The slug of the method.
@@ -903,6 +904,7 @@ export async function fetchFoundation(slug) {
903
904
  * @param {string} slug - The slug of the method.
904
905
  * @returns {Promise<Object|null>} - The fetched methods data or null if not found.
905
906
  */
907
+ //todo BEH-1446 depreciated. remove all old method functions
906
908
  export async function fetchMethod(brand, slug) {
907
909
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
908
910