musora-content-services 2.15.1 → 2.17.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.
Files changed (124) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/pull_request_template.md +0 -0
  4. package/.github/workflows/conventional-commits.yaml +0 -0
  5. package/.github/workflows/docs.js.yml +0 -0
  6. package/CHANGELOG.md +23 -0
  7. package/README.md +0 -0
  8. package/docs/Content-Organization.html +0 -0
  9. package/docs/ContentOrganization.html +2 -2
  10. package/docs/Gamification.html +2 -2
  11. package/docs/UserManagement.html +0 -0
  12. package/docs/UserManagementSystem.html +2 -2
  13. package/docs/api_types.js.html +2 -2
  14. package/docs/config.js.html +2 -2
  15. package/docs/content-org_content-org.js.html +2 -2
  16. package/docs/content-org_playlists-types.js.html +2 -2
  17. package/docs/content-org_playlists.js.html +2 -2
  18. package/docs/content.js.html +2 -2
  19. package/docs/gamification_awards.js.html +2 -2
  20. package/docs/gamification_gamification.js.html +2 -2
  21. package/docs/gamification_types.js.html +2 -2
  22. package/docs/global.html +2 -2
  23. package/docs/global.html#User +0 -0
  24. package/docs/index.html +2 -2
  25. package/docs/module-Awards.html +2 -2
  26. package/docs/module-Config.html +2 -2
  27. package/docs/module-Content-Services-V2.html +2 -2
  28. package/docs/module-Interests.html +2 -2
  29. package/docs/module-Notifications.html +0 -0
  30. package/docs/module-Permissions.html +2 -2
  31. package/docs/module-Playlists.html +2 -2
  32. package/docs/module-Railcontent-Services.html +175 -7
  33. package/docs/module-Sanity-Services.html +28 -28
  34. package/docs/module-Session-Management.html +0 -0
  35. package/docs/module-Sessions.html +2 -2
  36. package/docs/module-User-Activity.html +0 -0
  37. package/docs/module-User-Management.html +0 -0
  38. package/docs/module-User-Permissions.html +0 -0
  39. package/docs/module-UserActivity.html +30 -6
  40. package/docs/module-UserChat.html +2 -2
  41. package/docs/module-UserManagement.html +2 -2
  42. package/docs/module-UserNotifications.html +12 -98
  43. package/docs/module-UserProfile.html +2 -2
  44. package/docs/railcontent.js.html +19 -2
  45. package/docs/sanity.js.html +17 -8
  46. package/docs/types.js.html +0 -0
  47. package/docs/userActivity.js.html +15 -21
  48. package/docs/user_chat.js.html +2 -2
  49. package/docs/user_interests.js.html +2 -2
  50. package/docs/user_management.js.html +2 -2
  51. package/docs/user_notifications.js.html +8 -18
  52. package/docs/user_permissions.js.html +2 -2
  53. package/docs/user_profile.js.html +2 -2
  54. package/docs/user_sessions.js.html +2 -2
  55. package/docs/user_types.js.html +2 -2
  56. package/docs/user_user-management-system.js.html +2 -2
  57. package/docs/user_user-management.js.html +0 -0
  58. package/jsdoc.json +0 -0
  59. package/package.json +1 -1
  60. package/src/contentMetaData.js +0 -0
  61. package/src/contentTypeConfig.js +4 -3
  62. package/src/filterBuilder.js +0 -0
  63. package/src/index.d.ts +11 -1
  64. package/src/index.js +11 -1
  65. package/src/infrastructure/http/HttpClient.ts +0 -0
  66. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  67. package/src/infrastructure/http/index.ts +0 -0
  68. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  69. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  70. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  71. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  72. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  73. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  74. package/src/lib/httpHelper.js +0 -0
  75. package/src/lib/lastUpdated.js +0 -0
  76. package/src/services/api/types.js +0 -0
  77. package/src/services/config.js +0 -0
  78. package/src/services/content-org/content-org.js +0 -0
  79. package/src/services/content-org/guided-courses.ts +20 -0
  80. package/src/services/content-org/playlists-types.js +0 -0
  81. package/src/services/content-org/playlists.js +0 -0
  82. package/src/services/content.js +0 -0
  83. package/src/services/contentAggregator.js +2 -2
  84. package/src/services/contentLikes.js +0 -0
  85. package/src/services/contentProgress.js +1 -1
  86. package/src/services/dataContext.js +0 -0
  87. package/src/services/dateUtils.js +0 -0
  88. package/src/services/forum.js +0 -0
  89. package/src/services/gamification/awards.js +0 -0
  90. package/src/services/gamification/gamification.js +0 -0
  91. package/src/services/gamification/types.js +0 -0
  92. package/src/services/imageSRCBuilder.js +0 -0
  93. package/src/services/railcontent.js +17 -0
  94. package/src/services/recommendations.js +0 -0
  95. package/src/services/sanity.js +11 -3
  96. package/src/services/types.js +0 -0
  97. package/src/services/user/chat.js +0 -0
  98. package/src/services/user/interests.js +0 -0
  99. package/src/services/user/management.js +0 -0
  100. package/src/services/user/notifications.js +6 -16
  101. package/src/services/user/permissions.js +0 -0
  102. package/src/services/user/profile.js +0 -0
  103. package/src/services/user/sessions.js +0 -0
  104. package/src/services/user/types.js +0 -0
  105. package/src/services/user/user-management-system.js +0 -0
  106. package/src/services/userActivity.js +13 -19
  107. package/test/HttpClient.test.js +0 -0
  108. package/test/content.test.js +0 -0
  109. package/test/contentLikes.test.js +0 -0
  110. package/test/contentProgress.test.js +0 -0
  111. package/test/forum.test.js +0 -0
  112. package/test/initializeTests.js +0 -0
  113. package/test/lib/lastUpdated.test.js +0 -0
  114. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  115. package/test/mockData/mockData_progress_content.json +0 -0
  116. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  117. package/test/mockData/mockData_user_practices.json +0 -0
  118. package/test/notifications.test.js +3 -12
  119. package/test/progressRows.test.js +0 -0
  120. package/test/sanityQueryService.test.js +0 -0
  121. package/test/streakMessage.test.js +0 -0
  122. package/test/user/permissions.test.js +0 -0
  123. package/test/userActivity.test.js +0 -0
  124. package/tools/generate-index.cjs +0 -0
