musora-content-services 2.30.5 → 2.30.9

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 CHANGED
@@ -2,6 +2,46 @@
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.30.9](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.9) (2025-08-13)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
11
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
12
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
13
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
14
+
15
+ ### [2.30.8](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.8) (2025-08-13)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
21
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
22
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
23
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
24
+
25
+ ### [2.30.7](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.7) (2025-08-13)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
31
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
32
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
33
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
34
+
35
+ ### [2.30.6](https://github.com/railroadmedia/musora-content-services/compare/v2.30.5...v2.30.6) (2025-08-13)
36
+
37
+
38
+ ### Bug Fixes
39
+
40
+ * **BEH-845:** enable soft deletes for user activities ([#384](https://github.com/railroadmedia/musora-content-services/issues/384)) ([9c146c4](https://github.com/railroadmedia/musora-content-services/commit/9c146c43536e517ca69d3f264b117850fec748ec))
41
+ * **MU-921:** add context to content empty arrays ([#410](https://github.com/railroadmedia/musora-content-services/issues/410)) ([0ca84ae](https://github.com/railroadmedia/musora-content-services/commit/0ca84aea741e605d17c0f45bbfb6cc8ea018e676))
42
+ * **recsys:** support navigateTo and permissions filtering for recomme… ([#403](https://github.com/railroadmedia/musora-content-services/issues/403)) ([864ec0f](https://github.com/railroadmedia/musora-content-services/commit/864ec0fc05704d9641a965bc4de63a97422668c4))
43
+ * **T3PS-454:** rank lessons by recommended ([#380](https://github.com/railroadmedia/musora-content-services/issues/380)) ([cc55bfd](https://github.com/railroadmedia/musora-content-services/commit/cc55bfd0fb19f1b42c42eb2ea6573817a8d2cc69))
44
+
5
45
  ### [2.30.5](https://github.com/railroadmedia/musora-content-services/compare/v2.30.4...v2.30.5) (2025-08-06)
6
46
 
7
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.30.5",
3
+ "version": "2.30.9",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -21,6 +21,7 @@ export class FilterBuilder {
21
21
  isSingle = false,
22
22
  allowsPullSongsContent = true,
23
23
  isChildrenFilter = false,
24
+ isParentFilter = false,
24
25
  } = {}
25
26
  ) {
26
27
  this.availableContentStatuses = availableContentStatuses
@@ -35,7 +36,17 @@ export class FilterBuilder {
35
36
  this.filter = filter
36
37
  // this.debug = process.env.DEBUG === 'true' || false;
37
38
  this.debug = false
38
- this.prefix = isChildrenFilter ? '@->' : ''
39
+ this.prefix = this.getPrefix(isParentFilter, isChildrenFilter)
40
+ }
41
+
42
+ getPrefix(isParentFilter, isChildrenFilter) {
43
+ if (isParentFilter) {
44
+ return '^.'
45
+ } else if (isChildrenFilter) {
46
+ return '@->'
47
+ } else {
48
+ return ''
49
+ }
39
50
  }
40
51
 
41
52
  static withOnlyFilterAvailableStatuses(filter, availableContentStatuses, bypassPermissions) {
package/src/index.d.ts CHANGED
@@ -44,7 +44,8 @@ import {
44
44
  } from './services/content.js';
45
45
 
46
46
  import {
47
- addContextToContent
47
+ addContextToContent,
48
+ getNavigateToForPlaylists
48
49
  } from './services/contentAggregator.js';
49
50
 
50
51
  import {
@@ -61,6 +62,7 @@ import {
61
62
  getAllStarted,
62
63
  getAllStartedOrCompleted,
63
64
  getLastInteractedOf,
65
+ getNavigateTo,
64
66
  getNextLesson,
65
67
  getProgressDateByIds,
66
68
  getProgressPercentage,
@@ -143,6 +145,7 @@ import {
143
145
  postContentComplete,
144
146
  postContentLiked,
145
147
  postContentReset,
148
+ postContentRestore,
146
149
  postContentUnliked,
147
150
  postPlaylistContentEngaged,
148
151
  postRecordWatchSession,
@@ -254,6 +257,7 @@ import {
254
257
  markNotificationAsRead,
255
258
  markNotificationAsUnread,
256
259
  pauseLiveEventPolling,
260
+ restoreNotification,
257
261
  startLiveEventPolling,
258
262
  updateNotificationSetting
259
263
  } from './services/user/notifications.js';
@@ -292,6 +296,7 @@ import {
292
296
  recordUserPractice,
293
297
  removeUserPractice,
294
298
  restorePracticeSession,
299
+ restoreUserActivity,
295
300
  restoreUserPractice,
296
301
  unpinProgressRow,
297
302
  updatePracticeNotes,
@@ -423,6 +428,8 @@ declare module 'musora-content-services' {
423
428
  getLastInteractedOf,
424
429
  getLessonContentRows,
425
430
  getMonday,
431
+ getNavigateTo,
432
+ getNavigateToForPlaylists,
426
433
  getNewAndUpcoming,
427
434
  getNextLesson,
428
435
  getPracticeNotes,
@@ -477,6 +484,7 @@ declare module 'musora-content-services' {
477
484
  postContentComplete,
478
485
  postContentLiked,
479
486
  postContentReset,
487
+ postContentRestore,
480
488
  postContentUnliked,
481
489
  postPlaylistContentEngaged,
482
490
  postRecordWatchSession,
@@ -496,7 +504,9 @@ declare module 'musora-content-services' {
496
504
  resetPassword,
497
505
  restoreComment,
498
506
  restoreItemFromPlaylist,
507
+ restoreNotification,
499
508
  restorePracticeSession,
509
+ restoreUserActivity,
500
510
  restoreUserPractice,
501
511
  sendAccountSetupEmail,
502
512
  sendPasswordResetEmail,
package/src/index.js CHANGED
@@ -44,7 +44,8 @@ import {
44
44
  } from './services/content.js';
45
45
 
46
46
  import {
47
- addContextToContent
47
+ addContextToContent,
48
+ getNavigateToForPlaylists
48
49
  } from './services/contentAggregator.js';
49
50
 
50
51
  import {
@@ -61,6 +62,7 @@ import {
61
62
  getAllStarted,
62
63
  getAllStartedOrCompleted,
63
64
  getLastInteractedOf,
65
+ getNavigateTo,
64
66
  getNextLesson,
65
67
  getProgressDateByIds,
66
68
  getProgressPercentage,
@@ -129,9 +131,9 @@ import {
129
131
  fetchNextContentDataForParent,
130
132
  fetchRecentUserActivities,
131
133
  fetchSongsInProgress,
134
+ fetchTopComment,
132
135
  fetchUserAward,
133
136
  fetchUserBadges,
134
- fetchTopComment,
135
137
  fetchUserLikes,
136
138
  fetchUserPermissionsData,
137
139
  fetchUserPracticeMeta,
@@ -143,6 +145,7 @@ import {
143
145
  postContentComplete,
144
146
  postContentLiked,
145
147
  postContentReset,
148
+ postContentRestore,
146
149
  postContentUnliked,
147
150
  postPlaylistContentEngaged,
148
151
  postRecordWatchSession,
@@ -254,6 +257,7 @@ import {
254
257
  markNotificationAsRead,
255
258
  markNotificationAsUnread,
256
259
  pauseLiveEventPolling,
260
+ restoreNotification,
257
261
  startLiveEventPolling,
258
262
  updateNotificationSetting
259
263
  } from './services/user/notifications.js';
@@ -292,6 +296,7 @@ import {
292
296
  recordUserPractice,
293
297
  removeUserPractice,
294
298
  restorePracticeSession,
299
+ restoreUserActivity,
295
300
  restoreUserPractice,
296
301
  unpinProgressRow,
297
302
  updatePracticeNotes,
@@ -422,6 +427,8 @@ export {
422
427
  getLastInteractedOf,
423
428
  getLessonContentRows,
424
429
  getMonday,
430
+ getNavigateTo,
431
+ getNavigateToForPlaylists,
425
432
  getNewAndUpcoming,
426
433
  getNextLesson,
427
434
  getPracticeNotes,
@@ -476,6 +483,7 @@ export {
476
483
  postContentComplete,
477
484
  postContentLiked,
478
485
  postContentReset,
486
+ postContentRestore,
479
487
  postContentUnliked,
480
488
  postPlaylistContentEngaged,
481
489
  postRecordWatchSession,
@@ -495,7 +503,9 @@ export {
495
503
  resetPassword,
496
504
  restoreComment,
497
505
  restoreItemFromPlaylist,
506
+ restoreNotification,
498
507
  restorePracticeSession,
508
+ restoreUserActivity,
499
509
  restoreUserPractice,
500
510
  sendAccountSetupEmail,
501
511
  sendPasswordResetEmail,
@@ -15,7 +15,7 @@ import {
15
15
  } from './sanity.js'
16
16
  import {TabResponseType, Tabs, capitalizeFirstLetter} from '../contentMetaData.js'
17
17
  import {fetchHandler} from "./railcontent";
18
- import {recommendations, rankCategories} from "./recommendations";
18
+ import {recommendations, rankCategories, rankItems} from "./recommendations";
19
19
  import {addContextToContent} from "./contentAggregator.js";
20
20
 
21
21
 
@@ -85,12 +85,22 @@ export async function getTabResults(brand, pageName, tabName, {
85
85
  addProgressStatus: true
86
86
  })
87
87
  } else {
88
- let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
89
- results = await addContextToContent(() => temp.entity, {
90
- addNextLesson: true,
91
- addNavigateTo: true,
92
- addProgressPercentage: true,
93
- addProgressStatus: true
88
+ let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
89
+
90
+ const [ranking, contextResults] = await Promise.all([
91
+ sort === 'recommended' ? rankItems(brand, temp.entity.map(e => e.railcontent_id)) : [],
92
+ addContextToContent(() => temp.entity, {
93
+ addNextLesson: true,
94
+ addNavigateTo: true,
95
+ addProgressPercentage: true,
96
+ addProgressStatus: true
97
+ })
98
+ ]);
99
+
100
+ results = ranking.length === 0 ? contextResults : contextResults.sort((a, b) => {
101
+ const indexA = ranking.indexOf(a.railcontent_id);
102
+ const indexB = ranking.indexOf(b.railcontent_id);
103
+ return (indexA === -1 ? Infinity : indexA) - (indexB === -1 ? Infinity : indexB);
94
104
  })
95
105
  }
96
106
 
@@ -392,7 +402,7 @@ export async function getRecommendedForYou(brand, rowId = null, {
392
402
  // Apply pagination before calling fetchByRailContentIds
393
403
  const startIndex = (page - 1) * limit;
394
404
  const paginatedData = data.slice(startIndex, startIndex + limit);
395
- const contents = await addContextToContent(fetchByRailContentIds, paginatedData, 'tab-data',
405
+ const contents = await addContextToContent(fetchByRailContentIds, paginatedData, 'tab-data', brand, true,
396
406
  {
397
407
  addNextLesson: true,
398
408
  addNavigateTo: true,
@@ -77,12 +77,13 @@ export async function addContextToContent(dataPromise, ...dataArgs)
77
77
  const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
78
78
 
79
79
  let data = await dataPromise(...dataParam)
80
- if(!data) return false
81
80
  const isDataAnArray = Array.isArray(data)
82
- const items = extractItemsFromData(data, dataField, isDataAnArray, dataField_includeParent)
83
- const ids = items.map(item => item?.id).filter(Boolean)
81
+ if(isDataAnArray && data.length === 0) return data
82
+ if(!data) return false
84
83
 
85
- if(ids.length === 0) return false
84
+ const items = extractItemsFromData(data, dataField, isDataAnArray, dataField_includeParent) ?? []
85
+ const ids = items.map(item => item?.id).filter(Boolean)
86
+ if(ids.length === 0) return data
86
87
 
87
88
  const [progressData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData, navigateToData] = await Promise.all([
88
89
  addProgressPercentage || addProgressStatus || addProgressTimestamp ? getProgressDateByIds(ids) : Promise.resolve(null),
@@ -378,16 +378,36 @@ export async function fetchUserBadges(brand = null) {
378
378
  return await fetchHandler(url, 'get')
379
379
  }
380
380
 
381
+ /**
382
+ * complete a content's progress for a given user
383
+ * @param contentId
384
+ * @returns {Promise<any|string|null>}
385
+ */
381
386
  export async function postContentComplete(contentId) {
382
387
  let url = `/api/content/v1/user/progress/complete/${contentId}`
383
388
  return postDataHandler(url)
384
389
  }
385
390
 
391
+ /**
392
+ * resets the user's progress on a content
393
+ * @param contentId
394
+ * @returns {Promise<any|string|null>}
395
+ */
386
396
  export async function postContentReset(contentId) {
387
397
  let url = `/api/content/v1/user/progress/reset/${contentId}`
388
398
  return postDataHandler(url)
389
399
  }
390
400
 
401
+ /**
402
+ * restores the user's progress on a content
403
+ * @param contentId
404
+ * @returns {Promise<any|string|null>}
405
+ */
406
+ export async function postContentRestore(contentId) {
407
+ let url = `/api/content/v1/user/progress/restore/${contentId}`
408
+ return postDataHandler(url)
409
+ }
410
+
391
411
  /**
392
412
  * Set a user's StudentView Flag
393
413
  *
@@ -476,7 +476,7 @@ export async function fetchByRailContentId(id, contentType) {
476
476
  * .then(contents => console.log(contents))
477
477
  * .catch(error => console.error(error));
478
478
  */
479
- export async function fetchByRailContentIds(ids, contentType = undefined, brand = undefined) {
479
+ export async function fetchByRailContentIds(ids, contentType = undefined, brand = undefined, includePermissionsAndStatusFilter = false) {
480
480
  if (!ids?.length) {
481
481
  return []
482
482
  }
@@ -485,8 +485,10 @@ export async function fetchByRailContentIds(ids, contentType = undefined, brand
485
485
  const brandFilter = brand ? ` && brand == "${brand}"` : ''
486
486
  const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {pullFutureContent: true}).buildFilter()
487
487
  const fields = await getFieldsForContentTypeWithFilteredChildren(contentType, true)
488
+ const baseFilter = `railcontent_id in [${idsString}]${brandFilter}`
489
+ const finalFilter = includePermissionsAndStatusFilter ? await new FilterBuilder(baseFilter).buildFilter() : baseFilter
488
490
  const query = `*[
489
- railcontent_id in [${idsString}]${brandFilter}
491
+ ${finalFilter}
490
492
  ]{
491
493
  ${fields}
492
494
  'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
@@ -535,7 +537,9 @@ export async function fetchContentRows(brand, pageName, contentRowSlug)
535
537
  name,
536
538
  'slug': slug.current,
537
539
  'content': content[${childFilter}]->{
538
- 'children': child[${childFilter}]->{ 'id': railcontent_id, 'children': child[${childFilter}]->{'id': railcontent_id}, },
540
+ 'children': child[${childFilter}]->{ 'id': railcontent_id,
541
+ 'type': _type, brand, 'thumbnail': thumbnail.asset->url,
542
+ 'children': child[${childFilter}]->{'id': railcontent_id}, },
539
543
  ${getFieldsForContentType('tab-data')}
540
544
  'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
541
545
  },
@@ -1089,6 +1093,7 @@ export async function jumpToContinueContent(railcontentId) {
1089
1093
  /**
1090
1094
  * Fetch the page data for a specific lesson by Railcontent ID.
1091
1095
  * @param {string} railContentId - The Railcontent ID of the current lesson.
1096
+ * @parent {boolean} addParent - Whether to include parent content data in the response.
1092
1097
  * @returns {Promise<Object|null>} - The fetched page data or null if found.
1093
1098
  *
1094
1099
  * @example
@@ -1096,44 +1101,51 @@ export async function jumpToContinueContent(railcontentId) {
1096
1101
  * .then(data => console.log(data))
1097
1102
  * .catch(error => console.error(error));
1098
1103
  */
1099
- export async function fetchLessonContent(railContentId) {
1104
+ export async function fetchLessonContent(railContentId, { addParent = false } = {}) {
1100
1105
  const filterParams = { isSingle: true, pullFutureContent: true }
1101
1106
 
1107
+ const parentQuery = addParent
1108
+ ? `"parent_content_data": *[railcontent_id in [...(^.parent_content_data[].id)]]{
1109
+ "id": railcontent_id,
1110
+ title,
1111
+ slug,
1112
+ "type": _type,
1113
+ "logo" : logo_image_url.asset->url,
1114
+ "dark_mode_logo": dark_mode_logo_url.asset->url,
1115
+ "light_mode_logo": light_mode_logo_url.asset->url,
1116
+ "badge": badge[0]->badge.asset->url,
1117
+ },`
1118
+ : ''
1119
+
1102
1120
  const fields = `${getFieldsForContentType()}
1103
- "resources": ${resourcesField},
1104
- soundslice,
1105
- instrumentless,
1106
- soundslice_slug,
1107
- "description": ${descriptionField},
1108
- "chapters": ${chapterField},
1109
- "instructors":instructor[]->name,
1110
- "instructor": ${instructorField},
1111
- ${assignmentsField}
1112
- video,
1113
- length_in_seconds,
1114
- mp3_no_drums_no_click_url,
1115
- mp3_no_drums_yes_click_url,
1116
- mp3_yes_drums_no_click_url,
1117
- mp3_yes_drums_yes_click_url,
1118
- "permission_id": permission[]->railcontent_id,
1119
- "parent_content_data": parent_content_data[]{
1120
- "id": id,
1121
- "title": *[railcontent_id == ^.id][0].title,
1122
- "slug":*[railcontent_id == ^.id][0].slug,
1123
- "type": *[railcontent_id == ^.id][0]._type,
1124
- "logo" : *[railcontent_id == ^.id][0].logo_image_url.asset->url,
1125
- "dark_mode_logo": *[railcontent_id == ^.id][0].dark_mode_logo_url.asset->url,
1126
- "light_mode_logo": *[railcontent_id == ^.id][0].light_mode_logo_url.asset->url,
1127
- },
1128
- ...select(
1129
- defined(live_event_start_time) => {
1130
- "live_event_start_time": live_event_start_time,
1131
- "live_event_end_time": live_event_end_time,
1132
- "live_event_youtube_id": live_event_youtube_id,
1133
- "videoId": coalesce(live_event_youtube_id, video.external_id),
1134
- "live_event_is_global": live_global_event == true
1135
- }
1136
- )`
1121
+ "resources": ${resourcesField},
1122
+ soundslice,
1123
+ instrumentless,
1124
+ soundslice_slug,
1125
+ "description": ${descriptionField},
1126
+ "chapters": ${chapterField},
1127
+ "instructors":instructor[]->name,
1128
+ "instructor": ${instructorField},
1129
+ ${assignmentsField}
1130
+ video,
1131
+ length_in_seconds,
1132
+ mp3_no_drums_no_click_url,
1133
+ mp3_no_drums_yes_click_url,
1134
+ mp3_yes_drums_no_click_url,
1135
+ mp3_yes_drums_yes_click_url,
1136
+ "permission_id": permission[]->railcontent_id,
1137
+ ${parentQuery}
1138
+ ...select(
1139
+ defined(live_event_start_time) => {
1140
+ "live_event_start_time": live_event_start_time,
1141
+ "live_event_end_time": live_event_end_time,
1142
+ "live_event_youtube_id": live_event_youtube_id,
1143
+ "videoId": coalesce(live_event_youtube_id, video.external_id),
1144
+ "live_event_is_global": live_global_event == true
1145
+ }
1146
+ )
1147
+ `
1148
+
1137
1149
  const query = await buildQuery(`railcontent_id == ${railContentId}`, filterParams, fields, {
1138
1150
  isSingle: true,
1139
1151
  })
@@ -1166,7 +1178,7 @@ export async function fetchLessonContent(railContentId) {
1166
1178
  export async function fetchRelatedRecommendedContent(railContentId, brand, count = 10) {
1167
1179
  const recommendedItems = await fetchSimilarItems(railContentId, brand, count)
1168
1180
  if (recommendedItems && recommendedItems.length > 0) {
1169
- return fetchByRailContentIds(recommendedItems)
1181
+ return fetchByRailContentIds(recommendedItems, 'tab-data', brand, true)
1170
1182
  }
1171
1183
 
1172
1184
  return await fetchRelatedLessons(railContentId, brand).then((result) =>
@@ -1239,20 +1251,26 @@ async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, cou
1239
1251
  * @param {string} brand - The current brand.
1240
1252
  * @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
1241
1253
  */
1242
- export async function fetchSiblingContent(railContentId, brand)
1254
+ export async function fetchSiblingContent(railContentId, brand= null)
1243
1255
  {
1244
1256
  const filterGetParent = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
1257
+ pullFutureContent: true
1258
+ }).buildFilter()
1259
+ const filterForParentList = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
1245
1260
  pullFutureContent: true,
1261
+ isParentFilter: true,
1246
1262
  }).buildFilter()
1263
+
1247
1264
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1248
1265
 
1266
+ const brandString = brand ? ` && brand == "${brand}"` : ''
1249
1267
  const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission[]->railcontent_id, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1250
1268
 
1251
- const query = `*[railcontent_id == ${railContentId} && brand == "${brand}"]{
1269
+ const query = `*[railcontent_id == ${railContentId}${brandString}]{
1252
1270
  _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
1253
1271
  'for-calculations': *[${filterGetParent}][0]{
1254
1272
  'siblings-list': child[]->railcontent_id,
1255
- 'parents-list': *[${filterGetParent}][0].child[]->railcontent_id
1273
+ 'parents-list': *[${filterForParentList}][0].child[]->railcontent_id
1256
1274
  },
1257
1275
  "related_lessons" : *[${filterGetParent}][0].child[${childrenFilter}]->{${queryFields}}
1258
1276
  }`
@@ -2157,7 +2175,6 @@ export async function fetchTabData(
2157
2175
  ) {
2158
2176
  const start = (page - 1) * limit
2159
2177
  const end = start + limit
2160
- let withoutPagination = false
2161
2178
  // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
2162
2179
  const includedFieldsFilter =
2163
2180
  includedFields.length > 0 ? filtersToGroq(includedFields, [], pageName) : ''
@@ -2168,27 +2185,19 @@ export async function fetchTabData(
2168
2185
  case 'recent':
2169
2186
  progressIds = await getAllStartedOrCompleted({ brand, onlyIds: true });
2170
2187
  sortOrder = null;
2171
- withoutPagination = true;
2172
2188
  break;
2173
2189
  case 'incomplete':
2174
2190
  progressIds = await getAllStarted();
2175
2191
  sortOrder = null;
2176
- withoutPagination = true;
2177
2192
  break;
2178
2193
  case 'completed':
2179
2194
  progressIds = await getAllCompleted();
2180
2195
  sortOrder = null;
2181
- withoutPagination = true;
2182
2196
  break;
2183
2197
  }
2184
2198
 
2185
2199
  // limits the results to supplied progressIds for started & completed filters
2186
2200
  const progressFilter = await getProgressFilter(progress, progressIds)
2187
- if (sort === "recommended"){
2188
- progressIds = await recommendations(brand);
2189
- withoutPagination = true;
2190
- }
2191
-
2192
2201
  const fieldsString = getFieldsForContentType('tab-data');
2193
2202
  const now = getSanityDate(new Date())
2194
2203
 
@@ -2202,7 +2211,7 @@ export async function fetchTabData(
2202
2211
  const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()
2203
2212
  entityFieldsString =
2204
2213
  ` ${fieldsString}
2205
- 'children': child[${childrenFilter}]->{'id': railcontent_id},
2214
+ 'children': child[${childrenFilter}]->{'id': railcontent_id, 'type': _type, brand, 'thumbnail': thumbnail.asset->url},
2206
2215
  'isLive': live_event_start_time <= "${now}" && live_event_end_time >= "${now}",
2207
2216
  'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
2208
2217
  'length_in_seconds': coalesce(
@@ -2217,13 +2226,12 @@ export async function fetchTabData(
2217
2226
  query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
2218
2227
  sortOrder: sortOrder,
2219
2228
  start: start,
2220
- end: end,
2221
- withoutPagination: withoutPagination,
2229
+ end: end
2222
2230
  })
2223
2231
 
2224
2232
  let results = await fetchSanity(query, true);
2225
2233
 
2226
- if ((['recent', 'incomplete', 'completed'].includes(progress) || sort == "recommended" )&& results.entity.length > 1) {
2234
+ if (['recent', 'incomplete', 'completed'].includes(progress) && results.entity.length > 1) {
2227
2235
  const orderMap = new Map(progressIds.map((id, index) => [id, index]))
2228
2236
  results.entity = results.entity
2229
2237
  .sort((a, b) => {
@@ -114,11 +114,33 @@ export async function deleteNotification(notificationId) {
114
114
  if (!notificationId) {
115
115
  throw new Error('notificationId is required')
116
116
  }
117
-
118
117
  const url = `${baseUrl}/v1/${notificationId}`
119
118
  return fetchHandler(url, 'delete')
120
119
  }
121
120
 
121
+ /**
122
+ * Restores a specific notification.
123
+ *
124
+ * @param {number} notificationId - The ID of the notification to restore.
125
+ *
126
+ * @returns {Promise<any>} - A promise that resolves when the notification is successfully restored.
127
+ *
128
+ * @throws {Error} - Throws an error if notificationId is not provided.
129
+ *
130
+ * @example
131
+ * restoreNotification(123)
132
+ * .then(response => console.log(response))
133
+ * .catch(error => console.error(error));
134
+ */
135
+ export async function restoreNotification(notificationId) {
136
+ if (!notificationId) {
137
+ throw new Error('notificationId is required')
138
+ }
139
+
140
+ const url = `${baseUrl}/v1/${notificationId}`
141
+ return fetchHandler(url, 'put')
142
+ }
143
+
122
144
  /**
123
145
  * Fetches the count of unread notifications for the current user in a given brand context.
124
146
  *
@@ -11,7 +11,7 @@ import {
11
11
  fetchRecentUserActivities,
12
12
  } from './railcontent'
13
13
  import { DataContext, UserActivityVersionKey } from './dataContext.js'
14
- import { fetchByRailContentIds, fetchShows } from './sanity'
14
+ import { fetchByRailContentId, fetchByRailContentIds, fetchShows } from './sanity'
15
15
  import { fetchPlaylist, fetchUserPlaylists } from './content-org/playlists'
16
16
  import { pinnedGuidedCourses } from './content-org/guided-courses'
17
17
  import {
@@ -930,6 +930,22 @@ export async function deleteUserActivity(id) {
930
930
  return await fetchHandler(url, 'DELETE')
931
931
  }
932
932
 
933
+ /**
934
+ * Restores a specific user activity by its ID.
935
+ *
936
+ * @param {number|string} id - The ID of the user activity to restore.
937
+ * @returns {Promise<Object>} - A promise that resolves to the API response after restoration.
938
+ *
939
+ * @example
940
+ * restoreUserActivity(789)
941
+ * .then(response => console.log('Restored:', response))
942
+ * .catch(error => console.error(error));
943
+ */
944
+ export async function restoreUserActivity(id) {
945
+ const url = `/api/user-management-system/v1/activities/${id}`
946
+ return await fetchHandler(url, 'POST')
947
+ }
948
+
933
949
  async function extractPinnedItemsAndSortAllItems(
934
950
  userPinnedItem,
935
951
  contentsMap,
@@ -1149,24 +1165,21 @@ async function processContentItem(content) {
1149
1165
  }
1150
1166
 
1151
1167
  return {
1152
- id: content.id,
1153
- progressType: 'content',
1154
- header: contentType,
1155
- pinned: content.pinned ?? false,
1156
- content: content,
1157
- body: {
1168
+ id: content.id,
1169
+ progressType: 'content',
1170
+ header: contentType,
1171
+ pinned: content.pinned ?? false,
1172
+ content: content,
1173
+ body: {
1158
1174
  progressPercent: isLive ? undefined : content.progressPercentage,
1159
- thumbnail: content.thumbnail,
1160
- title: content.title,
1161
- isLive: isLive,
1162
- badge: content.badge ?? null,
1163
- isLocked: content.is_locked ?? false,
1164
- subtitle:
1165
- !content.child_count || content.lesson_count === 1
1166
- ? contentType === 'lesson' && isLive === false
1167
- ? `${content.progressPercentage}% Complete`
1168
- : `${content.difficulty_string} • ${content.artist_name}`
1169
- : `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`,
1175
+ thumbnail: content.thumbnail,
1176
+ title: content.title,
1177
+ isLive: isLive,
1178
+ badge: content.badge ?? null,
1179
+ isLocked: content.is_locked ?? false,
1180
+ subtitle: collectionLessonTypes.includes(content.type) || content.lesson_count > 1
1181
+ ? `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
1182
+ : (contentType === 'lesson' && isLive === false) ? `${content.progressPercentage}% Complete`: `${content.difficulty_string} • ${content.artist_name}`
1170
1183
  },
1171
1184
  cta: {
1172
1185
  text: ctaText,