musora-content-services 1.6.5 → 1.6.7

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