File without changes
File without changes
@@ -24,3 +24,23 @@ export async function fetchEnrollmentPageMetadata(guidedCourse) {
24
24
  const url: string = `${BASE_PATH}/v1/user/guided-courses/enrollment/${guidedCourse}`
25
25
  return await fetchHandler(url, 'GET')
26
26
  }
27
+
28
+ export async function pinGuidedCourse(guidedCourse) {
29
+ const url: string = `${BASE_PATH}/v1/user/guided-courses/pin/${guidedCourse}`
30
+ return await fetchHandler(url, 'POST')
31
+ }
32
+
33
+ export async function unPinGuidedCourse(guidedCourse) {
34
+ const url: string = `${BASE_PATH}/v1/user/guided-courses/unpin/${guidedCourse}`
35
+ return await fetchHandler(url, 'POST')
36
+ }
37
+
38
+ export async function guidedCourses() {
39
+ const url: string = `${BASE_PATH}/v1/user/guided-courses/`
40
+ return await fetchHandler(url, 'GET')
41
+ }
42
+
43
+ export async function pinnedGuidedCourses() {
44
+ const url: string = `${BASE_PATH}/v1/user/guided-courses/pinned`
45
+ return await fetchHandler(url, 'GET')
46
+ }
File without changes
File without changes
File without changes
@@ -43,8 +43,7 @@ export async function addContextToContent(dataPromise, ...dataArgs)
43
43
  addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
44
44
  addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
45
45
  ])
46
- console.log('ids', ids)
47
- console.log('lastInteractedChildData', lastInteractedChildData)
46
+
48
47
  const addContext = async (item) => ({
49
48
  ...item,
50
49
  ...(addProgressPercentage ? { progressPercentage: progressPercentageData?.[item.id] } : {}),
@@ -54,6 +53,7 @@ export async function addContextToContent(dataPromise, ...dataArgs)
54
53
  ...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
55
54
  ...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
56
55
  })
