musora-content-services 2.92.3 → 2.92.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/src/index.d.ts +2 -0
  4. package/src/index.js +2 -0
  5. package/src/services/content-org/learning-paths.ts +3 -3
  6. package/src/services/progress-row/method-card.js +1 -1
  7. package/src/services/sync/fetch.ts +3 -0
  8. package/docs/Content.html +0 -269
  9. package/docs/ContentOrganization.html +0 -245
  10. package/docs/Forums.html +0 -269
  11. package/docs/Gamification.html +0 -245
  12. package/docs/TestUser.html +0 -260
  13. package/docs/UserManagementSystem.html +0 -317
  14. package/docs/api_types.js.html +0 -97
  15. package/docs/config.js.html +0 -140
  16. package/docs/content-org_content-org.js.html +0 -76
  17. package/docs/content-org_guided-courses.ts.html +0 -110
  18. package/docs/content-org_learning-paths.ts.html +0 -391
  19. package/docs/content-org_playlists-types.js.html +0 -128
  20. package/docs/content-org_playlists.js.html +0 -440
  21. package/docs/content.js.html +0 -603
  22. package/docs/content_artist.ts.html +0 -206
  23. package/docs/content_content.ts.html +0 -77
  24. package/docs/content_genre.ts.html +0 -209
  25. package/docs/content_instructor.ts.html +0 -206
  26. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  27. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  28. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  29. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  30. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  31. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  32. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  33. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -978
  36. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  39. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  40. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -1049
  41. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  42. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  43. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  44. package/docs/forums_categories.ts.html +0 -156
  45. package/docs/forums_discussions.js.html +0 -95
  46. package/docs/forums_forum.js.html +0 -95
  47. package/docs/forums_forums.ts.html +0 -160
  48. package/docs/forums_posts.ts.html +0 -284
  49. package/docs/forums_threads.ts.html +0 -284
  50. package/docs/gamification_awards.js.html +0 -165
  51. package/docs/gamification_awards.ts.html +0 -195
  52. package/docs/gamification_gamification.js.html +0 -76
  53. package/docs/gamification_types.js.html +0 -80
  54. package/docs/global.html +0 -6019
  55. package/docs/index.html +0 -167
  56. package/docs/liveTesting.ts.html +0 -103
  57. package/docs/module-Accounts.html +0 -2283
  58. package/docs/module-Artist.html +0 -993
  59. package/docs/module-Awards.html +0 -836
  60. package/docs/module-Categories.html +0 -711
  61. package/docs/module-Config.html +0 -431
  62. package/docs/module-Content-Services-V2.html +0 -2998
  63. package/docs/module-ForumCategories.html +0 -687
  64. package/docs/module-ForumDiscussions.html +0 -370
  65. package/docs/module-Forums.html +0 -16599
  66. package/docs/module-Genre.html +0 -981
  67. package/docs/module-GuidedCourses.html +0 -108
  68. package/docs/module-Instructor.html +0 -929
  69. package/docs/module-Interests.html +0 -1066
  70. package/docs/module-LearningPaths.html +0 -2424
  71. package/docs/module-Onboarding.html +0 -882
  72. package/docs/module-Payments.html +0 -392
  73. package/docs/module-Permissions.html +0 -406
  74. package/docs/module-Playlists.html +0 -3030
  75. package/docs/module-ProgressRow.html +0 -108
  76. package/docs/module-Railcontent-Services.html +0 -5876
  77. package/docs/module-Sanity-Services.html +0 -8244
  78. package/docs/module-Sessions.html +0 -575
  79. package/docs/module-Threads.html +0 -1119
  80. package/docs/module-UserActivity.html +0 -4534
  81. package/docs/module-UserChat.html +0 -410
  82. package/docs/module-UserManagement.html +0 -1932
  83. package/docs/module-UserMemberships.html +0 -829
  84. package/docs/module-UserNotifications.html +0 -2595
  85. package/docs/module-UserProfile.html +0 -370
  86. package/docs/progress-row_method-card.js.html +0 -184
  87. package/docs/railcontent.js.html +0 -724
  88. package/docs/sanity.js.html +0 -2322
  89. package/docs/scripts/collapse.js +0 -39
  90. package/docs/scripts/commonNav.js +0 -28
  91. package/docs/scripts/linenumber.js +0 -25
  92. package/docs/scripts/nav.js +0 -12
  93. package/docs/scripts/polyfill.js +0 -4
  94. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  95. package/docs/scripts/prettify/lang-css.js +0 -2
  96. package/docs/scripts/prettify/prettify.js +0 -28
  97. package/docs/scripts/search.js +0 -99
  98. package/docs/styles/jsdoc.css +0 -776
  99. package/docs/styles/prettify.css +0 -80
  100. package/docs/userActivity.js.html +0 -1512
  101. package/docs/user_account.ts.html +0 -265
  102. package/docs/user_chat.js.html +0 -98
  103. package/docs/user_interests.js.html +0 -150
  104. package/docs/user_management.js.html +0 -258
  105. package/docs/user_memberships.js.html +0 -144
  106. package/docs/user_memberships.ts.html +0 -292
  107. package/docs/user_notifications.js.html +0 -374
  108. package/docs/user_onboarding.ts.html +0 -329
  109. package/docs/user_payments.ts.html +0 -146
  110. package/docs/user_permissions.js.html +0 -110
  111. package/docs/user_profile.js.html +0 -115
  112. package/docs/user_sessions.js.html +0 -170
  113. package/docs/user_types.js.html +0 -224
  114. package/docs/user_user-management-system.js.html +0 -79
@@ -1,2322 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
-
5
- <meta charset="utf-8">
6
- <title>sanity.js - Documentation</title>
7
-
8
-
9
- <script src="scripts/prettify/prettify.js"></script>
10
- <script src="scripts/prettify/lang-css.js"></script>
11
- <!--[if lt IE 9]>
12
- <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
- <![endif]-->
14
- <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
- <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
- <script src="scripts/nav.js" defer></script>
17
-
18
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
- </head>
20
- <body>
21
-
22
- <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
- <label for="nav-trigger" class="navicon-button x">
24
- <div class="navicon"></div>
25
- </label>
26
-
27
- <label for="nav-trigger" class="overlay"></label>
28
-
29
- <nav >
30
-
31
-
32
- <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-Accounts.html">Accounts</a><ul class='methods'><li data-type='method'><a href="module-Accounts.html#~confirmEmailChange">confirmEmailChange</a></li><li data-type='method'><a href="module-Accounts.html#~deleteAccount">deleteAccount</a></li><li data-type='method'><a href="module-Accounts.html#~numberOfActiveUsers">numberOfActiveUsers</a></li><li data-type='method'><a href="module-Accounts.html#~requestEmailChange">requestEmailChange</a></li><li data-type='method'><a href="module-Accounts.html#~resetPassword">resetPassword</a></li><li data-type='method'><a href="module-Accounts.html#~sendAccountSetupEmail">sendAccountSetupEmail</a></li><li data-type='method'><a href="module-Accounts.html#~sendPasswordResetEmail">sendPasswordResetEmail</a></li><li data-type='method'><a href="module-Accounts.html#~setupAccount">setupAccount</a></li><li data-type='method'><a href="module-Accounts.html#~status">status</a></li><li data-type='method'><a href="module-Accounts.html#~toggleStudentView">toggleStudentView</a></li></ul></li><li><a href="module-Artist.html">Artist</a><ul class='methods'><li data-type='method'><a href="module-Artist.html#~fetchArtistBySlug">fetchArtistBySlug</a></li><li data-type='method'><a href="module-Artist.html#~fetchArtistLessons">fetchArtistLessons</a></li><li data-type='method'><a href="module-Artist.html#~fetchArtists">fetchArtists</a></li></ul></li><li><a href="module-Awards.html">Awards</a><ul class='methods'><li data-type='method'><a href="module-Awards.html#~fetchAwardsForUser">fetchAwardsForUser</a></li><li data-type='method'><a href="module-Awards.html#~fetchCertificate">fetchCertificate</a></li><li data-type='method'><a href="module-Awards.html#~getAwardDataForGuidedContent">getAwardDataForGuidedContent</a></li></ul></li><li><a href="module-Config.html">Config</a><ul class='methods'><li data-type='method'><a href="module-Config.html#.initializeService">initializeService</a></li></ul></li><li><a href="module-Content-Services-V2.html">Content-Services-V2</a><ul class='methods'><li data-type='method'><a href="module-Content-Services-V2.html#.getContentRows">getContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getLegacyMethods">getLegacyMethods</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getNewAndUpcoming">getNewAndUpcoming</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getOwnedContent">getOwnedContent</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecent">getRecent</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecommendedForYou">getRecommendedForYou</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getScheduleContentRows">getScheduleContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getTabResults">getTabResults</a></li></ul></li><li><a href="module-Forums.html">Forums</a><ul class='methods'><li data-type='method'><a href="module-Forums.html#~createForumCategory">createForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~createPost">createPost</a></li><li data-type='method'><a href="module-Forums.html#~createThread">createThread</a></li><li data-type='method'><a href="module-Forums.html#~deleteForumCategory">deleteForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~deletePost">deletePost</a></li><li data-type='method'><a href="module-Forums.html#~deleteThread">deleteThread</a></li><li data-type='method'><a href="module-Forums.html#~fetchCommunityGuidelines">fetchCommunityGuidelines</a></li><li data-type='method'><a href="module-Forums.html#~fetchFollowedThreads">fetchFollowedThreads</a></li><li data-type='method'><a href="module-Forums.html#~fetchForumCategories">fetchForumCategories</a></li><li data-type='method'><a href="module-Forums.html#~fetchLatestThreads">fetchLatestThreads</a></li><li data-type='method'><a href="module-Forums.html#~fetchPosts">fetchPosts</a></li><li data-type='method'><a href="module-Forums.html#~fetchThreads">fetchThreads</a></li><li data-type='method'><a href="module-Forums.html#~followThread">followThread</a></li><li data-type='method'><a href="module-Forums.html#~jumpToPost">jumpToPost</a></li><li data-type='method'><a href="module-Forums.html#~likePost">likePost</a></li><li data-type='method'><a href="module-Forums.html#~lockThread">lockThread</a></li><li data-type='method'><a href="module-Forums.html#~markThreadAsRead">markThreadAsRead</a></li><li data-type='method'><a href="module-Forums.html#~pinThread">pinThread</a></li><li data-type='method'><a href="module-Forums.html#~search">search</a></li><li data-type='method'><a href="module-Forums.html#~unfollowThread">unfollowThread</a></li><li data-type='method'><a href="module-Forums.html#~unlikePost">unlikePost</a></li><li data-type='method'><a href="module-Forums.html#~unlockThread">unlockThread</a></li><li data-type='method'><a href="module-Forums.html#~unpinThread">unpinThread</a></li><li data-type='method'><a href="module-Forums.html#~updateForumCategory">updateForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~updatePost">updatePost</a></li><li data-type='method'><a href="module-Forums.html#~updateThread">updateThread</a></li></ul></li><li></li><li></li><li><a href="module-Genre.html">Genre</a><ul class='methods'><li data-type='method'><a href="module-Genre.html#~fetchGenreBySlug">fetchGenreBySlug</a></li><li data-type='method'><a href="module-Genre.html#~fetchGenreLessons">fetchGenreLessons</a></li><li data-type='method'><a href="module-Genre.html#~fetchGenres">fetchGenres</a></li></ul></li><li><a href="module-GuidedCourses.html">GuidedCourses</a></li><li><a href="module-Instructor.html">Instructor</a><ul class='methods'><li data-type='method'><a href="module-Instructor.html#~fetchInstructorBySlug">fetchInstructorBySlug</a></li><li data-type='method'><a href="module-Instructor.html#~fetchInstructorLessons">fetchInstructorLessons</a></li><li data-type='method'><a href="module-Instructor.html#~fetchInstructors">fetchInstructors</a></li></ul></li><li><a href="module-Interests.html">Interests</a><ul class='methods'><li data-type='method'><a href="module-Interests.html#.fetchInterests">fetchInterests</a></li><li data-type='method'><a href="module-Interests.html#.fetchUninterests">fetchUninterests</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsInterested">markContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsNotInterested">markContentAsNotInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsInterested">removeContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsNotInterested">removeContentAsNotInterested</a></li></ul></li><li><a href="module-LearningPaths.html">LearningPaths</a><ul class='methods'><li data-type='method'><a href="module-LearningPaths.html#~completeLearningPathIntroVideo">completeLearningPathIntroVideo</a></li><li data-type='method'><a href="module-LearningPaths.html#~completeMethodIntroVideo">completeMethodIntroVideo</a></li><li data-type='method'><a href="module-LearningPaths.html#~fetchLearningPathLessons">fetchLearningPathLessons</a></li><li data-type='method'><a href="module-LearningPaths.html#~fetchLearningPathProgressCheckLessons">fetchLearningPathProgressCheckLessons</a></li><li data-type='method'><a href="module-LearningPaths.html#~getActivePath">getActivePath</a></li><li data-type='method'><a href="module-LearningPaths.html#~getDailySession">getDailySession</a></li><li data-type='method'><a href="module-LearningPaths.html#~getEnrichedLearningPath">getEnrichedLearningPath</a></li><li data-type='method'><a href="module-LearningPaths.html#~getLearningPathLessonsByIds">getLearningPathLessonsByIds</a></li><li data-type='method'><a href="module-LearningPaths.html#~mapContentToParent">mapContentToParent</a></li><li data-type='method'><a href="module-LearningPaths.html#~resetAllLearningPaths">resetAllLearningPaths</a></li><li data-type='method'><a href="module-LearningPaths.html#~startLearningPath">startLearningPath</a></li><li data-type='method'><a href="module-LearningPaths.html#~updateDailySession">updateDailySession</a></li></ul></li><li><a href="module-Onboarding.html">Onboarding</a><ul class='methods'><li data-type='method'><a href="module-Onboarding.html#~getOnboardingRecommendedContent">getOnboardingRecommendedContent</a></li><li data-type='method'><a href="module-Onboarding.html#~startOnboarding">startOnboarding</a></li><li data-type='method'><a href="module-Onboarding.html#~updateOnboarding">updateOnboarding</a></li><li data-type='method'><a href="module-Onboarding.html#~userOnboardingForBrand">userOnboardingForBrand</a></li></ul></li><li><a href="module-Payments.html">Payments</a><ul class='methods'><li data-type='method'><a href="module-Payments.html#~fetchCustomerPayments">fetchCustomerPayments</a></li></ul></li><li><a href="module-Permissions.html">Permissions</a><ul class='methods'><li data-type='method'><a href="module-Permissions.html#.fetchUserPermissions">fetchUserPermissions</a></li><li data-type='method'><a href="module-Permissions.html#.reset">reset</a></li></ul></li><li><a href="module-Playlists.html">Playlists</a><ul class='methods'><li data-type='method'><a href="module-Playlists.html#.addItemToPlaylist">addItemToPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.createPlaylist">createPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.deletePlaylist">deletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.duplicatePlaylist">duplicatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylist">fetchPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylistItems">fetchPlaylistItems</a></li><li data-type='method'><a href="module-Playlists.html#.fetchUserPlaylists">fetchUserPlaylists</a></li><li data-type='method'><a href="module-Playlists.html#.togglePlaylistPrivate">togglePlaylistPrivate</a></li><li data-type='method'><a href="module-Playlists.html#.undeletePlaylist">undeletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.updatePlaylist">updatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~deleteItemsFromPlaylist">deleteItemsFromPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~likePlaylist">likePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~reportPlaylist">reportPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~restoreItemFromPlaylist">restoreItemFromPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~unlikePlaylist">unlikePlaylist</a></li></ul></li><li><a href="module-ProgressRow.html">ProgressRow</a></li><li><a href="module-Railcontent-Services.html">Railcontent-Services</a><ul class='methods'><li data-type='method'><a href="module-Railcontent-Services.html#.assignModeratorToComment">assignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.closeComment">closeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.createComment">createComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.deleteComment">deleteComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.editComment">editComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchAllCompletedStates">fetchAllCompletedStates</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCarouselCardData">fetchCarouselCardData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchComment">fetchComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCommentRelies">fetchCommentRelies</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchComments">fetchComments</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedContent">fetchCompletedContent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedState">fetchCompletedState</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentInProgress">fetchContentInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentPageUserData">fetchContentPageUserData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchNextContentDataForParent">fetchNextContentDataForParent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchRecentUserActivities">fetchRecentUserActivities</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchSongsInProgress">fetchSongsInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchTopComment">fetchTopComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserAward">fetchUserAward</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserBadges">fetchUserBadges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserPracticeNotes">fetchUserPracticeNotes</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.likeComment">likeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.openComment">openComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.replyToComment">replyToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.reportComment">reportComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.restoreComment">restoreComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.setStudentViewForUser">setStudentViewForUser</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unassignModeratorToComment">unassignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unlikeComment">unlikeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#~fetchLastInteractedChild">fetchLastInteractedChild</a></li></ul></li><li><a href="module-Sanity-Services.html">Sanity-Services</a><ul class='methods'><li data-type='method'><a href="module-Sanity-Services.html#.fetchAll">fetchAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllFilterOptions">fetchAllFilterOptions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllPacks">fetchAllPacks</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentId">fetchByRailContentId</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentIds">fetchByRailContentIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByReference">fetchByReference</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchComingSoon">fetchComingSoon</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchCommentModContentData">fetchCommentModContentData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchFoundation">fetchFoundation</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLeaving">fetchLeaving</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonContent">fetchLessonContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonsFeaturingThisContent">fetchLessonsFeaturingThisContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMetadata">fetchMetadata</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethod">fetchMethod</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildren">fetchMethodChildren</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildrenIds">fetchMethodChildrenIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodPreviousNextLesson">fetchMethodPreviousNextLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodV2IntroVideo">fetchMethodV2IntroVideo</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodV2Structure">fetchMethodV2Structure</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodV2StructureFromId">fetchMethodV2StructureFromId</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNewReleases">fetchNewReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNextPreviousLesson">fetchNextPreviousLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchOtherSongVersions">fetchOtherSongVersions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchOwnedContent">fetchOwnedContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackAll">fetchPackAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackData">fetchPackData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedLessons">fetchRelatedLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedRecommendedContent">fetchRelatedRecommendedContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedSongs">fetchRelatedSongs</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchReturning">fetchReturning</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSanity">fetchSanity</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchScheduledReleases">fetchScheduledReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchShowsData">fetchShowsData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSiblingContent">fetchSiblingContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongArtistCount">fetchSongArtistCount</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongById">fetchSongById</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchTopLevelParentId">fetchTopLevelParentId</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchUpcomingEvents">fetchUpcomingEvents</a></li><li data-type='method'><a href="module-Sanity-Services.html#.jumpToContinueContent">jumpToContinueContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#~fetchRelatedByLicense">fetchRelatedByLicense</a></li><li data-type='method'><a href="module-Sanity-Services.html#~getQueryFromPage">getQueryFromPage</a></li></ul></li><li><a href="module-Sessions.html">Sessions</a><ul class='methods'><li data-type='method'><a href="module-Sessions.html#.login">login</a></li><li data-type='method'><a href="module-Sessions.html#.logout">logout</a></li></ul></li><li><a href="module-UserActivity.html">UserActivity</a><ul class='methods'><li data-type='method'><a href="module-UserActivity.html#.calculateLongestStreaks">calculateLongestStreaks</a></li><li data-type='method'><a href="module-UserActivity.html#.createPracticeNotes">createPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.deletePracticeSession">deletePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.deleteUserActivity">deleteUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeNotes">getPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeSessions">getPracticeSessions</a></li><li data-type='method'><a href="module-UserActivity.html#.getProgressRows">getProgressRows</a></li><li data-type='method'><a href="module-UserActivity.html#.getRecentActivity">getRecentActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserMonthlyStats">getUserMonthlyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserWeeklyStats">getUserWeeklyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.pinProgressRow">pinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserActivity">recordUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserPractice">recordUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.removeUserPractice">removeUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.restorePracticeSession">restorePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.restoreUserActivity">restoreUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.restoreUserPractice">restoreUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.unpinProgressRow">unpinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.updatePracticeNotes">updatePracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.updateUserPractice">updateUserPractice</a></li></ul></li><li><a href="module-UserChat.html">UserChat</a><ul class='methods'><li data-type='method'><a href="module-UserChat.html#.fetchChatSettings">fetchChatSettings</a></li></ul></li><li><a href="module-UserManagement.html">UserManagement</a><ul class='methods'><li data-type='method'><a href="module-UserManagement.html#.blockUser">blockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.blockedUsers">blockedUsers</a></li><li data-type='method'><a href="module-UserManagement.html#.deletePicture">deletePicture</a></li><li data-type='method'><a href="module-UserManagement.html#.getUserData">getUserData</a></li><li data-type='method'><a href="module-UserManagement.html#.getUserSignature">getUserSignature</a></li><li data-type='method'><a href="module-UserManagement.html#.isUsernameAvailable">isUsernameAvailable</a></li><li data-type='method'><a href="module-UserManagement.html#.setUserSignature">setUserSignature</a></li><li data-type='method'><a href="module-UserManagement.html#.toggleSignaturePrivate">toggleSignaturePrivate</a></li><li data-type='method'><a href="module-UserManagement.html#.unblockUser">unblockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.updateDisplayName">updateDisplayName</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPicture">uploadPicture</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPictureFromS3">uploadPictureFromS3</a></li></ul></li><li><a href="module-UserMemberships.html">UserMemberships</a><ul class='methods'><li data-type='method'><a href="module-UserMemberships.html#~fetchMemberships">fetchMemberships</a></li><li data-type='method'><a href="module-UserMemberships.html#~fetchRechargeTokens">fetchRechargeTokens</a></li><li data-type='method'><a href="module-UserMemberships.html#~restorePurchases">restorePurchases</a></li><li data-type='method'><a href="module-UserMemberships.html#~upgradeSubscription">upgradeSubscription</a></li></ul></li><li><a href="module-UserNotifications.html">UserNotifications</a><ul class='methods'><li data-type='method'><a href="module-UserNotifications.html#.deleteNotification">deleteNotification</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchLiveEventPollingState">fetchLiveEventPollingState</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotificationSettings">fetchNotificationSettings</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotifications">fetchNotifications</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchUnreadCount">fetchUnreadCount</a></li><li data-type='method'><a href="module-UserNotifications.html#.markAllNotificationsAsRead">markAllNotificationsAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsRead">markNotificationAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsUnread">markNotificationAsUnread</a></li><li data-type='method'><a href="module-UserNotifications.html#.pauseLiveEventPolling">pauseLiveEventPolling</a></li><li data-type='method'><a href="module-UserNotifications.html#.restoreNotification">restoreNotification</a></li><li data-type='method'><a href="module-UserNotifications.html#.startLiveEventPolling">startLiveEventPolling</a></li><li data-type='method'><a href="module-UserNotifications.html#.updateNotificationSetting">updateNotificationSetting</a></li></ul></li><li><a href="module-UserProfile.html">UserProfile</a><ul class='methods'><li data-type='method'><a href="module-UserProfile.html#.deleteProfilePicture">deleteProfilePicture</a></li><li data-type='method'><a href="module-UserProfile.html#.otherStats">otherStats</a></li></ul></li></ul><h3>Namespaces</h3><ul><li><a href="ContentOrganization.html">ContentOrganization</a></li><li><a href="Forums.html">Forums</a></li><li><a href="Gamification.html">Gamification</a></li><li><a href="UserManagementSystem.html">UserManagementSystem</a></li></ul><h3>Interfaces</h3><ul><li><a href="TestUser.html">TestUser</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createTestUser">createTestUser</a></li></ul>
33
-
34
- </nav>
35
-
36
- <div id="main">
37
-
38
- <h1 class="page-title">sanity.js</h1>
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
- <section>
47
- <article>
48
- <pre class="prettyprint source linenums"><code>/**
49
- * @module Sanity-Services
50
- */
51
- import {
52
- artistOrInstructorName,
53
- instructorField,
54
- chapterField,
55
- assignmentsField,
56
- descriptionField,
57
- resourcesField,
58
- contentTypeConfig,
59
- getIntroVideoFields,
60
- DEFAULT_FIELDS,
61
- getFieldsForContentType,
62
- filtersToGroq,
63
- getUpcomingEventsTypes,
64
- showsTypes,
65
- getNewReleasesTypes,
66
- coachLessonsTypes,
67
- getFieldsForContentTypeWithFilteredChildren,
68
- getChildFieldsForContentType,
69
- SONG_TYPES,
70
- SONG_TYPES_WITH_CHILDREN,
71
- } from '../contentTypeConfig.js'
72
- import { fetchSimilarItems, recommendations } from './recommendations.js'
73
- import { processMetadata, typeWithSortOrder } from '../contentMetaData.js'
74
-
75
- import { globalConfig } from './config.js'
76
-
77
- import { fetchNextContentDataForParent, fetchHandler } from './railcontent.js'
78
- import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
79
- import { getPermissionsAdapter } from './permissions/index.ts'
80
- import { getAllCompleted, getAllStarted, getAllStartedOrCompleted } from './contentProgress.js'
81
- import { fetchRecentActivitiesActiveTabs } from './userActivity.js'
82
-
83
- /**
84
- * Exported functions that are excluded from index generation.
85
- *
86
- * @type {string[]}
87
- */
88
- const excludeFromGeneratedIndex = ['fetchRelatedByLicense']
89
-
90
- /**
91
- * Fetch a song by its document ID from Sanity.
92
- *
93
- * @param {string} documentId - The ID of the document to fetch.
94
- * @returns {Promise&lt;Object|null>} - A promise that resolves to an object containing the song data or null if not found.
95
- *
96
- * @example
97
- * fetchSongById('abc123')
98
- * .then(song => console.log(song))
99
- * .catch(error => console.error(error));
100
- */
101
- export async function fetchSongById(documentId) {
102
- const fields = getFieldsForContentType('song')
103
- const filterParams = {}
104
- const query = await buildQuery(
105
- `_type == "song" &amp;&amp; railcontent_id == ${documentId}`,
106
- filterParams,
107
- fields,
108
- {
109
- isSingle: true,
110
- }
111
- )
112
- return fetchSanity(query, false)
113
- }
114
-
115
- /**
116
- * fetches from Sanity all content marked for removal next quarter
117
- *
118
- * @string brand
119
- * @number pageNumber
120
- * @number contentPerPage
121
- * @returns {Promise&lt;Object|null>}
122
- */
123
- export async function fetchLeaving(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
124
- const today = new Date()
125
- const isoDateOnly = getDateOnly(today)
126
- const filterString = `brand == '${brand}' &amp;&amp; quarter_removed > '${isoDateOnly}'`
127
- const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
128
- const sortOrder = {
129
- sortOrder: 'quarter_removed asc, published_on desc, id desc',
130
- start: startEndOrder['start'],
131
- end: startEndOrder['end'],
132
- }
133
- const query = await buildQuery(
134
- filterString,
135
- { pullFutureContent: false, availableContentStatuses: ['published'] },
136
- getFieldsForContentType('leaving'),
137
- sortOrder
138
- )
139
- return fetchSanity(query, true)
140
- }
141
-
142
- /**
143
- * fetches from Sanity all content marked for return next quarter
144
- *
145
- * @string brand
146
- * @number pageNumber
147
- * @number contentPerPage
148
- * @returns {Promise&lt;Object|null>}
149
- */
150
- export async function fetchReturning(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
151
- const today = new Date()
152
- const isoDateOnly = getDateOnly(today)
153
- const filterString = `brand == '${brand}' &amp;&amp; quarter_published >= '${isoDateOnly}'`
154
- const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
155
- const sortOrder = {
156
- sortOrder: 'quarter_published asc, published_on desc, id desc',
157
- start: startEndOrder['start'],
158
- end: startEndOrder['end'],
159
- }
160
- const query = await buildQuery(
161
- filterString,
162
- { pullFutureContent: true, availableContentStatuses: ['draft'] },
163
- getFieldsForContentType('returning'),
164
- sortOrder
165
- )
166
-
167
- return fetchSanity(query, true)
168
- }
169
-
170
- /**
171
- * fetches from Sanity all songs coming soon (new) next quarter
172
- *
173
- * @string brand
174
- * @number pageNumber
175
- * @number contentPerPage
176
- * @returns {Promise&lt;Object|null>}
177
- */
178
- export async function fetchComingSoon(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
179
- const filterString = `brand == '${brand}' &amp;&amp; _type == 'song'`
180
- const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
181
- const sortOrder = {
182
- sortOrder: 'published_on desc, id desc',
183
- start: startEndOrder['start'],
184
- end: startEndOrder['end'],
185
- }
186
- const query = await buildQuery(
187
- filterString,
188
- { getFutureContentOnly: true },
189
- getFieldsForContentType(),
190
- sortOrder
191
- )
192
- return fetchSanity(query, true)
193
- }
194
-
195
- /**
196
- *
197
- * @number page
198
- * @returns {number[]}
199
- */
200
- function getQueryFromPage(pageNumber, contentPerPage) {
201
- const start = contentPerPage * (pageNumber - 1)
202
- const end = contentPerPage * pageNumber
203
- let result = []
204
- result['start'] = start
205
- result['end'] = end
206
- return result
207
- }
208
-
209
- /**
210
- * Fetch current number of artists for songs within a brand.
211
- * @param {string} brand - The current brand.
212
- * @returns {Promise&lt;int|null>} - The fetched count of artists.
213
- */
214
- export async function fetchSongArtistCount(brand) {
215
- const filter = await new FilterBuilder(
216
- `_type == "song" &amp;&amp; brand == "${brand}" &amp;&amp; references(^._id)`,
217
- { bypassPermissions: true }
218
- ).buildFilter()
219
- const query = `
220
- count(*[_type == "artist"]{
221
- name,
222
- "lessonsCount": count(*[${filter}])
223
- }[lessonsCount > 0])`
224
- return fetchSanity(query, true, { processNeedAccess: false })
225
- }
226
-
227
- export async function fetchPlayAlongsCount(
228
- brand,
229
- { searchTerm, includedFields, progressIds, progress }
230
- ) {
231
- const searchFilter = searchTerm
232
- ? `&amp;&amp; (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*" || name match "${searchTerm}*")`
233
- : ''
234
-
235
- // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
236
- const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
237
-
238
- // limits the results to supplied progressIds for started &amp; completed filters
239
- const progressFilter = await getProgressFilter(progress, progressIds)
240
- const query = `count(*[brand == '${brand}' &amp;&amp; _type == "play-along" ${searchFilter} ${includedFieldsFilter} ${progressFilter} ]) `
241
- return fetchSanity(query, true, { processNeedAccess: false })
242
- }
243
-
244
- /**
245
- * Fetch related songs for a specific brand and song ID.
246
- *
247
- * @param {string} brand - The brand for which to fetch related songs.
248
- * @param {string} songId - The ID of the song to find related songs for.
249
- * @returns {Promise&lt;Object|null>} - A promise that resolves to an array of related song objects or null if not found.
250
- *
251
- * @example
252
- * fetchRelatedSongs('drumeo', '12345')
253
- * .then(relatedSongs => console.log(relatedSongs))
254
- * .catch(error => console.error(error));
255
- */
256
- export async function fetchRelatedSongs(brand, songId) {
257
- const now = getSanityDate(new Date())
258
- const query = `
259
- *[_type == "song" &amp;&amp; railcontent_id == ${songId}]{
260
- "entity": array::unique([
261
- ...(*[_type == "song" &amp;&amp; brand == "${brand}" &amp;&amp; railcontent_id != ${songId} &amp;&amp; references(^.artist->_id)
262
- &amp;&amp; (status in ['published'] || (status == 'scheduled' &amp;&amp; defined(published_on) &amp;&amp; published_on >= '${now}'))]{
263
- "type": _type,
264
- "id": railcontent_id,
265
- "url": web_url_path,
266
- "published_on": published_on,
267
- status,
268
- "image": thumbnail.asset->url,
269
- "permission_id": permission[]->railcontent_id,
270
- "fields": [
271
- {
272
- "key": "title",
273
- "value": title
274
- },
275
- {
276
- "key": "artist",
277
- "value": artist->name
278
- },
279
- {
280
- "key": "difficulty",
281
- "value": difficulty
282
- },
283
- {
284
- "key": "length_in_seconds",
285
- "value": soundslice[0].soundslice_length_in_second
286
- }
287
- ],
288
- }[0...10]),
289
- ...(*[_type == "song" &amp;&amp; brand == "${brand}" &amp;&amp; railcontent_id != ${songId} &amp;&amp; references(^.genre[]->_id)
290
- &amp;&amp; (status in ['published'] || (status == 'scheduled' &amp;&amp; defined(published_on) &amp;&amp; published_on >= '${now}'))]{
291
- "type": _type,
292
- "id": railcontent_id,
293
- "url": web_url_path,
294
- "published_on": published_on,
295
- "permission_id": permission[]->railcontent_id,
296
- status,
297
- "fields": [
298
- {
299
- "key": "title",
300
- "value": title
301
- },
302
- {
303
- "key": "artist",
304
- "value": artist->name
305
- },
306
- {
307
- "key": "difficulty",
308
- "value": difficulty
309
- },
310
- {
311
- "key": "length_in_seconds",
312
- "value": soundslice[0].soundslice_length_in_second
313
- }
314
- ],
315
- "data": [{
316
- "key": "thumbnail_url",
317
- "value": thumbnail.asset->url
318
- }]
319
- }[0...10])
320
- ])[0...10]
321
- }`
322
-
323
- // Fetch the related songs data
324
- return fetchSanity(query, false)
325
- }
326
-
327
- /**
328
- * Fetch the latest new releases for a specific brand.
329
- * @param {string} brand - The brand for which to fetch new releases.
330
- * @returns {Promise&lt;Object|null>} - The fetched new releases data or null if not found.
331
- */
332
- export async function fetchNewReleases(
333
- brand,
334
- { page = 1, limit = 20, sort = '-published_on' } = {}
335
- ) {
336
- const newTypes = getNewReleasesTypes(brand)
337
- const typesString = arrayToStringRepresentation(newTypes)
338
- const start = (page - 1) * limit
339
- const end = start + limit
340
- const sortOrder = getSortOrder(sort, brand)
341
- const now = getDateOnly()
342
- const filter = `_type in ${typesString} &amp;&amp; brand == '${brand}' &amp;&amp; (status == 'published' &amp;&amp; show_in_new_feed == true &amp;&amp; published_on &lt;= '${now}')`
343
- const fields = `
344
- "id": railcontent_id,
345
- title,
346
- "image": thumbnail.asset->url,
347
- "thumbnail": thumbnail.asset->url,
348
- ${artistOrInstructorName()},
349
- "artists": instructor[]->name,
350
- difficulty,
351
- difficulty_string,
352
- length_in_seconds,
353
- published_on,
354
- "type": _type,
355
- web_url_path,
356
- "permission_id": permission[]->railcontent_id,
357
- `
358
- const query = buildRawQuery(filter, fields, { sortOrder: sortOrder, start, end: end })
359
- return fetchSanity(query, true)
360
- }
361
-
362
- /**
363
- * Fetch upcoming events for a specific brand.
364
- *
365
- * @param {string} brand - The brand for which to fetch upcoming events.
366
- * @returns {Promise&lt;Object|null>} - A promise that resolves to an array of upcoming event objects or null if not found.
367
- *
368
- * @example
369
- * fetchUpcomingEvents('drumeo', {
370
- * page: 2,
371
- * limit: 20,
372
- * })
373
- * .then(events => console.log(events))
374
- * .catch(error => console.error(error));
375
- */
376
- export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {}) {
377
- const now = getSanityDate(new Date())
378
- const start = (page - 1) * limit
379
- const end = start + limit
380
- const fields = `
381
- "id": railcontent_id,
382
- title,
383
- "image": thumbnail.asset->url,
384
- "thumbnail": thumbnail.asset->url,
385
- ${artistOrInstructorName()},
386
- "artists": instructor[]->name,
387
- difficulty,
388
- difficulty_string,
389
- length_in_seconds,
390
- published_on,
391
- "type": _type,
392
- web_url_path,
393
- "permission_id": permission[]->railcontent_id,
394
- live_event_start_time,
395
- live_event_end_time,
396
- "isLive": live_event_start_time &lt;= '${now}' &amp;&amp; live_event_end_time >= '${now}'`
397
- const query = buildRawQuery(
398
- `defined(live_event_start_time) &amp;&amp; live_event_start_time >= '${now}' &amp;&amp; (!defined(live_event_end_time) || live_event_end_time >= '${now}' ) &amp;&amp; brand == '${brand}' &amp;&amp; status == 'scheduled'`,
399
- fields,
400
- {
401
- sortOrder: 'published_on asc',
402
- start: start,
403
- end: end,
404
- }
405
- )
406
- return fetchSanity(query, true)
407
- }
408
-
409
- /**
410
- * Fetch scheduled releases for a specific brand.
411
- *
412
- * @param {string} brand - The brand for which to fetch scheduled releasess.
413
- * @returns {Promise&lt;Object|null>} - A promise that resolves to an array of scheduled release objects or null if not found.
414
- *
415
- * @example
416
- * fetchScheduledReleases('drumeo', {
417
- * page: 2,
418
- * limit: 20,
419
- * })
420
- * .then(content => console.log(content))
421
- * .catch(error => console.error(error));
422
- */
423
- export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
424
- const upcomingTypes = getUpcomingEventsTypes(brand)
425
- const newTypes = getNewReleasesTypes(brand)
426
-
427
- const scheduledTypes = merge(upcomingTypes, newTypes)
428
- const typesString = arrayJoinWithQuotes(scheduledTypes)
429
- const now = getSanityDate(new Date())
430
- const start = (page - 1) * limit
431
- const end = start + limit
432
- const query = `*[_type in [${typesString}] &amp;&amp; brand == '${brand}' &amp;&amp; status in ['published','scheduled'] &amp;&amp; (!defined(live_event_end_time) || live_event_end_time &lt; '${now}' ) &amp;&amp; published_on > '${now}']{
433
- "id": railcontent_id,
434
- title,
435
- "image": thumbnail.asset->url,
436
- "thumbnail": thumbnail.asset->url,
437
- ${artistOrInstructorName()},
438
- "artists": instructor[]->name,
439
- difficulty,
440
- difficulty_string,
441
- length_in_seconds,
442
- published_on,
443
- "type": _type,
444
- web_url_path,
445
- "permission_id": permission[]->railcontent_id,
446
- } | order(published_on asc)[${start}...${end}]`
447
- return fetchSanity(query, true)
448
- }
449
-
450
- /**
451
- * Fetch content by a specific Railcontent ID.
452
- *
453
- * @param {string} id - The Railcontent ID of the content to fetch.
454
- * @param {string} contentType - The document type of content to fetch
455
- * @returns {Promise&lt;Object|null>} - A promise that resolves to the content object or null if not found.
456
- *
457
- * @example
458
- * fetchByRailContentId('abc123')
459
- * .then(content => console.log(content))
460
- * .catch(error => console.error(error));
461
- */
462
- export async function fetchByRailContentId(id, contentType) {
463
- const fields = await getFieldsForContentTypeWithFilteredChildren(contentType, true)
464
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
465
- const entityFieldsString = ` ${fields}
466
- 'child_count': coalesce(count(child[${childrenFilter}]->), 0) ,
467
- 'length_in_seconds': coalesce(
468
- math::sum(
469
- select(
470
- child[${childrenFilter}]->length_in_seconds
471
- )
472
- ),
473
- length_in_seconds
474
- ),`
475
-
476
- const query = buildRawQuery(
477
- `railcontent_id == ${id} &amp;&amp; _type == '${contentType}'`,
478
- entityFieldsString,
479
- {
480
- isSingle: true,
481
- }
482
- )
483
-
484
- return fetchSanity(query, false)
485
- }
486
-
487
- /**
488
- * Fetch content by an array of Railcontent IDs.
489
- *
490
- * @param {Array&lt;string|number>} ids - The array of Railcontent IDs of the content to fetch.
491
- * @param {string} [contentType] - The content type the IDs to add needed fields to the response.
492
- * @returns {Promise&lt;Array&lt;Object>|null>} - A promise that resolves to an array of content objects or null if not found.
493
- *
494
- * @example
495
- * fetchByRailContentIds(['abc123', 'def456', 'ghi789'])
496
- * .then(contents => console.log(contents))
497
- * .catch(error => console.error(error));
498
- */
499
- export async function fetchByRailContentIds(
500
- ids,
501
- contentType = undefined,
502
- brand = undefined,
503
- includePermissionsAndStatusFilter = false
504
- ) {
505
- if (!ids?.length) {
506
- return []
507
- }
508
- ids = [...new Set(ids.filter((item) => item !== null &amp;&amp; item !== undefined))]
509
- const idsString = ids.join(',')
510
- const brandFilter = brand ? ` &amp;&amp; brand == "${brand}"` : ''
511
- const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {
512
- pullFutureContent: true,
513
- }).buildFilter()
514
- const fields = await getFieldsForContentTypeWithFilteredChildren(contentType, true)
515
- const baseFilter = `railcontent_id in [${idsString}]${brandFilter}`
516
- const finalFilter = includePermissionsAndStatusFilter
517
- ? await new FilterBuilder(baseFilter).buildFilter()
518
- : baseFilter
519
- const query = `*[
520
- ${finalFilter}
521
- ]{
522
- ${fields}
523
- 'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
524
- live_event_start_time,
525
- live_event_end_time,
526
- }`
527
-
528
- const customPostProcess = (results) => {
529
- const now = getSanityDate(new Date(), false)
530
- const liveProcess = (result) => {
531
- if (result.live_event_start_time &amp;&amp; result.live_event_end_time) {
532
- result.isLive = result.live_event_start_time &lt;= now &amp;&amp; result.live_event_end_time >= now
533
- } else {
534
- result.isLive = false
535
- }
536
- return result
537
- }
538
- return results.map(liveProcess)
539
- }
540
- const results = await fetchSanity(query, true, {
541
- customPostProcess: customPostProcess,
542
- processNeedAccess: true,
543
- })
544
-
545
- const sortFuction = function compare(a, b) {
546
- const indexA = ids.indexOf(a['id'])
547
- const indexB = ids.indexOf(b['id'])
548
- if (indexA === indexB) return 0
549
- if (indexA > indexB) return 1
550
- return -1
551
- }
552
-
553
- // Sort results to match the order of the input IDs
554
- const sortedResults = results?.sort(sortFuction) ?? null
555
-
556
- return sortedResults
557
- }
558
-
559
- export async function fetchContentRows(brand, pageName, contentRowSlug) {
560
- if (pageName === 'lessons') pageName = 'lesson'
561
- if (pageName === 'songs') pageName = 'song'
562
- const rowString = contentRowSlug ? ` &amp;&amp; slug.current == "${contentRowSlug.toLowerCase()}"` : ''
563
- const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {
564
- pullFutureContent: true,
565
- showMembershipRestrictedContent: true,
566
- }).buildFilter()
567
- const childFilter = await new FilterBuilder('', {
568
- isChildrenFilter: true,
569
- showMembershipRestrictedContent: true,
570
- }).buildFilter()
571
- const query = `*[_type == 'recommended-content-row' &amp;&amp; brand == '${brand}' &amp;&amp; type == '${pageName}'${rowString}]{
572
- brand,
573
- name,
574
- 'slug': slug.current,
575
- 'content': content[${childFilter}]->{
576
- 'children': child[${childFilter}]->{ 'id': railcontent_id,
577
- 'type': _type, brand, 'thumbnail': thumbnail.asset->url,
578
- 'children': child[${childFilter}]->{'id': railcontent_id}, },
579
- ${getFieldsForContentType('tab-data')}
580
- 'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
581
- },
582
- }`
583
- return fetchSanity(query, true, { processNeedAccess: true })
584
- }
585
-
586
- /**
587
- * Fetch all content for a specific brand and type with pagination, search, and grouping options.
588
- * @param {string} brand - The brand for which to fetch content.
589
- * @param {string} type - The content type to fetch (e.g., 'song', 'artist').
590
- * @param {Object} params - Parameters for pagination, filtering, sorting, and grouping.
591
- * @param {number} [params.page=1] - The page number for pagination.
592
- * @param {number} [params.limit=10] - The number of items per page.
593
- * @param {string} [params.searchTerm=""] - The search term to filter content by title or artist.
594
- * @param {string} [params.sort="-published_on"] - The field to sort the content by.
595
- * @param {Array&lt;string>} [params.includedFields=[]] - The fields to include in the query.
596
- * @param {string} [params.groupBy=""] - The field to group the results by (e.g., 'artist', 'genre').
597
- * @param {Array&lt;string>} [params.progressIds=undefined] - An array of railcontent IDs to filter the results by. Used for filtering by progress.
598
- * @param {boolean} [params.useDefaultFields=true] - use the default sanity fields for content Type
599
- * @param {Array&lt;string>} [params.customFields=[]] - An array of sanity fields to include in the request
600
- * @param {string} [params.progress="all"] - An string representing which progress filter to use ("all", "in progress", "complete", "not started").
601
- * @returns {Promise&lt;Object|null>} - The fetched content data or null if not found.
602
- *
603
- * @example
604
- * fetchAll('drumeo', 'song', {
605
- * page: 2,
606
- * limit: 20,
607
- * searchTerm: 'jazz',
608
- * sort: '-popularity',
609
- * includedFields: ['difficulty,Intermediate'],
610
- * groupBy: 'artist',
611
- * progressIds: [123, 321],
612
- * useDefaultFields: false,
613
- * customFields: ['is_house_coach', 'slug.current', "'instructors': instructor[]->name"],
614
- * })
615
- * .then(content => console.log(content))
616
- * .catch(error => console.error(error));
617
- */
618
- export async function fetchAll(
619
- brand,
620
- type,
621
- {
622
- page = 1,
623
- limit = 10,
624
- searchTerm = '',
625
- sort = '-published_on',
626
- includedFields = [],
627
- groupBy = '',
628
- progressIds = undefined,
629
- useDefaultFields = true,
630
- customFields = [],
631
- progress = 'all',
632
- } = {}
633
- ) {
634
- let config = contentTypeConfig[type] ?? {}
635
- let additionalFields = config?.fields ?? []
636
- let isGroupByOneToOne = (groupBy ? config?.relationships?.[groupBy]?.isOneToOne : false) ?? false
637
- let webUrlPathType = config?.slug ?? type
638
- const start = (page - 1) * limit
639
- const end = start + limit
640
- let bypassStatusAndPublishedValidation =
641
- type == 'instructor' || groupBy == 'artist' || groupBy == 'genre' || groupBy == 'instructor'
642
- let bypassPermissions = bypassStatusAndPublishedValidation
643
- // Construct the type filter
644
- let typeFilter
645
-
646
- if (type === 'archives') {
647
- typeFilter = `&amp;&amp; status == "archived"`
648
- bypassStatusAndPublishedValidation = true
649
- } else if (type === 'lessons' || type === 'songs') {
650
- typeFilter = ``
651
- } else if (type === 'pack') {
652
- typeFilter = `&amp;&amp; (_type == 'pack' || _type == 'semester-pack')`
653
- } else {
654
- typeFilter = type ? `&amp;&amp; _type == '${type}'` : ''
655
- }
656
-
657
- // Construct the search filter
658
- const searchFilter = searchTerm
659
- ? groupBy !== ''
660
- ? `&amp;&amp; (^.name match "${searchTerm}*" || title match "${searchTerm}*")`
661
- : `&amp;&amp; (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*" || name match "${searchTerm}*")`
662
- : ''
663
-
664
- // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
665
- const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
666
-
667
- // limits the results to supplied progressIds for started &amp; completed filters
668
- const progressFilter = await getProgressFilter(progress, progressIds)
669
-
670
- // Determine the sort order
671
- const sortOrder = getSortOrder(sort, brand, groupBy)
672
-
673
- let fields = useDefaultFields
674
- ? customFields.concat(DEFAULT_FIELDS, additionalFields)
675
- : customFields
676
- let fieldsString = fields.join(',')
677
-
678
- let customFilter = ''
679
- if (type == 'instructor') {
680
- customFilter = '&amp;&amp; coach_card_image != null'
681
- }
682
- // Determine the group by clause
683
- let query = ''
684
- let entityFieldsString = ''
685
- let filter = ''
686
- if (groupBy !== '' &amp;&amp; isGroupByOneToOne) {
687
- const webUrlPath = 'artists'
688
- const lessonsFilter = `_type == '${type}' &amp;&amp; brand == '${brand}' &amp;&amp; ^._id == ${groupBy}._ref ${searchFilter} ${includedFieldsFilter} ${progressFilter} ${customFilter}`
689
- const lessonsFilterWithRestrictions = await new FilterBuilder(lessonsFilter).buildFilter()
690
- entityFieldsString = `
691
- 'id': railcontent_id,
692
- 'type': _type,
693
- name,
694
- 'head_shot_picture_url': thumbnail_url.asset->url,
695
- 'web_url_path': '/${brand}/${webUrlPath}/'+name+'?included_fieds[]=type,${type}',
696
- 'all_lessons_count': count(*[${lessonsFilterWithRestrictions}]._id),
697
- 'children': *[${lessonsFilterWithRestrictions}]{
698
- ${fieldsString},
699
- ${groupBy}
700
- }[0...20]
701
- `
702
- filter = `_type == '${groupBy}' &amp;&amp; count(*[${lessonsFilterWithRestrictions}]._id) > 0`
703
- } else if (groupBy !== '') {
704
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
705
-
706
- const webUrlPath = groupBy == 'genre' ? '/genres' : ''
707
- const lessonsFilter = `brand == '${brand}' &amp;&amp; ^._id in ${groupBy}[]._ref ${typeFilter} ${searchFilter} ${includedFieldsFilter} ${progressFilter} ${customFilter}`
708
- const lessonsFilterWithRestrictions = await new FilterBuilder(lessonsFilter).buildFilter()
709
-
710
- entityFieldsString = `
711
- 'id': railcontent_id,
712
- 'type': _type,
713
- name,
714
- 'head_shot_picture_url': thumbnail_url.asset->url,
715
- 'web_url_path': select(defined(web_url_path)=> web_url_path +'?included_fieds[]=type,${type}',!defined(web_url_path)=> '/${brand}${webUrlPath}/'+name+'/${webUrlPathType}'),
716
- 'all_lessons_count': count(*[${lessonsFilterWithRestrictions}]._id),
717
- 'children': *[${lessonsFilterWithRestrictions}]{
718
- ${fieldsString},
719
- 'lesson_count': coalesce(count(child[${childrenFilter}]->), 0) ,
720
- ${groupBy}
721
- }[0...20]`
722
- filter = `_type == '${groupBy}' &amp;&amp; count(*[${lessonsFilterWithRestrictions}]._id) > 0`
723
- } else {
724
- filter = `brand == "${brand}" ${typeFilter} ${searchFilter} ${includedFieldsFilter} ${progressFilter} ${customFilter}`
725
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
726
- entityFieldsString = ` ${fieldsString},
727
- 'lesson_count': coalesce(count(child[${childrenFilter}]->), 0) ,
728
- 'length_in_seconds': coalesce(
729
- math::sum(
730
- select(
731
- child[${childrenFilter}]->length_in_seconds
732
- )
733
- ),
734
- length_in_seconds
735
- ),`
736
- }
737
-
738
- const filterWithRestrictions = await new FilterBuilder(filter, {
739
- bypassStatuses: bypassStatusAndPublishedValidation,
740
- bypassPermissions: bypassPermissions,
741
- bypassPublishedDateRestriction: bypassStatusAndPublishedValidation,
742
- }).buildFilter()
743
- query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
744
- sortOrder: sortOrder,
745
- start: start,
746
- end: end,
747
- })
748
-
749
- return fetchSanity(query, true)
750
- }
751
-
752
- async function getProgressFilter(progress, progressIds) {
753
- switch (progress) {
754
- case 'all':
755
- return progressIds !== undefined ? `&amp;&amp; railcontent_id in [${progressIds.join(',')}]` : ''
756
- case 'in progress': {
757
- const ids = await getAllStarted()
758
- return `&amp;&amp; railcontent_id in [${ids.join(',')}]`
759
- }
760
- case 'completed': {
761
- const ids = await getAllCompleted()
762
- return `&amp;&amp; railcontent_id in [${ids.join(',')}]`
763
- }
764
- case 'not started': {
765
- const ids = await getAllStartedOrCompleted()
766
- return `&amp;&amp; !(railcontent_id in [${ids.join(',')}])`
767
- }
768
- case 'recent': {
769
- const ids = progressIds !== undefined ? progressIds : await getAllStartedOrCompleted()
770
- return `&amp;&amp; (railcontent_id in [${ids.join(',')}])`
771
- }
772
- case 'incomplete': {
773
- const ids = progressIds !== undefined ? progressIds : await getAllStarted()
774
- return `&amp;&amp; railcontent_id in [${ids.join(',')}]`
775
- }
776
- default:
777
- throw new Error(`'${progress}' progress option not implemented`)
778
- }
779
- }
780
-
781
- export function getSortOrder(sort = '-published_on', brand, groupBy) {
782
- const sanitizedSort = sort?.trim() || '-published_on'
783
- let isDesc = sanitizedSort.startsWith('-')
784
- const sortField = isDesc ? sanitizedSort.substring(1) : sanitizedSort
785
-
786
- let sortOrder = ''
787
-
788
- switch (sortField) {
789
- case 'slug':
790
- sortOrder = groupBy ? 'name' : '!defined(title), lower(title)'
791
- break
792
-
793
- case 'popularity':
794
- if (groupBy == 'artist' || groupBy == 'genre') {
795
- sortOrder = isDesc ? `coalesce(popularity.${brand}, -1)` : 'popularity'
796
- } else {
797
- sortOrder = isDesc ? 'coalesce(popularity, -1)' : 'popularity'
798
- }
799
- break
800
-
801
- case 'recommended':
802
- sortOrder = 'published_on'
803
- isDesc = true
804
- break
805
-
806
- default:
807
- sortOrder = sortField
808
- break
809
- }
810
-
811
- sortOrder += isDesc ? ' desc' : ' asc'
812
- return sortOrder
813
- }
814
-
815
- /**
816
- * Fetches all available filter options based on brand, filters, and various optional criteria.
817
- *
818
- * This function constructs a query to retrieve the total number of results and filter options such as difficulty, instrument type, and genre.
819
- * The filter options are dynamically generated based on the provided filters, style, artist, and content type.
820
- * If a coachId is provided, the content type must be 'coach-lessons'.
821
- *
822
- * @param {string} brand - Brand to filter.
823
- * @param {string[]} filters - Key-value pairs to filter the query.
824
- * @param {string} [style] - Optional style/genre filter.
825
- * @param {string} [artist] - Optional artist name filter.
826
- * @param {string} contentType - Content type (e.g., 'song', 'lesson').
827
- * @param {string} [term] - Optional search term for title, album, artist, or genre.
828
- * @param {Array&lt;string>} [progressIds] - Optional array of progress IDs to filter by.
829
- * @param {string} [coachId] - Optional coach ID (only valid if contentType is 'coach-lessons').
830
- * @param {boolean} [includeTabs=false] - Whether to include tabs in the returned metadata.
831
- * @returns {Promise&lt;Object>} - The filter options and metadata.
832
- * @throws {Error} If coachId is provided but contentType isn't 'coach-lessons'.
833
- *
834
- * @example
835
- * // Fetch filter options for 'song' content type:
836
- * fetchAllFilterOptions('myBrand', [], 'Rock', 'John Doe', 'song', 'Love')
837
- * .then(options => console.log(options))
838
- * .catch(error => console.error(error));
839
- *
840
- * @example
841
- * // Fetch filter options for a coach's lessons with coachId:
842
- * fetchAllFilterOptions('myBrand', [], 'Rock', 'John Doe', 'coach-lessons', 'Love', undefined, '123')
843
- * .then(options => console.log(options))
844
- * .catch(error => console.error(error));
845
- */
846
- export async function fetchAllFilterOptions(
847
- brand,
848
- filters = [],
849
- style,
850
- artist,
851
- contentType,
852
- term,
853
- progressIds,
854
- coachId,
855
- includeTabs = false
856
- ) {
857
- if (contentType == 'lessons' || contentType == 'songs') {
858
- const metaData = processMetadata(brand, contentType, true)
859
- return {
860
- meta: metaData,
861
- }
862
- }
863
-
864
- if (coachId &amp;&amp; contentType !== 'coach-lessons') {
865
- throw new Error(
866
- `Invalid contentType: '${contentType}' for coachId. It must be 'coach-lessons'.`
867
- )
868
- }
869
-
870
- const includedFieldsFilter = filters?.length ? filtersToGroq(filters) : undefined
871
- const progressFilter = progressIds ? `&amp;&amp; railcontent_id in [${progressIds.join(',')}]` : ''
872
- const adapter = getPermissionsAdapter()
873
- const userPermissionsData = await adapter.fetchUserPermissions()
874
- const isAdmin = adapter.isAdmin(userPermissionsData)
875
-
876
- const constructCommonFilter = (excludeFilter) => {
877
- const filterWithoutOption = excludeFilter
878
- ? filtersToGroq(filters, excludeFilter)
879
- : includedFieldsFilter
880
- const statusFilter = ' &amp;&amp; status == "published"'
881
- const includeStatusFilter = !isAdmin &amp;&amp; !['instructor', 'artist', 'genre'].includes(contentType)
882
-
883
- return coachId
884
- ? `brand == '${brand}' &amp;&amp; status == "published" &amp;&amp; references(*[_type=='instructor' &amp;&amp; railcontent_id == ${coachId}]._id) ${filterWithoutOption || ''} ${term ? ` &amp;&amp; (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}`
885
- : `_type == '${contentType}' &amp;&amp; brand == "${brand}"${includeStatusFilter ? statusFilter : ''}${style &amp;&amp; excludeFilter !== 'style' ? ` &amp;&amp; '${style}' in genre[]->name` : ''}${artist &amp;&amp; excludeFilter !== 'artist' ? ` &amp;&amp; artist->name == '${artist}'` : ''} ${progressFilter} ${filterWithoutOption || ''} ${term ? ` &amp;&amp; (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}`
886
- }
887
-
888
- const metaData = processMetadata(brand, contentType, true)
889
- const allowableFilters = metaData?.allowableFilters || []
890
- const tabs = metaData?.tabs || []
891
- const catalogName = metaData?.shortname || metaData?.name
892
-
893
- const dynamicFilterOptions = allowableFilters
894
- .map((filter) => getFilterOptions(filter, constructCommonFilter(filter), contentType, brand))
895
- .join(' ')
896
-
897
- const query = `
898
- {
899
- "meta": {
900
- "totalResults": count(*[${constructCommonFilter()}
901
- ${term ? ` &amp;&amp; (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}]),
902
- "filterOptions": {
903
- ${dynamicFilterOptions}
904
- }
905
- }
906
- }`
907
-
908
- const results = await fetchSanity(query, true, { processNeedAccess: false })
909
-
910
- return includeTabs ? { ...results, tabs, catalogName } : results
911
- }
912
-
913
- //Daniel Nov 14 2025 note - keeping this for when we migrate foundations to packs, so we know what fields to use.
914
- /**
915
- * Fetch the Foundations 2019.
916
- * @param {string} slug - The slug of the method.
917
- * @returns {Promise&lt;Object|null>} - The fetched foundation data or null if not found.
918
- */
919
- export async function fetchFoundation(slug) {
920
- const filterParams = {}
921
- const query = await buildQuery(
922
- `_type == 'foundation' &amp;&amp; slug.current == "${slug}"`,
923
- filterParams,
924
- getFieldsForContentType('foundation'),
925
- {
926
- sortOrder: 'published_on asc',
927
- isSingle: true,
928
- }
929
- )
930
- return fetchSanity(query, false)
931
- }
932
-
933
- /**
934
- * Fetch the Method (learning-paths) for a specific brand.
935
- * @param {string} brand - The brand for which to fetch methods.
936
- * @param {string} slug - The slug of the method.
937
- * @returns {Promise&lt;Object|null>} - The fetched methods data or null if not found.
938
- */
939
- //todo BEH-1446 depreciated. remove all old method functions
940
- export async function fetchMethod(brand, slug) {
941
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
942
-
943
- const query = `*[_type == 'learning-path' &amp;&amp; brand == "${brand}" &amp;&amp; slug.current == "${slug}"] {
944
- "description": ${descriptionField},
945
- "instructors":instructor[]->name,
946
- published_on,
947
- "id": railcontent_id,
948
- railcontent_id,
949
- "slug": slug.current,
950
- status,
951
- title,
952
- video,
953
- length_in_seconds,
954
- parent_content_data,
955
- "breadcrumbs_data": parent_content_data[] {
956
- "id": id,
957
- "title": *[railcontent_id == ^.id][0].title,
958
- "url": *[railcontent_id == ^.id][0].web_url_path
959
- } | order(length(url)),
960
- "type": _type,
961
- "permission_id": permission[]->railcontent_id,
962
- "levels": child[${childrenFilter}]->
963
- {
964
- "id": railcontent_id,
965
- published_on,
966
- child_count,
967
- difficulty,
968
- difficulty_string,
969
- "thumbnail": thumbnail.asset->url,
970
- "instructor": instructor[]->{name},
971
- title,
972
- "type": _type,
973
- "description": ${descriptionField},
974
- "url": web_url_path,
975
- web_url_path,
976
- xp,
977
- total_xp
978
- }
979
- } | order(published_on asc)`
980
- return fetchSanity(query, false)
981
- }
982
-
983
- /**
984
- * Fetch the child courses for a specific method by Railcontent ID.
985
- * @param {string} railcontentId - The Railcontent ID of the current lesson.
986
- * @returns {Promise&lt;Object|null>} - The fetched next lesson data or null if not found.
987
- */
988
- export async function fetchMethodChildren(railcontentId) {
989
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
990
-
991
- const query = `*[railcontent_id == ${railcontentId}]{
992
- "child_count":coalesce(count(child[${childrenFilter}]->), 0),
993
- "id": railcontent_id,
994
- "description": ${descriptionField},
995
- "thumbnail": thumbnail.asset->url,
996
- title,
997
- xp,
998
- total_xp,
999
- parent_content_data,
1000
- "resources": ${resourcesField},
1001
- "breadcrumbs_data": parent_content_data[] {
1002
- "id": id,
1003
- "title": *[railcontent_id == ^.id][0].title,
1004
- "url": *[railcontent_id == ^.id][0].web_url_path
1005
- } | order(length(url)),
1006
- 'children': child[(${childrenFilter})]->{
1007
- ${getFieldsForContentType('method')}
1008
- },
1009
- }[0..1]`
1010
- return fetchSanity(query, true)
1011
- }
1012
-
1013
- /**
1014
- * Fetch the next lesson for a specific method by Railcontent ID.
1015
- * @param {string} railcontentId - The Railcontent ID of the current lesson.
1016
- * @param {string} methodId - The RailcontentID of the method
1017
- * @returns {Promise&lt;Object|null>} - object with `nextLesson` and `previousLesson` attributes
1018
- * @example
1019
- * fetchMethodPreviousNextLesson(241284, 241247)
1020
- * .then(data => { console.log('nextLesson', data.nextLesson); console.log('prevlesson', data.prevLesson);})
1021
- * .catch(error => console.error(error));
1022
- */
1023
- export async function fetchMethodPreviousNextLesson(railcontentId, methodId) {
1024
- const sortedChildren = await fetchMethodChildrenIds(methodId)
1025
- const index = sortedChildren.indexOf(Number(railcontentId))
1026
- let nextId = sortedChildren[index + 1]
1027
- let previousId = sortedChildren[index - 1]
1028
- let ids = []
1029
- if (nextId) ids.push(nextId)
1030
- if (previousId) ids.push(previousId)
1031
- let nextPrev = await fetchByRailContentIds(ids)
1032
- const nextLesson = nextPrev.find((elem) => {
1033
- return elem['id'] === nextId
1034
- })
1035
- const prevLesson = nextPrev.find((elem) => {
1036
- return elem['id'] === previousId
1037
- })
1038
- return { nextLesson, prevLesson }
1039
- }
1040
-
1041
- /**
1042
- * Fetch all children of a specific method by Railcontent ID.
1043
- * @param {string} railcontentId - The Railcontent ID of the method.
1044
- * @returns {Promise&lt;Array&lt;Object>|null>} - The fetched children data or null if not found.
1045
- */
1046
- export async function fetchMethodChildrenIds(railcontentId) {
1047
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1048
-
1049
- const query = `*[ railcontent_id == ${railcontentId}]{
1050
- 'children': child[${childrenFilter}]-> {
1051
- 'id': railcontent_id,
1052
- 'type' : _type,
1053
- 'children': child[${childrenFilter}]-> {
1054
- 'id': railcontent_id,
1055
- 'type' : _type,
1056
- 'children': child[${childrenFilter}]-> {
1057
- 'id': railcontent_id,
1058
- 'type' : _type,
1059
- }
1060
- }
1061
- }
1062
- }`
1063
- let allChildren = await fetchSanity(query, false)
1064
- return getChildrenToDepth(allChildren, 4)
1065
- }
1066
-
1067
- function getChildrenToDepth(parent, depth = 1) {
1068
- let allChildrenIds = []
1069
- if (parent &amp;&amp; parent['children'] &amp;&amp; depth > 0) {
1070
- parent['children'].forEach((child) => {
1071
- if (!child['children']) {
1072
- allChildrenIds.push(child['id'])
1073
- }
1074
- allChildrenIds = allChildrenIds.concat(getChildrenToDepth(child, depth - 1))
1075
- })
1076
- }
1077
- return allChildrenIds
1078
- }
1079
-
1080
- /**
1081
- * Fetch the next and previous lessons for a specific lesson by Railcontent ID.
1082
- * @param {string} railcontentId - The Railcontent ID of the current lesson.
1083
- * @returns {Promise&lt;Object|null>} - The fetched next and previous lesson data or null if found.
1084
- */
1085
- export async function fetchNextPreviousLesson(railcontentId) {
1086
- const document = await fetchLessonContent(railcontentId)
1087
- if (document.parent_content_data &amp;&amp; document.parent_content_data.length > 0) {
1088
- const lastElement = document.parent_content_data[document.parent_content_data.length - 1]
1089
- const results = await fetchMethodPreviousNextLesson(railcontentId, lastElement.id)
1090
- return results
1091
- }
1092
- const processedData = processMetadata(document.brand, document.type, true)
1093
- let sortBy = processedData?.sortBy ?? 'published_on'
1094
- const isDesc = sortBy.startsWith('-')
1095
- sortBy = isDesc ? sortBy.substring(1) : sortBy
1096
- let sortValue = document[sortBy]
1097
- if (sortValue == null) {
1098
- sortBy = 'railcontent_id'
1099
- sortValue = document['railcontent_id']
1100
- }
1101
- const isNumeric = !isNaN(sortValue)
1102
- let prevComparison = isNumeric ? `${sortBy} &lt;= ${sortValue}` : `${sortBy} &lt;= "${sortValue}"`
1103
- let nextComparison = isNumeric ? `${sortBy} >= ${sortValue}` : `${sortBy} >= "${sortValue}"`
1104
- const fields = getFieldsForContentType(document.type)
1105
- const query = `{
1106
- "prevLesson": *[brand == "${document.brand}" &amp;&amp; status == "${document.status}" &amp;&amp; _type == "${document.type}" &amp;&amp; ${prevComparison} &amp;&amp; railcontent_id != ${railcontentId}] | order(${sortBy} desc){${fields}}[0...1][0],
1107
- "nextLesson": *[brand == "${document.brand}" &amp;&amp; status == "${document.status}" &amp;&amp; _type == "${document.type}" &amp;&amp; ${nextComparison} &amp;&amp; railcontent_id != ${railcontentId}] | order(${sortBy} asc){${fields}}[0...1][0]
1108
- }`
1109
-
1110
- return await fetchSanity(query, true)
1111
- }
1112
-
1113
- /**
1114
- * Fetch the next piece of content under a parent by Railcontent ID
1115
- * @param {int} railcontentId - The Railcontent ID of the parent content
1116
- * @returns {Promise&lt;{next: (Object|null)}|null>} - object with 'next' attribute
1117
- * @example
1118
- * jumpToContinueContent(296693)
1119
- * then.(data => { console.log('next', data.next);})
1120
- * .catch(error => console.error(error));
1121
- */
1122
- export async function jumpToContinueContent(railcontentId) {
1123
- const nextContent = await fetchNextContentDataForParent(railcontentId)
1124
- if (!nextContent || !nextContent.id) {
1125
- return null
1126
- }
1127
- let next = await fetchByRailContentId(nextContent.id, nextContent.type)
1128
- return { next }
1129
- }
1130
-
1131
- /**
1132
- * Fetch the page data for a specific lesson by Railcontent ID.
1133
- * @param {string} railContentId - The Railcontent ID of the current lesson.
1134
- * @parent {boolean} addParent - Whether to include parent content data in the response.
1135
- * @returns {Promise&lt;Object|null>} - The fetched page data or null if found.
1136
- *
1137
- * @example
1138
- * fetchLessonContent('lesson123')
1139
- * .then(data => console.log(data))
1140
- * .catch(error => console.error(error));
1141
- */
1142
- export async function fetchLessonContent(railContentId, { addParent = false } = {}) {
1143
- const filterParams = {
1144
- isSingle: true,
1145
- pullFutureContent: true,
1146
- showMembershipRestrictedContent: true,
1147
- }
1148
-
1149
- const parentQuery = addParent
1150
- ? `"parent_content_data": *[railcontent_id in [...(^.parent_content_data[].id)]]{
1151
- "id": railcontent_id,
1152
- title,
1153
- slug,
1154
- "type": _type,
1155
- "logo" : logo_image_url.asset->url,
1156
- "dark_mode_logo": dark_mode_logo_url.asset->url,
1157
- "light_mode_logo": light_mode_logo_url.asset->url,
1158
- "badge": *[references(^._id) &amp;&amp; _type == 'content-award'][0].badge.asset->url,
1159
- },`
1160
- : ''
1161
-
1162
- const fields = `${getFieldsForContentType()}
1163
- "resources": ${resourcesField},
1164
- soundslice,
1165
- instrumentless,
1166
- soundslice_slug,
1167
- "description": ${descriptionField},
1168
- "chapters": ${chapterField},
1169
- "instructors":instructor[]->name,
1170
- "instructor": ${instructorField},
1171
- ${assignmentsField}
1172
- video,
1173
- length_in_seconds,
1174
- mp3_no_drums_no_click_url,
1175
- mp3_no_drums_yes_click_url,
1176
- mp3_yes_drums_no_click_url,
1177
- mp3_yes_drums_yes_click_url,
1178
- "permission_id": permission[]->railcontent_id,
1179
- ${parentQuery}
1180
- ...select(
1181
- defined(live_event_start_time) => {
1182
- "live_event_start_time": live_event_start_time,
1183
- "live_event_end_time": live_event_end_time,
1184
- "live_event_stream_id": live_event_stream_id,
1185
- "videoId": coalesce(live_event_stream_id, video.external_id),
1186
- "live_event_is_global": live_global_event == true
1187
- }
1188
- )
1189
- `
1190
-
1191
- const query = await buildQuery(`railcontent_id == ${railContentId}`, filterParams, fields, {
1192
- isSingle: true,
1193
- })
1194
- const chapterProcess = (result) => {
1195
- const now = getSanityDate(new Date(), false)
1196
- if (result.live_event_start_time &amp;&amp; result.live_event_end_time) {
1197
- result.isLive = result.live_event_start_time &lt;= now &amp;&amp; result.live_event_end_time >= now
1198
- }
1199
- const chapters = result.chapters ?? []
1200
- if (chapters.length === 0) return result
1201
- result.chapters = chapters.map((chapter, index) => ({
1202
- ...chapter,
1203
- chapter_thumbnail_url: `https://musora-web-platform.s3.amazonaws.com/chapters/${result.brand}/Chapter${index + 1}.jpg`,
1204
- }))
1205
- return result
1206
- }
1207
-
1208
- return fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true })
1209
- }
1210
-
1211
- /**
1212
- * Returns a list of recommended content based on the provided railContentId.
1213
- * If no recommendations found in recsys, falls back to fetching related lessons.
1214
- *
1215
- * @param railContentId
1216
- * @param brand
1217
- * @param count
1218
- * @returns {Promise&lt;Array&lt;Object>>}
1219
- */
1220
- export async function fetchRelatedRecommendedContent(railContentId, brand, count = 10) {
1221
- const recommendedItems = await fetchSimilarItems(railContentId, brand, count)
1222
- if (recommendedItems &amp;&amp; recommendedItems.length > 0) {
1223
- return fetchByRailContentIds(recommendedItems, 'tab-data', brand, true)
1224
- }
1225
-
1226
- return await fetchRelatedLessons(railContentId, brand).then((result) =>
1227
- result.related_lessons?.splice(0, count)
1228
- )
1229
- }
1230
-
1231
- /**
1232
- * Get song type (transcriptions, jam packs, play alongs, tutorial children) content documents that share content information with the provided railcontent document.
1233
- * These are linked through content that shares a license with the provided railcontent document
1234
- *
1235
- * @param railcontentId
1236
- * @param brand
1237
- * @param count
1238
- * @returns {Promise&lt;Array&lt;Object>>}
1239
- */
1240
- export async function fetchOtherSongVersions(railcontentId, brand, count = 3) {
1241
- return fetchRelatedByLicense(railcontentId, brand, true, count)
1242
- }
1243
-
1244
- /**
1245
- * Get non-song content documents that share content information with the provided railcontent document.
1246
- * These are linked through content that shares a license with the provided railcontent document
1247
- *
1248
- * @param {integer} railcontentId
1249
- * @param {string} brand
1250
- * @param {integer:3} count
1251
- * @returns {Promise&lt;Array&lt;Object>>}
1252
- */
1253
- export async function fetchLessonsFeaturingThisContent(railcontentId, brand, count = 3) {
1254
- return fetchRelatedByLicense(railcontentId, brand, false, count)
1255
- }
1256
-
1257
- /**
1258
- * Get content documents that share license information with the provided railcontent id
1259
- *
1260
- * @param {integer} railcontentId
1261
- * @param {string} brand
1262
- * @param {boolean} onlyUseSongTypes - if true, only return the song type documents. If false, return everything except those
1263
- * @param {integer:3} count
1264
- * @returns {Promise&lt;Array&lt;Object>>}
1265
- */
1266
- async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, count) {
1267
- const typeCheck = `@->_type in [${arrayJoinWithQuotes(SONG_TYPES)}]`
1268
- let typeCheckString = `@->brand == '${brand}' &amp;&amp; `
1269
- typeCheckString += onlyUseSongTypes ? `${typeCheck}` : `!(${typeCheck})`
1270
- const contentFromLicenseFilter = `_type == 'license' &amp;&amp; references(^._id)].content[${typeCheckString} &amp;&amp; @->railcontent_id != ${railcontentId}`
1271
- let filterSongTypesWithSameLicense = await new FilterBuilder(contentFromLicenseFilter, {
1272
- isChildrenFilter: true,
1273
- }).buildFilter()
1274
- let queryFields = getFieldsForContentType()
1275
- const baseParentQuery = `railcontent_id == ${railcontentId}`
1276
- let parentQuery = await new FilterBuilder(baseParentQuery).buildFilter()
1277
-
1278
- // queryFields = 'railcontent_id, title'
1279
- // parentQuery = baseParentQuery
1280
- // filterSongTypesWithSameLicense = contentFromLicenseFilter
1281
- const query = `*[${parentQuery}]{
1282
- _type, railcontent_id,
1283
- "related_by_license" :
1284
- *[${filterSongTypesWithSameLicense}]->{${queryFields}}|order(published_on desc, title asc)[0...${count}],
1285
- }[0...1]`
1286
- const results = await fetchSanity(query, false)
1287
- return results ? (results['related_by_license'] ?? []) : []
1288
- }
1289
-
1290
- /**
1291
- * Fetch sibling lessons to a specific lesson
1292
- * @param {string} railContentId - The RailContent ID of the current lesson.
1293
- * @param {string} brand - The current brand.
1294
- * @returns {Promise&lt;Array&lt;Object>|null>} - The fetched related lessons data or null if not found.
1295
- */
1296
- export async function fetchSiblingContent(railContentId, brand = null) {
1297
- const filterGetParent = await new FilterBuilder(`references(^._id) &amp;&amp; _type == ^.parent_type`, {
1298
- pullFutureContent: true,
1299
- showMembershipRestrictedContent: true, // Show parent even without permissions
1300
- }).buildFilter()
1301
- const filterForParentList = await new FilterBuilder(
1302
- `references(^._id) &amp;&amp; _type == ^.parent_type`,
1303
- {
1304
- pullFutureContent: true,
1305
- isParentFilter: true,
1306
- showMembershipRestrictedContent: true, // Show parent even without permissions
1307
- }
1308
- ).buildFilter()
1309
-
1310
- const childrenFilter = await new FilterBuilder(``, {
1311
- isChildrenFilter: true,
1312
- showMembershipRestrictedContent: true, // Show all lessons in sidebar, need_access applied on individual page
1313
- }).buildFilter()
1314
-
1315
- const brandString = brand ? ` &amp;&amp; brand == "${brand}"` : ''
1316
- 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`
1317
-
1318
- const query = `*[railcontent_id == ${railContentId}${brandString}]{
1319
- _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
1320
- 'for-calculations': *[${filterGetParent}][0]{
1321
- 'siblings-list': child[]->railcontent_id,
1322
- 'parents-list': *[${filterForParentList}][0].child[]->railcontent_id
1323
- },
1324
- "related_lessons" : *[${filterGetParent}][0].child[${childrenFilter}]->{${queryFields}}
1325
- }`
1326
-
1327
- let result = await fetchSanity(query, false, { processNeedAccess: true })
1328
-
1329
- //there's no way in sanity to retrieve the index of an array, so we must calculate after fetch
1330
- if (result['for-calculations'] &amp;&amp; result['for-calculations']['parents-list']) {
1331
- const calc = result['for-calculations']
1332
- const parentCount = calc['parents-list'].length
1333
- const currentParentIndex = calc['parents-list'].indexOf(result['parent_id']) + 1
1334
- const siblingCount = calc['siblings-list'].length
1335
- const currentSiblingIndex = calc['siblings-list'].indexOf(result['railcontent_id']) + 1
1336
-
1337
- delete result['for-calculations']
1338
- result = { ...result, parentCount, currentParentIndex, siblingCount, currentSiblingIndex }
1339
- return result
1340
- } else {
1341
- delete result['for-calculations']
1342
- return result
1343
- }
1344
- }
1345
-
1346
- /**
1347
- * Fetch lessons related to a specific lesson by RailContent ID and type.
1348
- * @param {string} railContentId - The RailContent ID of the current lesson.
1349
- * @returns {Promise&lt;Array&lt;Object>|null>} - The fetched related lessons data or null if not found.
1350
- */
1351
- export async function fetchRelatedLessons(railContentId) {
1352
- const defaultFilterFields = `_type==^._type &amp;&amp; brand == ^.brand &amp;&amp; railcontent_id != ${railContentId}`
1353
-
1354
- const filterSameArtist = await new FilterBuilder(
1355
- `${defaultFilterFields} &amp;&amp; references(^.artist->_id)`,
1356
- { showMembershipRestrictedContent: true }
1357
- ).buildFilter()
1358
- const filterSameGenre = await new FilterBuilder(
1359
- `${defaultFilterFields} &amp;&amp; references(^.genre[]->_id)`,
1360
- { showMembershipRestrictedContent: true }
1361
- ).buildFilter()
1362
-
1363
- 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, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
1364
-
1365
- const query = `*[railcontent_id == ${railContentId} &amp;&amp; (!defined(permission) || references(*[_type=='permission']._id))]{
1366
- _type, parent_type, railcontent_id,
1367
- "related_lessons" : array::unique([
1368
- ...(*[${filterSameArtist}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
1369
- ...(*[${filterSameGenre}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
1370
- ])[0...10]}`
1371
-
1372
- return await fetchSanity(query, false, { processNeedAccess: true })
1373
- }
1374
-
1375
- /**
1376
- * Fetch all packs.
1377
- * @param {string} brand - The brand for which to fetch packs.
1378
- * @param {string} [searchTerm=""] - The search term to filter packs.
1379
- * @param {string} [sort="-published_on"] - The field to sort the packs by.
1380
- * @param {number} [params.page=1] - The page number for pagination.
1381
- * @param {number} [params.limit=10] - The number of items per page.
1382
- * @returns {Promise&lt;Array&lt;Object>|null>} - The fetched pack content data or null if not found.
1383
- */
1384
- export async function fetchAllPacks(
1385
- brand,
1386
- sort = '-published_on',
1387
- searchTerm = '',
1388
- page = 1,
1389
- limit = 10
1390
- ) {
1391
- const sortOrder = getSortOrder(sort, brand)
1392
- const filter = `(_type == 'pack' || _type == 'semester-pack') &amp;&amp; brand == '${brand}' &amp;&amp; title match "${searchTerm}*"`
1393
- const filterParams = {}
1394
- const start = (page - 1) * limit
1395
- const end = start + limit
1396
-
1397
- const query = await buildQuery(
1398
- filter,
1399
- filterParams,
1400
- await getFieldsForContentTypeWithFilteredChildren('pack'),
1401
- {
1402
- sortOrder: sortOrder,
1403
- start,
1404
- end,
1405
- }
1406
- )
1407
- return fetchSanity(query, true)
1408
- }
1409
-
1410
- /**
1411
- * Fetch all content for a specific pack by Railcontent ID.
1412
- * @param {string} railcontentId - The Railcontent ID of the pack.
1413
- * @returns {Promise&lt;Array&lt;Object>|null>} - The fetched pack content data or null if not found.
1414
- */
1415
- export async function fetchPackAll(railcontentId, type = 'pack') {
1416
- return fetchByRailContentId(railcontentId, type)
1417
- }
1418
-
1419
- export async function fetchLiveEvent(brand, forcedContentId = null) {
1420
- const LIVE_EXTRA_MINUTES = 30
1421
- //calendarIDs taken from addevent.php
1422
- // TODO import instructor calendars to Sanity
1423
- let defaultCalendarID = ''
1424
- switch (brand) {
1425
- case 'drumeo':
1426
- defaultCalendarID = 'GP142387'
1427
- break
1428
- case 'pianote':
1429
- defaultCalendarID = 'be142408'
1430
- break
1431
- case 'guitareo':
1432
- defaultCalendarID = 'IJ142407'
1433
- break
1434
- case 'singeo':
1435
- defaultCalendarID = 'bk354284'
1436
- break
1437
- default:
1438
- break
1439
- }
1440
- let startDateTemp = new Date()
1441
- let endDateTemp = new Date()
1442
-
1443
- startDateTemp = new Date(
1444
- startDateTemp.setMinutes(startDateTemp.getMinutes() + LIVE_EXTRA_MINUTES)
1445
- )
1446
- endDateTemp = new Date(endDateTemp.setMinutes(endDateTemp.getMinutes() - LIVE_EXTRA_MINUTES))
1447
-
1448
- // See LiveStreamEventService.getCurrentOrNextLiveEvent for some nice complicated logic which I don't think is actually importart
1449
- // this has some +- on times
1450
- // But this query just finds the first scheduled event (sorted by start_time) that ends after now()
1451
- const query =
1452
- forcedContentId !== null
1453
- ? `*[railcontent_id == ${forcedContentId} ]{
1454
- 'slug': slug.current,
1455
- 'id': railcontent_id,
1456
- live_event_start_time,
1457
- live_event_end_time,
1458
- live_event_stream_id,
1459
- railcontent_id,
1460
- published_on,
1461
- 'event_coach_url' : instructor[0]->web_url_path,
1462
- 'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}'),
1463
- title,
1464
- "thumbnail": thumbnail.asset->url,
1465
- ${artistOrInstructorName()},
1466
- difficulty_string,
1467
- "instructors": ${instructorField},
1468
- 'videoId': coalesce(live_event_stream_id, video.external_id),
1469
- } | order(live_event_start_time)[0...1]`
1470
- : `*[status == 'scheduled' &amp;&amp; brand == '${brand}' &amp;&amp; defined(live_event_start_time) &amp;&amp; live_event_start_time &lt;= '${getSanityDate(startDateTemp, false)}' &amp;&amp; live_event_end_time >= '${getSanityDate(endDateTemp, false)}']{
1471
- 'slug': slug.current,
1472
- 'id': railcontent_id,
1473
- live_event_start_time,
1474
- live_event_end_time,
1475
- live_event_stream_id,
1476
- railcontent_id,
1477
- published_on,
1478
- 'event_coach_url' : instructor[0]->web_url_path,
1479
- 'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}'),
1480
- title,
1481
- "thumbnail": thumbnail.asset->url,
1482
- ${artistOrInstructorName()},
1483
- difficulty_string,
1484
- "instructors": instructor[]->{
1485
- name,
1486
- web_url_path,
1487
- },
1488
- 'videoId': coalesce(live_event_stream_id, video.external_id),
1489
- } | order(live_event_start_time)[0...1]`
1490
-
1491
- return await fetchSanity(query, false, { processNeedAccess: false })
1492
- }
1493
-
1494
- /**
1495
- * Fetch the data needed for the Pack Overview screen.
1496
- * @param {number} id - The Railcontent ID of the pack
1497
- * @returns {Promise&lt;Object|null>} - The pack information and lessons or null if not found.
1498
- *
1499
- * @example
1500
- * fetchPackData(404048)
1501
- * .then(pack => console.log(pack))
1502
- * .catch(error => console.error(error));
1503
- */
1504
- export async function fetchPackData(id) {
1505
- const builder = await new FilterBuilder(`railcontent_id == ${id}`).buildFilter()
1506
- const query = `*[${builder}]{
1507
- ${await getFieldsForContentTypeWithFilteredChildren('pack')}
1508
- } [0...1]`
1509
- return fetchSanity(query, false)
1510
- }
1511
-
1512
- /**
1513
- * Fetch the data needed for the coach screen.
1514
- * @param {string} id - The Railcontent ID of the coach
1515
- *
1516
- * @returns {Promise&lt;Object|null>} - The lessons for the instructor or null if not found.
1517
- *
1518
- * @example
1519
- * fetchCoachLessons('coach123')
1520
- * .then(lessons => console.log(lessons))
1521
- * .catch(error => console.error(error));
1522
- */
1523
- export async function fetchByReference(
1524
- brand,
1525
- { sortOrder = '-published_on', searchTerm = '', page = 1, limit = 20, includedFields = [] } = {}
1526
- ) {
1527
- const fieldsString = getFieldsForContentType()
1528
- const start = (page - 1) * limit
1529
- const end = start + limit
1530
- const searchFilter = searchTerm ? `&amp;&amp; title match "${searchTerm}*"` : ''
1531
- const includedFieldsFilter = includedFields.length > 0 ? includedFields.join(' &amp;&amp; ') : ''
1532
-
1533
- const filter = `brand == '${brand}' ${searchFilter} &amp;&amp; references(*[${includedFieldsFilter}]._id)`
1534
- const filterWithRestrictions = await new FilterBuilder(filter).buildFilter()
1535
- const query = buildEntityAndTotalQuery(filterWithRestrictions, fieldsString, {
1536
- sortOrder: getSortOrder(sortOrder, brand),
1537
- start: start,
1538
- end: end,
1539
- })
1540
- return fetchSanity(query, true)
1541
- }
1542
-
1543
- /**
1544
- *
1545
- * Return the top level parent content railcontent_id.
1546
- * Ignores learning-path-v2 parents.
1547
- * ex: if railcontentId is of type 'skill-pack-lesson', return the corresponding 'skill-pack' railcontent_id
1548
- *
1549
- * @param {int} railcontentId
1550
- * @returns {Promise&lt;int|null>}
1551
- */
1552
- export async function fetchTopLevelParentId(railcontentId) {
1553
- const parentFilter = "railcontent_id in [...(^.parent_content_data[].id)]"
1554
- const statusFilter = "&amp;&amp; status in ['scheduled', 'published', 'archived', 'unlisted']"
1555
-
1556
- const query = `*[railcontent_id == ${railcontentId}]{
1557
- railcontent_id,
1558
- 'parents': *[${parentFilter} ${statusFilter}]{
1559
- railcontent_id
1560
- }
1561
- }`
1562
- let response = await fetchSanity(query, false, { processNeedAccess: false })
1563
- if (!response) return null
1564
- let parents = response['parents']
1565
- let parentsLength = parents ? response['parents'].length : 0
1566
- if (parentsLength > 0) {
1567
- // return the last parent
1568
- return parents[parentsLength - 1]['railcontent_id']
1569
- }
1570
- return response['railcontent_id']
1571
- }
1572
-
1573
- export async function fetchHierarchy(railcontentId) {
1574
- let topLevelId = await fetchTopLevelParentId(railcontentId)
1575
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1576
- const query = `*[railcontent_id == ${topLevelId}]{
1577
- railcontent_id,
1578
- 'assignments': assignment[]{railcontent_id},
1579
- 'children': child[${childrenFilter}]->{
1580
- railcontent_id,
1581
- 'assignments': assignment[]{railcontent_id},
1582
- 'children': child[${childrenFilter}]->{
1583
- railcontent_id,
1584
- 'assignments': assignment[]{railcontent_id},
1585
- 'children': child[${childrenFilter}]->{
1586
- railcontent_id,
1587
- 'assignments': assignment[]{railcontent_id},
1588
- 'children': child[${childrenFilter}]->{
1589
- railcontent_id,
1590
- }
1591
- }
1592
- }
1593
- },
1594
- }`
1595
- let response = await fetchSanity(query, false, { processNeedAccess: false })
1596
- if (!response) return null
1597
- let data = {
1598
- topLevelId: topLevelId,
1599
- parents: {},
1600
- children: {},
1601
- }
1602
- populateHierarchyLookups(response, data, null)
1603
- return data
1604
- }
1605
-
1606
- function populateHierarchyLookups(currentLevel, data, parentId) {
1607
- let contentId = currentLevel['railcontent_id']
1608
- let children = currentLevel['children']
1609
-
1610
- data.parents[contentId] = parentId
1611
- if (children) {
1612
- data.children[contentId] = children.map((child) => child['railcontent_id'])
1613
- for (let i = 0; i &lt; children.length; i++) {
1614
- populateHierarchyLookups(children[i], data, contentId)
1615
- }
1616
- } else {
1617
- data.children[contentId] = []
1618
- }
1619
-
1620
- let assignments = currentLevel['assignments']
1621
- if (assignments) {
1622
- let assignmentIds = assignments.map((assignment) => assignment['railcontent_id'])
1623
- data.children[contentId] = (data.children[contentId] ?? []).concat(assignmentIds)
1624
- assignmentIds.forEach((assignmentId) => {
1625
- data.parents[assignmentId] = contentId
1626
- })
1627
- }
1628
- }
1629
-
1630
- /**
1631
- * Fetch data for comment mod page
1632
- *
1633
- * @param {array} ids - List of ids get data for
1634
- * @returns {Promise&lt;Object|null>} - A promise that resolves to an object containing the data
1635
- */
1636
- export async function fetchCommentModContentData(ids) {
1637
- const idsString = ids.join(',')
1638
- const fields = `"id": railcontent_id, "type": _type, title, "url": web_url_path, "parent": *[^._id in child[]._ref]{"id": railcontent_id, title}`
1639
- const query = await buildQuery(
1640
- `railcontent_id in [${idsString}]`,
1641
- { bypassPermissions: true },
1642
- fields,
1643
- { end: 50 }
1644
- )
1645
- let data = await fetchSanity(query, true)
1646
- let mapped = {}
1647
- data.forEach(function (content) {
1648
- mapped[content.id] = {
1649
- id: content.id,
1650
- type: content.type,
1651
- title: content.title,
1652
- url: content.url,
1653
- parentTitle: content.parent[0]?.title ?? null,
1654
- }
1655
- })
1656
- return mapped
1657
- }
1658
-
1659
- /**
1660
- *
1661
- * @param {string} query - The GROQ query to execute against the Sanity API.
1662
- * @param {boolean} isList - Whether to return an array or a single result.
1663
- * @param {Object} options - Additional options for fetching data.
1664
- * @param {Function} [options.customPostProcess=null] - custom post process callback
1665
- * @param {boolean} [options.processNeedAccess=true] - execute the needs_access callback
1666
- * @param {boolean} [options.processPageType=true] - execute the page_type callback
1667
- * @returns {Promise&lt;*|null>} - A promise that resolves to the fetched data or null if an error occurs or no results are found.
1668
- *
1669
- * @example
1670
- * const query = `*[_type == "song"]{title, artist->name}`;
1671
- * fetchSanity(query, true)
1672
- * .then(data => console.log(data))
1673
- * .catch(error => console.error(error));
1674
- */
1675
-
1676
- export async function fetchSanity(
1677
- query,
1678
- isList,
1679
- { customPostProcess = null, processNeedAccess = true, processPageType = true } = {}
1680
- ) {
1681
- // Check the config object before proceeding
1682
- if (!checkSanityConfig(globalConfig)) {
1683
- return null
1684
- }
1685
-
1686
- const perspective = globalConfig.sanityConfig.perspective ?? 'published'
1687
- const api = globalConfig.sanityConfig.useCachedAPI ? 'apicdn' : 'api'
1688
- const url = `https://${globalConfig.sanityConfig.projectId}.${api}.sanity.io/v${globalConfig.sanityConfig.version}/data/query/${globalConfig.sanityConfig.dataset}?perspective=${perspective}`
1689
- const headers = {
1690
- 'Content-Type': 'application/json',
1691
- }
1692
-
1693
- try {
1694
- const method = 'post'
1695
- const options = {
1696
- method,
1697
- headers,
1698
- body: JSON.stringify({ query: query }),
1699
- }
1700
-
1701
- const adapter = getPermissionsAdapter()
1702
- let promisesResult = await Promise.all([
1703
- fetch(url, options),
1704
- processNeedAccess ? adapter.fetchUserPermissions() : null,
1705
- ])
1706
- const response = promisesResult[0]
1707
- const userPermissions = promisesResult[1]
1708
-
1709
- if (!response.ok) {
1710
- throw new Error(`Sanity API error: ${response.status} - ${response.statusText}`)
1711
- }
1712
- const result = await response.json()
1713
- if (result.result) {
1714
- let results = isList ? result.result : result.result[0]
1715
- if (!results) {
1716
- throw new Error('No results found')
1717
- }
1718
- results = processNeedAccess ? await needsAccessDecorator(results, userPermissions) : results
1719
- results = processPageType ? pageTypeDecorator(results) : results
1720
- return customPostProcess ? customPostProcess(results) : results
1721
- } else {
1722
- throw new Error('No results found')
1723
- }
1724
- } catch (error) {
1725
- console.error('fetchSanity: Fetch error:', { error, query })
1726
- return null
1727
- }
1728
- }
1729
-
1730
- function contentResultsDecorator(results, fieldName, callback) {
1731
- if (Array.isArray(results)) {
1732
- results.forEach((result) => {
1733
- // Check if this is a content row structure
1734
- if (result.content &amp;&amp; Array.isArray(result.content)) {
1735
- // Content rows structure: array of rows, each with a content array
1736
- result.content.forEach((contentItem) => {
1737
- if (contentItem) {
1738
- contentItem[fieldName] = callback(contentItem)
1739
- }
1740
- })
1741
- } else {
1742
- result[fieldName] = callback(result)
1743
- }
1744
- })
1745
- } else if (results.entity &amp;&amp; Array.isArray(results.entity)) {
1746
- // Group By
1747
- results.entity.forEach((result) => {
1748
- if (result.lessons) {
1749
- result.lessons.forEach((lesson) => {
1750
- lesson[fieldName] = callback(lesson) // Updated to check lesson access
1751
- })
1752
- } else {
1753
- result[fieldName] = callback(result)
1754
- }
1755
- })
1756
- } else if (results.related_lessons &amp;&amp; Array.isArray(results.related_lessons)) {
1757
- results.related_lessons.forEach((result) => {
1758
- result[fieldName] = callback(result)
1759
- })
1760
- } else {
1761
- results[fieldName] = callback(results)
1762
- }
1763
-
1764
- return results
1765
- }
1766
-
1767
- function pageTypeDecorator(results) {
1768
- return contentResultsDecorator(results, 'page_type', function (content) {
1769
- return SONG_TYPES_WITH_CHILDREN.includes(content['type']) ? 'song' : 'lesson'
1770
- })
1771
- }
1772
-
1773
- function needsAccessDecorator(results, userPermissions) {
1774
- if (globalConfig.sanityConfig.useDummyRailContentMethods) return results
1775
- const adapter = getPermissionsAdapter()
1776
- return contentResultsDecorator(results, 'need_access', function (content) {
1777
- return adapter.doesUserNeedAccess(content, userPermissions)
1778
- })
1779
- }
1780
-
1781
- function doesUserNeedAccessToContent(result, userPermissions) {
1782
- // Legacy function - now delegates to adapter
1783
- // Kept for backwards compatibility if used elsewhere
1784
- const adapter = getPermissionsAdapter()
1785
- return adapter.doesUserNeedAccess(result, userPermissions)
1786
- }
1787
-
1788
- /**
1789
- * Fetch shows data for a brand.
1790
- *
1791
- * @param brand - The brand for which to fetch shows.
1792
- * @returns {Promise&lt;{name, description, type: *, thumbnailUrl}>}
1793
- *
1794
- * @example
1795
- *
1796
- * fetchShowsData('drumeo')
1797
- * .then(data => console.log(data))
1798
- * .catch(error => console.error(error));
1799
- */
1800
- export async function fetchShowsData(brand) {
1801
- let shows = showsTypes[brand] ?? []
1802
- const showsInfo = []
1803
-
1804
- shows.forEach((type) => {
1805
- const processedData = processMetadata(brand, type)
1806
- if (processedData) showsInfo.push(processedData)
1807
- })
1808
-
1809
- return showsInfo
1810
- }
1811
-
1812
- /**
1813
- * Fetch metadata from the contentMetaData.js based on brand and type.
1814
- * For v2 you need to provide page type('lessons' or 'songs') in type parameter
1815
- *
1816
- * @param {string} brand - The brand for which to fetch metadata.
1817
- * @param {string} type - The type for which to fetch metadata.
1818
- * @returns {Promise&lt;{name, description, type: *, thumbnailUrl}>}
1819
- *
1820
- * @example
1821
- *
1822
- * fetchMetadata('drumeo','song')
1823
- * .then(data => console.log(data))
1824
- * .catch(error => console.error(error));
1825
- */
1826
- export async function fetchMetadata(brand, type) {
1827
- let processedData = processMetadata(brand, type, true)
1828
- if (processedData?.onlyAvailableTabs === true) {
1829
- const activeTabs = await fetchRecentActivitiesActiveTabs()
1830
- processedData.tabs = activeTabs
1831
- }
1832
-
1833
- return processedData ? processedData : {}
1834
- }
1835
-
1836
- export async function fetchChatAndLiveEnvent(brand, forcedId = null) {
1837
- const liveEvent =
1838
- forcedId !== null ? await fetchByRailContentIds([forcedId]) : [await fetchLiveEvent(brand)]
1839
- if (liveEvent.length === 0 || (liveEvent.length === 1 &amp;&amp; liveEvent[0] === undefined)) {
1840
- return null
1841
- }
1842
- let url = `/content/live-chat?brand=${brand}`
1843
- const chatData = await fetchHandler(url)
1844
- const mergedData = { ...chatData, ...liveEvent[0] }
1845
- return mergedData
1846
- }
1847
-
1848
- //Helper Functions
1849
- function arrayJoinWithQuotes(array, delimiter = ',') {
1850
- const wrapped = array.map((value) => `'${value}'`)
1851
- return wrapped.join(delimiter)
1852
- }
1853
-
1854
- export function getSanityDate(date, roundToHourForCaching = true) {
1855
- if (roundToHourForCaching) {
1856
- // We need to set the published on filter date to be a round time so that it doesn't bypass the query cache
1857
- // with every request by changing the filter date every second. I've set it to one minute past the current hour
1858
- // because publishing usually publishes content on the hour exactly which means it should still skip the cache
1859
- // when the new content is available.
1860
- // Round to the start of the current hour
1861
- const roundedDate = new Date(
1862
- date.getFullYear(),
1863
- date.getMonth(),
1864
- date.getDate(),
1865
- date.getHours()
1866
- )
1867
-
1868
- return roundedDate.toISOString()
1869
- }
1870
-
1871
- return date.toISOString()
1872
- }
1873
-
1874
- function getDateOnly(date = new Date()) {
1875
- return date.toISOString().split('T')[0]
1876
- }
1877
-
1878
- const merge = (a, b, predicate = (a, b) => a === b) => {
1879
- const c = [...a] // copy to avoid side effects
1880
- // add all items from B to copy C if they're not already present
1881
- b.forEach((bItem) => (c.some((cItem) => predicate(bItem, cItem)) ? null : c.push(bItem)))
1882
- return c
1883
- }
1884
-
1885
- function checkSanityConfig(config) {
1886
- if (!config.sanityConfig.token) {
1887
- console.warn('fetchSanity: The "token" property is missing in the config object.')
1888
- return false
1889
- }
1890
- if (!config.sanityConfig.projectId) {
1891
- console.warn('fetchSanity: The "projectId" property is missing in the config object.')
1892
- return false
1893
- }
1894
- if (!config.sanityConfig.dataset) {
1895
- console.warn('fetchSanity: The "dataset" property is missing in the config object.')
1896
- return false
1897
- }
1898
- if (!config.sanityConfig.version) {
1899
- console.warn('fetchSanity: The "version" property is missing in the config object.')
1900
- return false
1901
- }
1902
- return true
1903
- }
1904
-
1905
- function buildRawQuery(
1906
- filter = '',
1907
- fields = '...',
1908
- { sortOrder = 'published_on desc', start = 0, end = 10, isSingle = false }
1909
- ) {
1910
- const sortString = sortOrder ? `order(${sortOrder})` : ''
1911
- const countString = isSingle ? '[0...1]' : `[${start}...${end}]`
1912
- const query = `*[${filter}]{
1913
- ${fields}
1914
- } | ${sortString}${countString}`
1915
- return query
1916
- }
1917
-
1918
- async function buildQuery(
1919
- baseFilter = '',
1920
- filterParams = { pullFutureContent: false },
1921
- fields = '...',
1922
- { sortOrder = 'published_on desc', start = 0, end = 10, isSingle = false }
1923
- ) {
1924
- const filter = await new FilterBuilder(baseFilter, filterParams).buildFilter()
1925
- return buildRawQuery(filter, fields, { sortOrder, start, end, isSingle })
1926
- }
1927
-
1928
- export function buildEntityAndTotalQuery(
1929
- filter = '',
1930
- fields = '...',
1931
- {
1932
- sortOrder = 'published_on desc',
1933
- start = 0,
1934
- end = 10,
1935
- isSingle = false,
1936
- withoutPagination = false,
1937
- }
1938
- ) {
1939
- const sortString = sortOrder ? ` | order(${sortOrder})` : ''
1940
- const countString = isSingle ? '[0...1]' : withoutPagination ? `` : `[${start}...${end}]`
1941
- const query = `{
1942
- "entity": *[${filter}] ${sortString}${countString}
1943
- {
1944
- ${fields}
1945
- },
1946
- "total": 0
1947
- }`
1948
- return query
1949
- }
1950
-
1951
- function getFilterOptions(option, commonFilter, contentType, brand) {
1952
- let filterGroq = ''
1953
- const types = Array.from(new Set([...coachLessonsTypes, ...showsTypes[brand]]))
1954
-
1955
- switch (option) {
1956
- case 'difficulty':
1957
- filterGroq = `
1958
- "difficulty": [
1959
- {"type": "All", "count": count(*[${commonFilter} &amp;&amp; difficulty_string == "All"])},
1960
- {"type": "Introductory", "count": count(*[${commonFilter} &amp;&amp; (difficulty_string == "Novice" || difficulty_string == "Introductory")])},
1961
- {"type": "Beginner", "count": count(*[${commonFilter} &amp;&amp; difficulty_string == "Beginner"])},
1962
- {"type": "Intermediate", "count": count(*[${commonFilter} &amp;&amp; difficulty_string == "Intermediate" ])},
1963
- {"type": "Advanced", "count": count(*[${commonFilter} &amp;&amp; difficulty_string == "Advanced" ])},
1964
- {"type": "Expert", "count": count(*[${commonFilter} &amp;&amp; difficulty_string == "Expert" ])}
1965
- ][count > 0],`
1966
- break
1967
- case 'type':
1968
- const typesString = types
1969
- .map((t) => {
1970
- return `{"type": "${t}"}`
1971
- })
1972
- .join(', ')
1973
- filterGroq = `"type": [${typesString}]{type, 'count': count(*[_type == ^.type &amp;&amp; ${commonFilter}])}[count > 0],`
1974
- break
1975
- case 'genre':
1976
- case 'essential':
1977
- case 'focus':
1978
- case 'theory':
1979
- case 'topic':
1980
- case 'lifestyle':
1981
- case 'creativity':
1982
- filterGroq = `
1983
- "${option}": *[_type == '${option}' ${contentType ? ` &amp;&amp; '${contentType}' in filter_types` : ''} ] {
1984
- "type": name,
1985
- "count": count(*[${commonFilter} &amp;&amp; references(^._id)])
1986
- }[count > 0],`
1987
- break
1988
- case 'instrumentless':
1989
- filterGroq = `
1990
- "${option}": [
1991
- {"type": "Full Song Only", "count": count(*[${commonFilter} &amp;&amp; instrumentless == false ])},
1992
- {"type": "Instrument Removed", "count": count(*[${commonFilter} &amp;&amp; instrumentless == true ])}
1993
- ][count > 0],`
1994
- break
1995
- case 'gear':
1996
- filterGroq = `
1997
- "${option}": [
1998
- {"type": "Practice Pad", "count": count(*[${commonFilter} &amp;&amp; gear match 'Practice Pad' ])},
1999
- {"type": "Drum-Set", "count": count(*[${commonFilter} &amp;&amp; gear match 'Drum-Set'])}
2000
- ][count > 0],`
2001
- break
2002
- case 'bpm':
2003
- filterGroq = `
2004
- "${option}": [
2005
- {"type": "50-90", "count": count(*[${commonFilter} &amp;&amp; bpm > 50 &amp;&amp; bpm &lt; 91])},
2006
- {"type": "91-120", "count": count(*[${commonFilter} &amp;&amp; bpm > 90 &amp;&amp; bpm &lt; 121])},
2007
- {"type": "121-150", "count": count(*[${commonFilter} &amp;&amp; bpm > 120 &amp;&amp; bpm &lt; 151])},
2008
- {"type": "151-180", "count": count(*[${commonFilter} &amp;&amp; bpm > 150 &amp;&amp; bpm &lt; 181])},
2009
- {"type": "180+", "count": count(*[${commonFilter} &amp;&amp; bpm > 180])},
2010
- ][count > 0],`
2011
- break
2012
- default:
2013
- filterGroq = ''
2014
- break
2015
- }
2016
-
2017
- return filterGroq
2018
- }
2019
-
2020
- function cleanUpGroq(query) {
2021
- // Split the query into clauses based on the logical operators
2022
- const clauses = query.split(/(\s*&amp;&amp;|\s*\|\|)/).map((clause) => clause.trim())
2023
-
2024
- // Filter out empty clauses
2025
- const filteredClauses = clauses.filter((clause) => clause.length > 0)
2026
-
2027
- // Check if there are valid conditions in the clauses
2028
- const hasConditions = filteredClauses.some((clause) => !clause.match(/^\s*&amp;&amp;\s*|\s*\|\|\s*$/))
2029
-
2030
- if (!hasConditions) {
2031
- // If no valid conditions, return an empty string or the original query
2032
- return ''
2033
- }
2034
-
2035
- // Remove occurrences of '&amp;&amp; ()'
2036
- const cleanedQuery = filteredClauses
2037
- .join(' ')
2038
- .replace(/&amp;&amp;\s*\(\)/g, '')
2039
- .replace(/(\s*&amp;&amp;|\s*\|\|)(?=\s*[\s()]*$|(?=\s*&amp;&amp;|\s*\|\|))/g, '')
2040
- .trim()
2041
-
2042
- return cleanedQuery
2043
- }
2044
-
2045
- // V2 methods
2046
-
2047
- export async function fetchTabData(
2048
- brand,
2049
- pageName,
2050
- {
2051
- page = 1,
2052
- limit = 10,
2053
- sort = '-published_on',
2054
- includedFields = [],
2055
- progressIds = undefined,
2056
- progress = 'all',
2057
- showMembershipRestrictedContent = false,
2058
- } = {}
2059
- ) {
2060
- const start = (page - 1) * limit
2061
- const end = start + limit
2062
- // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
2063
- const includedFieldsFilter =
2064
- includedFields.length > 0 ? filtersToGroq(includedFields, [], pageName) : ''
2065
-
2066
- let sortOrder = getSortOrder(sort, brand, '')
2067
-
2068
- switch (progress) {
2069
- case 'recent':
2070
- progressIds = await getAllStartedOrCompleted({ brand, onlyIds: true })
2071
- sortOrder = null
2072
- break
2073
- case 'incomplete':
2074
- progressIds = await getAllStarted()
2075
- sortOrder = null
2076
- break
2077
- case 'completed':
2078
- progressIds = await getAllCompleted()
2079
- sortOrder = null
2080
- break
2081
- }
2082
-
2083
- // limits the results to supplied progressIds for started &amp; completed filters
2084
- const progressFilter = await getProgressFilter(progress, progressIds)
2085
- const fieldsString = getFieldsForContentType('tab-data')
2086
- const now = getSanityDate(new Date())
2087
-
2088
- // Determine the group by clause
2089
- let query = ''
2090
- let entityFieldsString = ''
2091
- let filter = ''
2092
-
2093
- filter = `brand == "${brand}" &amp;&amp; (defined(railcontent_id)) ${includedFieldsFilter} ${progressFilter}`
2094
- const childrenFilter = await new FilterBuilder(``, {
2095
- isChildrenFilter: true,
2096
- showMembershipRestrictedContent: true,
2097
- }).buildFilter()
2098
- const childrenFields = await getChildFieldsForContentType('tab-data')
2099
- const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()
2100
- entityFieldsString = ` ${fieldsString}
2101
- 'children': child[${childrenFilter}]->{ ${childrenFields} 'children': child[${childrenFilter}]->{ ${childrenFields} }, },
2102
- 'isLive': live_event_start_time &lt;= "${now}" &amp;&amp; live_event_end_time >= "${now}",
2103
- 'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
2104
- 'length_in_seconds': coalesce(
2105
- math::sum(
2106
- select(
2107
- child[${childrenFilter}]->length_in_seconds
2108
- )
2109
- ),
2110
- length_in_seconds
2111
- ),`
2112
- const filterWithRestrictions = await new FilterBuilder(filter, {
2113
- showMembershipRestrictedContent: true,
2114
- }).buildFilter()
2115
- query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
2116
- sortOrder: sortOrder,
2117
- start: start,
2118
- end: end,
2119
- })
2120
-
2121
- let results = await fetchSanity(query, true, { processNeedAccess: true })
2122
-
2123
- if (['recent', 'incomplete', 'completed'].includes(progress) &amp;&amp; results.entity.length > 1) {
2124
- const orderMap = new Map(progressIds.map((id, index) => [id, index]))
2125
- results.entity = results.entity
2126
- .sort((a, b) => {
2127
- const aIdx = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER
2128
- const bIdx = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER
2129
- return aIdx - bIdx || new Date(b.published_on) - new Date(a.published_on)
2130
- })
2131
- .slice(start, end)
2132
- }
2133
-
2134
- return results
2135
- }
2136
-
2137
- export async function fetchRecent(
2138
- brand,
2139
- pageName,
2140
- { page = 1, limit = 10, sort = '-published_on', includedFields = [], progress = 'recent' } = {}
2141
- ) {
2142
- const mergedIncludedFields = [...includedFields, `tab,all`]
2143
- const results = await fetchTabData(brand, pageName, {
2144
- page,
2145
- limit,
2146
- sort,
2147
- includedFields: mergedIncludedFields,
2148
- progress: progress.toLowerCase(),
2149
- })
2150
- return results.entity
2151
- }
2152
-
2153
- export async function fetchScheduledAndNewReleases(
2154
- brand,
2155
- { page = 1, limit = 20, sort = '-published_on' } = {}
2156
- ) {
2157
- const upcomingTypes = getUpcomingEventsTypes(brand)
2158
- const newTypes = getNewReleasesTypes(brand)
2159
-
2160
- const scheduledTypes = merge(upcomingTypes, newTypes)
2161
- const typesString = arrayJoinWithQuotes(scheduledTypes)
2162
- const now = getSanityDate(new Date())
2163
-
2164
- const start = (page - 1) * limit
2165
- const end = start + limit
2166
- const sortOrder = getSortOrder(sort, brand)
2167
-
2168
- const query = `
2169
- *[_type in [${typesString}] &amp;&amp; brand == '${brand}' &amp;&amp; ((status in ['published','scheduled'] )||(show_in_new_feed == true)) ]
2170
- [${start}...${end}]
2171
- | order(published_on asc) {
2172
- "id": railcontent_id,
2173
- title,
2174
- "image": thumbnail.asset->url,
2175
- "thumbnail": thumbnail.asset->url,
2176
- ${artistOrInstructorName()},
2177
- "artists": instructor[]->name,
2178
- difficulty,
2179
- difficulty_string,
2180
- length_in_seconds,
2181
- published_on,
2182
- "type": _type,
2183
- show_in_new_feed,
2184
- "permission_id": permission[]->railcontent_id,
2185
- "isLive": live_event_start_time &lt;= '${now}' &amp;&amp; live_event_end_time >= '${now}',
2186
- }`
2187
-
2188
- return fetchSanity(query, true)
2189
- }
2190
-
2191
- export async function fetchShows(brand, type, sort = 'sort') {
2192
- const sortOrder = getSortOrder(sort, brand)
2193
- const filter = `_type == '${type}' &amp;&amp; brand == '${brand}'`
2194
- const filterParams = {}
2195
-
2196
- const query = await buildQuery(filter, filterParams, getFieldsForContentType(type), {
2197
- sortOrder: sortOrder,
2198
- end: 100, // Adrian: added for homepage progress rows, this should be handled gracefully
2199
- })
2200
- return fetchSanity(query, true)
2201
- }
2202
-
2203
- /**
2204
- * Fetch the method intro video for a given brand.
2205
- * @param brand
2206
- * @returns {Promise&lt;*|null>}
2207
- */
2208
- export async function fetchMethodV2IntroVideo(brand) {
2209
- const type = 'method-intro'
2210
- const filter = `_type == '${type}' &amp;&amp; brand == '${brand}'`
2211
- const fields = getIntroVideoFields('method-v2')
2212
-
2213
- const query = `*[${filter}] { ${fields.join(', ')} }`
2214
- return fetchSanity(query, false)
2215
- }
2216
-
2217
- /**
2218
- * Fetch the structure (just ids) of the Method for a given brand.
2219
- * @param brand
2220
- * @returns {Promise&lt;*|null>}
2221
- */
2222
- export async function fetchMethodV2Structure(brand) {
2223
- const _type = 'method-v2'
2224
- const query = `*[_type == '${_type}' &amp;&amp; brand == '${brand}'][0...1]{
2225
- 'sanity_id': _id,
2226
- 'learning_paths': child[]->{
2227
- 'id': railcontent_id,
2228
- 'children': child[]->railcontent_id
2229
- }
2230
- }`
2231
- return await fetchSanity(query, false)
2232
- }
2233
-
2234
- /**
2235
- * Fetch the structure (just ids) of the Method of a given learning path or learning path lesson.
2236
- * @param contentId
2237
- * @returns {Promise&lt;*|null>}
2238
- */
2239
- export async function fetchMethodV2StructureFromId(contentId) {
2240
- const _type = "method-v2";
2241
- const query = `*[_type == '${_type}' &amp;&amp; brand == *[railcontent_id == ${contentId}][0].brand][0...1]{
2242
- 'sanity_id': _id,
2243
- 'learning_paths': child[]->{
2244
- 'id': railcontent_id,
2245
- 'children': child[]->railcontent_id
2246
- }
2247
- }`
2248
- return await fetchSanity(query, false);
2249
- }
2250
-
2251
- /**
2252
- * Fetch content owned by the user (excluding membership content).
2253
- * Shows only content accessible through purchases/entitlements, not through membership.
2254
- *
2255
- * @param {string} brand - The brand to filter content by
2256
- * @param {Object} options - Fetch options
2257
- * @param {Array&lt;string>} options.type - Content type(s) to filter (optional array, default: [])
2258
- * @param {number} options.page - Page number (default: 1)
2259
- * @param {number} options.limit - Items per page (default: 10)
2260
- * @param {string} options.sort - Sort field and direction (default: '-published_on')
2261
- * @returns {Promise&lt;Object>} Object with 'entity' (content array) and 'total' (count)
2262
- */
2263
- export async function fetchOwnedContent(
2264
- brand,
2265
- { type = [], page = 1, limit = 10, sort = '-published_on' } = {}
2266
- ) {
2267
- const start = (page - 1) * limit
2268
- const end = start + limit
2269
-
2270
- // Determine the sort order
2271
- const sortOrder = getSortOrder(sort, brand)
2272
-
2273
- // Build the type filter
2274
- let typeFilter = ''
2275
- if (type.length > 0) {
2276
- const typesString = type.map((t) => `'${t}'`).join(', ')
2277
- typeFilter = `&amp;&amp; _type in [${typesString}]`
2278
- }
2279
-
2280
- // Build the base filter
2281
- const filter = `brand == "${brand}" ${typeFilter}`
2282
-
2283
- // Apply owned content filter
2284
- const filterWithRestrictions = await new FilterBuilder(filter, {
2285
- showOnlyOwnedContent: true, // Key parameter: exclude membership content
2286
- }).buildFilter()
2287
-
2288
- const fieldsString = DEFAULT_FIELDS.join(',')
2289
-
2290
- const query = buildEntityAndTotalQuery(filterWithRestrictions, fieldsString, {
2291
- sortOrder: sortOrder,
2292
- start: start,
2293
- end: end,
2294
- })
2295
-
2296
- return fetchSanity(query, true)
2297
- }
2298
- </code></pre>
2299
- </article>
2300
- </section>
2301
-
2302
-
2303
-
2304
-
2305
-
2306
-
2307
- </div>
2308
-
2309
- <br class="clear">
2310
-
2311
- <footer>
2312
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.3</a> on Fri Nov 28 2025 11:22:56 GMT-0800 (Pacific Standard Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
2313
- </footer>
2314
-
2315
- <script>prettyPrint();</script>
2316
- <script src="scripts/polyfill.js"></script>
2317
- <script src="scripts/linenumber.js"></script>
2318
-
2319
-
2320
-
2321
- </body>
2322
- </html>