56
+
57
57
  if (dataField) {
58
58
  data[dataField] = Array.isArray(data[dataField])
59
59
  ? await Promise.all(data[dataField].map(addContext))
File without changes
@@ -361,7 +361,7 @@ function getMediaTypeId(mediaType, mediaCategory) {
361
361
  case 'practice_play-alongs':
362
362
  return 4
363
363
  case 'video_soundslice':
364
- return 6
364
+ return 3
365
365
  default:
366
366
  return 5
367
367
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -755,6 +755,23 @@ export async function reportComment(commentId, issue) {
755
755
  return await postDataHandler(url, data)
756
756
  }
757
757
 
758
+ /**
759
+ * Fetches a single comment by its ID.
760
+ *
761
+ * @param {number|string} commentId - The ID of the comment to fetch.
762
+ * @returns {Promise<Object|null>} - A promise that resolves to the comment object if found, otherwise null.
763
+ *
764
+ * @example
765
+ * fetchComment(123)
766
+ * .then(comment => console.log(comment))
767
+ * .catch(error => console.error(error));
768
+ */
769
+ export async function fetchComment(commentId) {
770
+ const url = `/api/content/v1/comments/${commentId}`
771
+ const comment = await fetchHandler(url)
772
+ return comment.parent ? comment.parent : comment
773
+ }
774
+
758
775
  export async function fetchUserPractices(currentVersion = 0, { userId } = {}) {
759
776
  const params = new URLSearchParams();
760
777
  if (userId) params.append('user_id', userId);
File without changes
@@ -17,7 +17,7 @@ import {
17
17
  getChildFieldsForContentType,
18
18
  SONG_TYPES,
19
19
  } from '../contentTypeConfig.js'
20
- import { fetchSimilarItems } from './recommendations.js'
20
+ import {fetchSimilarItems, recommendations} from './recommendations.js'
21
21
  import { processMetadata, typeWithSortOrder } from '../contentMetaData.js'
22
22
 
23
23
  import { globalConfig } from './config.js'
@@ -903,7 +903,7 @@ async function getProgressFilter(progress, progressIds) {
903
903
  export function getSortOrder(sort = '-published_on', brand, groupBy) {
904
904
  // Determine the sort order
905
905
  let sortOrder = ''
906
- const isDesc = sort.startsWith('-')
906
+ let isDesc = sort.startsWith('-')
907
907
  sort = isDesc ? sort.substring(1) : sort
908
908
  switch (sort) {
909
909
  case 'slug':
@@ -919,6 +919,10 @@ export function getSortOrder(sort = '-published_on', brand, groupBy) {
919
919
  sortOrder = isDesc ? 'coalesce(popularity, -1)' : 'popularity'
920
920
  }
921
921
  break
922
+ case 'recommended':
923
+ sortOrder = 'published_on'
924
+ isDesc = true
925
+ break
922
926
  case 'published_on':
923
927
  default:
924
928
  sortOrder = 'published_on'
@@ -2337,6 +2341,10 @@ export async function fetchTabData(
2337
2341
 
2338
2342
  // limits the results to supplied progressIds for started & completed filters
2339
2343
  const progressFilter = await getProgressFilter(progress, progressIds)
2344
+ if(sort == "recommended"){
2345
+ progressIds = await recommendations(brand);
2346
+ withoutPagination = true;
2347
+ }
2340
2348
 
2341
2349
  let fields = DEFAULT_FIELDS
2342
2350
  let fieldsString = fields.join(',')
@@ -2369,7 +2377,7 @@ export async function fetchTabData(
2369
2377
 
2370
2378
  let results = await fetchSanity(query, true);
2371
2379
 
2372
- if (['recent', 'incomplete', 'completed'].includes(progress) && results.entity.length > 1) {
2380
+ if ((['recent', 'incomplete', 'completed'].includes(progress) || sort == "recommended" )&& results.entity.length > 1) {
2373
2381
  const orderMap = new Map(progressIds.map((id, index) => [id, index]))
2374
2382
  results.entity = results.entity
2375
2383
  .sort((a, b) => {
File without changes
File without changes
File without changes
File without changes
@@ -16,7 +16,6 @@ const NotificationChannels = {
16
16
  * Fetches notifications for a given brand with optional filters for unread status and limit.
17
17
  *
18
18
  * @param {Object} [options={}] - Options for fetching notifications.
19
- * @param {string} options.brand - The brand to filter notifications by. (Required)
20
19
  * @param {number} [options.limit=10] - The maximum number of notifications to fetch.
21
20
  * @param {number} [options.page=1] - The page number for pagination.
22
21
  * @param {boolean} [options.onlyUnread=false] - Whether to fetch only unread notifications. If true, adds `unread=1` to the query.
@@ -26,17 +25,13 @@ const NotificationChannels = {
26
25
  * @throws {Error} - Throws an error if the brand is not provided.
27
26
  *
28
27
  * @example
29
- * fetchNotifications({ brand: 'drumeo', limit: 5, onlyUnread: true, page: 2 })
28
+ * fetchNotifications({ limit: 5, onlyUnread: true, page: 2 })
30
29
  * .then(notifications => console.log(notifications))
31
30
  * .catch(error => console.error(error));
32
31
  */
33
- export async function fetchNotifications({ brand = null, limit = 10, onlyUnread = false, page = 1 } = {}) {
34
- if (!brand) {
35
- throw new Error('brand is required')
36
- }
37
-
32
+ export async function fetchNotifications({ limit = 10, onlyUnread = false, page = 1 } = {}) {
38
33
  const unreadParam = onlyUnread ? '&unread=1' : ''
39
- const url = `${baseUrl}/v1?brand=${brand}${unreadParam}&limit=${limit}&page=${page}`
34
+ const url = `${baseUrl}/v1?limit=${limit}&page=${page}${unreadParam}`
40
35
  return fetchHandler(url, 'get')
41
36
  }
42
37
 
@@ -66,8 +61,6 @@ export async function markNotificationAsRead(notificationId) {
66
61
  /**
67
62
  * Marks all notifications as read for a specific brand.
68
63
  *
69
- * @param {string} brand - The brand to filter notifications by.
70
- *
71
64
  * @returns {Promise<any>} - A promise that resolves when all notifications are marked as read.
72
65
  *
73
66
  * @example
@@ -75,8 +68,8 @@ export async function markNotificationAsRead(notificationId) {
75
68
  * .then(response => console.log(response))
76
69
  * .catch(error => console.error(error));
77
70
  */
78
- export async function markAllNotificationsAsRead(brand) {
79
- const url = `${baseUrl}/v1/read?brand=${brand}`
71
+ export async function markAllNotificationsAsRead() {
72
+ const url = `${baseUrl}/v1/read`
80
73
  return fetchHandler(url, 'put')
81
74
  }
82
75
 
@@ -141,10 +134,7 @@ export async function deleteNotification(notificationId) {
141
134
  * .catch(error => console.error(error));
142
135
  */
143
136
  export async function fetchUnreadCount({ brand = null} = {}) {
144
- if (!brand) {
145
- throw new Error('brand is required')
146
- }
147
- const url = `${baseUrl}/v1/unread-count?brand=${brand}`
137
+ const url = `${baseUrl}/v1/unread-count`
148
138
  return fetchHandler(url, 'get')
149
139
  }
150
140
 
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1030,7 +1030,7 @@ async function processContentItem(item) {
1030
1030
  let ctaText = 'Continue';
1031
1031
  if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
1032
1032
  if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
1033
- if ((contentType === 'guided course' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) && status === 'completed') ctaText = 'Revisit Lessons' ;
1033
+ if (collectionLessonTypes.includes(contentType) && status === 'completed') ctaText = 'Revisit Lessons' ;
1034
1034
  if (contentType === 'pack' && status === 'completed') {
1035
1035
  ctaText = 'View Lessons';
1036
1036
  }
@@ -1064,15 +1064,6 @@ async function processContentItem(item) {
1064
1064
  const nextLesson = lessons.find(lesson => lesson.id === nextId)
1065
1065
  data.first_incomplete_child = nextLesson?.parent ?? nextLesson
1066
1066
  data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
1067
- if(data.type === 'challenge' && nextByProgress !== undefined ){
1068
- const challenge = await fetchChallengeLessonData(nextByProgress)
1069
- if(challenge.lesson.is_locked) {
1070
- const timeRemaining = getTimeRemainingUntilLocal(challenge.lesson.unlock_date, {withTotalSeconds:true})
1071
- data.is_locked = true
1072
- data.time_remaining_seconds = timeRemaining.totalSeconds
1073
- ctaText = 'Next lesson in ' + timeRemaining.formatted
1074
- }
1075
- }
1076
1067
  }
1077
1068
  }
1078
1069
 
@@ -1250,7 +1241,7 @@ function mergeAndSortItems(items, limit) {
1250
1241
 
1251
1242
  function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1252
1243
  const ids = Object.keys(progressOnItems).map(Number);
1253
- if (contentType === 'challenge') {
1244
+ if (contentType === 'guided-course') {
1254
1245
  // Return first incomplete lesson
1255
1246
  return ids.find(id => progressOnItems[id] !== 'completed') || ids.at(0);
1256
1247
  }
@@ -1283,10 +1274,11 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1283
1274
  * .catch(error => console.error(error));
1284
1275
  */
1285
1276
  export async function pinProgressRow(brand, id, progressType) {
1277
+ if (!(brand && id && progressType)) throw new Error(`undefined parameter progressType: ${progressType} brand: ${brand} or id: ${id}`)
1286
1278
  const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
1287
1279
  const response = await fetchHandler(url, 'PUT', null)
1288
- if (response && !response.error) {
1289
- await updatePinnedProgressRow(brand, {
1280
+ if (response && !response.error && response['action'] === 'update_user_pin') {
1281
+ await updateUserPinnedProgressRow(brand, {
1290
1282
  id,
1291
1283
  progressType,
1292
1284
  pinnedAt: new Date().toISOString(),
@@ -1298,23 +1290,25 @@ export async function pinProgressRow(brand, id, progressType) {
1298
1290
  * Unpins the current pinned progress row for a user, scoped by brand.
1299
1291
  *
1300
1292
  * @param {string} brand - The brand context for the unpin action.
1293
+ * @param {string} id - The content or playlist id to unpin.
1301
1294
  * @returns {Promise<Object>} - A promise resolving to the response from the unpin API.
1302
1295
  *
1303
1296
  * @example
1304
- * unpinProgressRow('drumeo')
1297
+ * unpinProgressRow('drumeo', 123456)
1305
1298
  * .then(response => console.log(response))
1306
1299
  * .catch(error => console.error(error));
1307
1300
  */
1308
- export async function unpinProgressRow(brand) {
1309
- const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
1301
+ export async function unpinProgressRow(brand, id) {
1302
+ if (!(brand && id)) throw new Error(`undefined parameter brand: ${brand} or id: ${id}`)
1303
+ const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}&id=${id}`
1310
1304
  const response = await fetchHandler(url, 'PUT', null)
1311
- if (response && !response.error) {
1312
- await updatePinnedProgressRow(brand, null)
1305
+ if (response && !response.error && response['action'] === 'clear_user_pin') {
1306
+ await updateUserPinnedProgressRow(brand, null)
1313
1307
  }
1314
1308
  return response
1315
1309
  }
1316
1310
 
1317
- async function updatePinnedProgressRow(brand, pinnedData) {
1311
+ async function updateUserPinnedProgressRow(brand, pinnedData) {
1318
1312
  const userRaw = await globalConfig.localStorage.getItem('user');
1319
1313
  const user = userRaw ? JSON.parse(userRaw) : {};
1320
1314
  user.brand_pinned_progress = user.brand_pinned_progress || {}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -16,22 +16,17 @@ describe('UserNotifications module', function () {
16
16
  })
17
17
 
18
18
  describe('fetchNotifications', () => {
19
- it('throws if brand not provided', async () => {
20
- await expect(UserNotifications.fetchNotifications()).rejects.toThrow('brand is required')
21
- })
22
-
23
19
  it('calls fetchHandler with correct url and method', async () => {
24
20
  fetchHandler.mockResolvedValueOnce([{ id: 1 }])
25
21
 
26
22
  const result = await UserNotifications.fetchNotifications({
27
- brand: 'drumeo',
28
23
  limit: 5,
29
24
  onlyUnread: true,
30
25
  page: 2,
31
26
  })
32
27
 
33
28
  expect(fetchHandler).toHaveBeenCalledWith(
34
- `${baseUrl}/v1?brand=drumeo&unread=1&limit=5&page=2`,
29
+ `${baseUrl}/v1?limit=5&page=2&unread=1`,
35
30
  'get'
36
31
  )
37
32
  expect(result).toEqual([{ id: 1 }])
@@ -91,15 +86,11 @@ describe('UserNotifications module', function () {
91
86
  })
92
87
 
93
88
  describe('fetchUnreadCount', () => {
94
- it('throws if brand not provided', async () => {
95
- await expect(UserNotifications.fetchUnreadCount()).rejects.toThrow('brand is required')
96
- })
97
-
98
89
  it('calls fetchHandler with correct url and method', async () => {
99
90
  fetchHandler.mockResolvedValueOnce({ unread_count: 42 })
100
91
 
101
- const result = await UserNotifications.fetchUnreadCount({ brand: 'drumeo' })
102
- expect(fetchHandler).toHaveBeenCalledWith(`${baseUrl}/v1/unread-count?brand=drumeo`, 'get')
92
+ const result = await UserNotifications.fetchUnreadCount()
93
+ expect(fetchHandler).toHaveBeenCalledWith(`${baseUrl}/v1/unread-count`, 'get')
103
94
  expect(result).toEqual({ unread_count: 42 })
104
95
  })
105
96
  })
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes