musora-content-services 2.80.0 → 2.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.coderabbit.yaml +0 -0
  3. package/.editorconfig +0 -0
  4. package/.github/pull_request_template.md +0 -0
  5. package/.github/workflows/conventional-commits.yaml +0 -0
  6. package/.github/workflows/docs.js.yml +0 -0
  7. package/.github/workflows/node.js.yml +0 -0
  8. package/.prettierignore +0 -0
  9. package/.prettierrc +0 -0
  10. package/.yarnrc.yml +1 -0
  11. package/CHANGELOG.md +25 -0
  12. package/README.md +0 -0
  13. package/babel.config.cjs +0 -0
  14. package/docs/Content.html +0 -0
  15. package/docs/ContentOrganization.html +2 -2
  16. package/docs/Forums.html +2 -2
  17. package/docs/Gamification.html +2 -2
  18. package/docs/TestUser.html +2 -2
  19. package/docs/UserManagementSystem.html +2 -2
  20. package/docs/api_types.js.html +2 -2
  21. package/docs/config.js.html +5 -2
  22. package/docs/content-org_content-org.js.html +2 -2
  23. package/docs/content-org_guided-courses.ts.html +2 -2
  24. package/docs/content-org_learning-paths.ts.html +5 -2
  25. package/docs/content-org_playlists-types.js.html +2 -2
  26. package/docs/content-org_playlists.js.html +3 -2
  27. package/docs/content.js.html +88 -10
  28. package/docs/content_artist.ts.html +2 -3
  29. package/docs/content_content.ts.html +0 -0
  30. package/docs/content_genre.ts.html +2 -2
  31. package/docs/content_instructor.ts.html +2 -2
  32. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  33. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  34. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  35. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  36. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  37. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  38. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  39. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  40. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  41. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  42. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  43. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  44. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  45. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  46. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  47. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  48. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  49. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  50. package/docs/forums_categories.ts.html +2 -2
  51. package/docs/forums_discussions.js.html +0 -0
  52. package/docs/forums_forum.js.html +0 -0
  53. package/docs/forums_forums.ts.html +2 -2
  54. package/docs/forums_posts.ts.html +2 -2
  55. package/docs/forums_threads.ts.html +2 -2
  56. package/docs/gamification_awards.js.html +0 -0
  57. package/docs/gamification_awards.ts.html +2 -2
  58. package/docs/gamification_gamification.js.html +2 -2
  59. package/docs/gamification_types.js.html +0 -0
  60. package/docs/global.html +2 -2
  61. package/docs/index.html +2 -2
  62. package/docs/liveTesting.ts.html +2 -2
  63. package/docs/module-Accounts.html +14 -14
  64. package/docs/module-Artist.html +5 -5
  65. package/docs/module-Awards.html +2 -2
  66. package/docs/module-Categories.html +0 -0
  67. package/docs/module-Config.html +5 -4
  68. package/docs/module-Content-Services-V2.html +440 -9
  69. package/docs/module-ForumCategories.html +0 -0
  70. package/docs/module-ForumDiscussions.html +0 -0
  71. package/docs/module-Forums.html +2 -2
  72. package/docs/module-Genre.html +2 -2
  73. package/docs/module-GuidedCourses.html +2 -2
  74. package/docs/module-Instructor.html +2 -2
  75. package/docs/module-Interests.html +2 -2
  76. package/docs/module-LearningPaths.html +4 -4
  77. package/docs/module-Onboarding.html +2 -2
  78. package/docs/module-Payments.html +2 -2
  79. package/docs/module-Permissions.html +2 -2
  80. package/docs/module-Playlists.html +16 -16
  81. package/docs/module-ProgressRow.html +2 -2
  82. package/docs/module-Railcontent-Services.html +2 -2
  83. package/docs/module-Sanity-Services.html +1797 -56
  84. package/docs/module-Sessions.html +2 -2
  85. package/docs/module-Threads.html +0 -0
  86. package/docs/module-UserActivity.html +4 -4
  87. package/docs/module-UserChat.html +2 -2
  88. package/docs/module-UserManagement.html +2 -2
  89. package/docs/module-UserMemberships.html +2 -2
  90. package/docs/module-UserNotifications.html +2 -2
  91. package/docs/module-UserProfile.html +2 -2
  92. package/docs/progress-row_method-card.js.html +2 -2
  93. package/docs/railcontent.js.html +2 -2
  94. package/docs/sanity.js.html +131 -29
  95. package/docs/scripts/collapse.js +0 -0
  96. package/docs/scripts/commonNav.js +0 -0
  97. package/docs/scripts/linenumber.js +0 -0
  98. package/docs/scripts/nav.js +0 -0
  99. package/docs/scripts/polyfill.js +0 -0
  100. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  101. package/docs/scripts/prettify/lang-css.js +0 -0
  102. package/docs/scripts/prettify/prettify.js +0 -0
  103. package/docs/scripts/search.js +0 -0
  104. package/docs/styles/jsdoc.css +0 -0
  105. package/docs/styles/prettify.css +0 -0
  106. package/docs/userActivity.js.html +3 -2
  107. package/docs/user_account.ts.html +11 -4
  108. package/docs/user_chat.js.html +2 -2
  109. package/docs/user_interests.js.html +2 -2
  110. package/docs/user_management.js.html +2 -2
  111. package/docs/user_memberships.js.html +0 -0
  112. package/docs/user_memberships.ts.html +2 -2
  113. package/docs/user_notifications.js.html +2 -2
  114. package/docs/user_onboarding.ts.html +2 -2
  115. package/docs/user_payments.ts.html +2 -2
  116. package/docs/user_permissions.js.html +2 -2
  117. package/docs/user_profile.js.html +2 -2
  118. package/docs/user_sessions.js.html +2 -2
  119. package/docs/user_types.js.html +2 -2
  120. package/docs/user_user-management-system.js.html +2 -2
  121. package/jest.config.js +0 -0
  122. package/jsdoc.json +0 -0
  123. package/package.json +1 -1
  124. package/src/contentMetaData.js +0 -0
  125. package/src/contentTypeConfig.js +33 -1
  126. package/src/filterBuilder.js +22 -12
  127. package/src/index.d.ts +4 -0
  128. package/src/index.js +4 -0
  129. package/src/infrastructure/http/HttpClient.ts +0 -0
  130. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  131. package/src/infrastructure/http/index.ts +0 -0
  132. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  133. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  134. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  135. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  136. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  137. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  138. package/src/lib/httpHelper.js +0 -0
  139. package/src/lib/lastUpdated.js +4 -4
  140. package/src/services/api/types.js +0 -0
  141. package/src/services/api/types.ts +0 -0
  142. package/src/services/config.js +3 -0
  143. package/src/services/content/artist.ts +0 -0
  144. package/src/services/content/content.ts +0 -0
  145. package/src/services/content/genre.ts +0 -0
  146. package/src/services/content/instructor.ts +0 -0
  147. package/src/services/content-org/content-org.js +0 -0
  148. package/src/services/content-org/guided-courses.ts +0 -0
  149. package/src/services/content-org/learning-paths.ts +3 -0
  150. package/src/services/content-org/playlists-types.js +0 -0
  151. package/src/services/content-org/playlists.js +0 -0
  152. package/src/services/content.js +86 -8
  153. package/src/services/contentAggregator.js +4 -4
  154. package/src/services/contentLikes.js +0 -0
  155. package/src/services/contentProgress.js +16 -3
  156. package/src/services/dataContext.js +0 -0
  157. package/src/services/dateUtils.js +0 -0
  158. package/src/services/eventsAPI.js +0 -0
  159. package/src/services/forums/categories.ts +0 -0
  160. package/src/services/forums/forums.ts +0 -0
  161. package/src/services/forums/posts.ts +0 -0
  162. package/src/services/forums/threads.ts +0 -0
  163. package/src/services/forums/types.ts +0 -0
  164. package/src/services/gamification/awards.ts +0 -0
  165. package/src/services/gamification/gamification.js +0 -0
  166. package/src/services/imageSRCBuilder.js +0 -0
  167. package/src/services/imageSRCVerify.js +0 -0
  168. package/src/services/liveTesting.ts +0 -0
  169. package/src/services/permissions/PermissionsAdapter.ts +111 -0
  170. package/src/services/permissions/PermissionsAdapterFactory.ts +71 -0
  171. package/src/services/permissions/PermissionsV1Adapter.ts +232 -0
  172. package/src/services/permissions/PermissionsV2Adapter.ts +226 -0
  173. package/src/services/permissions/README.md +139 -0
  174. package/src/services/permissions/index.ts +65 -0
  175. package/src/services/progress-row/method-card.js +0 -0
  176. package/src/services/railcontent.js +0 -0
  177. package/src/services/recommendations.js +0 -0
  178. package/src/services/sanity.js +103 -40
  179. package/src/services/types.js +1 -0
  180. package/src/services/user/account.ts +9 -2
  181. package/src/services/user/chat.js +0 -0
  182. package/src/services/user/interests.js +0 -0
  183. package/src/services/user/management.js +0 -0
  184. package/src/services/user/memberships.ts +0 -0
  185. package/src/services/user/notifications.js +0 -0
  186. package/src/services/user/onboarding.ts +0 -0
  187. package/src/services/user/payments.ts +0 -0
  188. package/src/services/user/permissions.js +1 -1
  189. package/src/services/user/profile.js +0 -0
  190. package/src/services/user/sessions.js +0 -0
  191. package/src/services/user/types.d.ts +133 -0
  192. package/src/services/user/types.js +0 -0
  193. package/src/services/user/user-management-system.js +0 -0
  194. package/src/services/userActivity.js +1 -0
  195. package/test/HttpClient.test.js +0 -0
  196. package/test/content.test.js +5 -0
  197. package/test/contentLikes.test.js +0 -0
  198. package/test/contentProgress.test.js +0 -0
  199. package/test/dataContext.test.js +0 -0
  200. package/test/forum.test.js +1 -1
  201. package/test/imageSRCBuilder.test.js +0 -0
  202. package/test/imageSRCVerify.test.js +0 -0
  203. package/test/initializeTests.js +5 -3
  204. package/test/learningPaths.test.js +0 -0
  205. package/test/lib/lastUpdated.test.js +0 -0
  206. package/test/live/contentProgressLive.test.js +0 -0
  207. package/test/live/railcontentLive.test.js +0 -0
  208. package/test/localStorageMock.js +0 -0
  209. package/test/log.js +0 -0
  210. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  211. package/test/mockData/mockData_progress_content.json +0 -0
  212. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  213. package/test/mockData/mockData_user_practices.json +0 -0
  214. package/test/notifications.test.js +0 -0
  215. package/test/progressRows.test.js +0 -0
  216. package/test/sanityQueryService.test.js +0 -0
  217. package/test/streakMessage.test.js +0 -0
  218. package/test/user/permissions.test.js +0 -0
  219. package/test/userActivity.test.js +0 -0
  220. package/tools/generate-index.cjs +5 -0
@@ -29,7 +29,7 @@ import { globalConfig } from './config.js'
29
29
 
30
30
  import { fetchNextContentDataForParent, fetchHandler } from './railcontent.js'
31
31
  import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
32
- import { fetchUserPermissions } from './user/permissions.js'
32
+ import { getPermissionsAdapter } from './permissions/index.ts'
33
33
  import { getAllCompleted, getAllStarted, getAllStartedOrCompleted } from './contentProgress.js'
34
34
  import { fetchRecentActivitiesActiveTabs } from './userActivity.js'
35
35
 
@@ -491,7 +491,7 @@ export async function fetchByRailContentIds(
491
491
  }
492
492
  return results.map(liveProcess)
493
493
  }
494
- const results = await fetchSanity(query, true, { customPostProcess: customPostProcess })
494
+ const results = await fetchSanity(query, true, { customPostProcess: customPostProcess, processNeedAccess: true })
495
495
 
496
496
  const sortFuction = function compare(a, b) {
497
497
  const indexA = ids.indexOf(a['id'])
@@ -511,10 +511,8 @@ export async function fetchContentRows(brand, pageName, contentRowSlug) {
511
511
  if (pageName === 'lessons') pageName = 'lesson'
512
512
  if (pageName === 'songs') pageName = 'song'
513
513
  const rowString = contentRowSlug ? ` && slug.current == "${contentRowSlug.toLowerCase()}"` : ''
514
- const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {
515
- pullFutureContent: true,
516
- }).buildFilter()
517
- const childFilter = await new FilterBuilder('', { isChildrenFilter: true }).buildFilter()
514
+ const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {pullFutureContent: true, showMembershipRestrictedContent: true}).buildFilter()
515
+ const childFilter = await new FilterBuilder('', {isChildrenFilter: true, showMembershipRestrictedContent: true}).buildFilter()
518
516
  const query = `*[_type == 'recommended-content-row' && brand == '${brand}' && type == '${pageName}'${rowString}]{
519
517
  brand,
520
518
  name,
@@ -527,7 +525,7 @@ export async function fetchContentRows(brand, pageName, contentRowSlug) {
527
525
  'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
528
526
  },
529
527
  }`
530
- return fetchSanity(query, true)
528
+ return fetchSanity(query, true, {processNeedAccess: true})
531
529
  }
532
530
 
533
531
  /**
@@ -816,7 +814,9 @@ export async function fetchAllFilterOptions(
816
814
 
817
815
  const includedFieldsFilter = filters?.length ? filtersToGroq(filters) : undefined
818
816
  const progressFilter = progressIds ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
819
- const isAdmin = (await fetchUserPermissions()).isAdmin
817
+ const adapter = getPermissionsAdapter()
818
+ const userPermissionsData = await adapter.fetchUserPermissions()
819
+ const isAdmin = adapter.isAdmin(userPermissionsData)
820
820
 
821
821
  const constructCommonFilter = (excludeFilter) => {
822
822
  const filterWithoutOption = excludeFilter
@@ -1085,7 +1085,7 @@ export async function jumpToContinueContent(railcontentId) {
1085
1085
  * .catch(error => console.error(error));
1086
1086
  */
1087
1087
  export async function fetchLessonContent(railContentId, { addParent = false } = {}) {
1088
- const filterParams = { isSingle: true, pullFutureContent: true }
1088
+ const filterParams = { isSingle: true, pullFutureContent: true, showMembershipRestrictedContent: true }
1089
1089
 
1090
1090
  const parentQuery = addParent
1091
1091
  ? `"parent_content_data": *[railcontent_id in [...(^.parent_content_data[].id)]]{
@@ -1146,7 +1146,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
1146
1146
  return result
1147
1147
  }
1148
1148
 
1149
- return fetchSanity(query, false, { customPostProcess: chapterProcess })
1149
+ return fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true })
1150
1150
  }
1151
1151
 
1152
1152
  /**
@@ -1237,16 +1237,21 @@ async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, cou
1237
1237
  export async function fetchSiblingContent(railContentId, brand = null) {
1238
1238
  const filterGetParent = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
1239
1239
  pullFutureContent: true,
1240
+ showMembershipRestrictedContent: true // Show parent even without permissions
1240
1241
  }).buildFilter()
1241
1242
  const filterForParentList = await new FilterBuilder(
1242
1243
  `references(^._id) && _type == ^.parent_type`,
1243
1244
  {
1244
1245
  pullFutureContent: true,
1245
1246
  isParentFilter: true,
1247
+ showMembershipRestrictedContent: true // Show parent even without permissions
1246
1248
  }
1247
1249
  ).buildFilter()
1248
1250
 
1249
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1251
+ const childrenFilter = await new FilterBuilder(``, {
1252
+ isChildrenFilter: true,
1253
+ showMembershipRestrictedContent: true // Show all lessons in sidebar, need_access applied on individual page
1254
+ }).buildFilter()
1250
1255
 
1251
1256
  const brandString = brand ? ` && brand == "${brand}"` : ''
1252
1257
  const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission[]->railcontent_id, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
@@ -1260,7 +1265,7 @@ export async function fetchSiblingContent(railContentId, brand = null) {
1260
1265
  "related_lessons" : *[${filterGetParent}][0].child[${childrenFilter}]->{${queryFields}}
1261
1266
  }`
1262
1267
 
1263
- let result = await fetchSanity(query, false)
1268
+ let result = await fetchSanity(query, false, { processNeedAccess: true })
1264
1269
 
1265
1270
  //there's no way in sanity to retrieve the index of an array, so we must calculate after fetch
1266
1271
  if (result['for-calculations'] && result['for-calculations']['parents-list']) {
@@ -1288,10 +1293,12 @@ export async function fetchRelatedLessons(railContentId) {
1288
1293
  const defaultFilterFields = `_type==^._type && brand == ^.brand && railcontent_id != ${railContentId}`
1289
1294
 
1290
1295
  const filterSameArtist = await new FilterBuilder(
1291
- `${defaultFilterFields} && references(^.artist->_id)`
1296
+ `${defaultFilterFields} && references(^.artist->_id)`,
1297
+ { showMembershipRestrictedContent: true }
1292
1298
  ).buildFilter()
1293
1299
  const filterSameGenre = await new FilterBuilder(
1294
- `${defaultFilterFields} && references(^.genre[]->_id)`
1300
+ `${defaultFilterFields} && references(^.genre[]->_id)`,
1301
+ { showMembershipRestrictedContent: true }
1295
1302
  ).buildFilter()
1296
1303
 
1297
1304
  const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
@@ -1303,7 +1310,7 @@ export async function fetchRelatedLessons(railContentId) {
1303
1310
  ...(*[${filterSameGenre}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
1304
1311
  ])[0...10]}`
1305
1312
 
1306
- return await fetchSanity(query, false)
1313
+ return await fetchSanity(query, false, { processNeedAccess: true })
1307
1314
  }
1308
1315
 
1309
1316
  /**
@@ -1633,13 +1640,13 @@ export async function fetchSanity(
1633
1640
  body: JSON.stringify({ query: query }),
1634
1641
  }
1635
1642
 
1643
+ const adapter = getPermissionsAdapter()
1636
1644
  let promisesResult = await Promise.all([
1637
1645
  fetch(url, options),
1638
- processNeedAccess ? fetchUserPermissions() : null,
1646
+ processNeedAccess ? adapter.fetchUserPermissions() : null,
1639
1647
  ])
1640
1648
  const response = promisesResult[0]
1641
- const userPermissions = promisesResult[1]?.permissions
1642
- const isAdmin = promisesResult[1]?.isAdmin
1649
+ const userPermissions = promisesResult[1]
1643
1650
 
1644
1651
  if (!response.ok) {
1645
1652
  throw new Error(`Sanity API error: ${response.status} - ${response.statusText}`)
@@ -1651,7 +1658,7 @@ export async function fetchSanity(
1651
1658
  throw new Error('No results found')
1652
1659
  }
1653
1660
  results = processNeedAccess
1654
- ? await needsAccessDecorator(results, userPermissions, isAdmin)
1661
+ ? await needsAccessDecorator(results, userPermissions)
1655
1662
  : results
1656
1663
  results = processPageType
1657
1664
  ? pageTypeDecorator(results)
@@ -1669,7 +1676,17 @@ export async function fetchSanity(
1669
1676
  function contentResultsDecorator(results, fieldName, callback) {
1670
1677
  if (Array.isArray(results)) {
1671
1678
  results.forEach((result) => {
1672
- result[fieldName] = callback(result)
1679
+ // Check if this is a content row structure
1680
+ if (result.content && Array.isArray(result.content)) {
1681
+ // Content rows structure: array of rows, each with a content array
1682
+ result.content.forEach((contentItem) => {
1683
+ if (contentItem) {
1684
+ contentItem[fieldName] = callback(contentItem)
1685
+ }
1686
+ })
1687
+ } else {
1688
+ result[fieldName] = callback(result)
1689
+ }
1673
1690
  })
1674
1691
  } else if (results.entity && Array.isArray(results.entity)) {
1675
1692
  // Group By
@@ -1699,28 +1716,20 @@ function pageTypeDecorator(results) {
1699
1716
  })
1700
1717
  }
1701
1718
 
1702
- function needsAccessDecorator(results, userPermissions, isAdmin) {
1719
+
1720
+ function needsAccessDecorator(results, userPermissions) {
1703
1721
  if (globalConfig.sanityConfig.useDummyRailContentMethods) return results
1704
- userPermissions = new Set(userPermissions)
1722
+ const adapter = getPermissionsAdapter()
1705
1723
  return contentResultsDecorator(results, 'need_access', function (content) {
1706
- return doesUserNeedAccessToContent(content, userPermissions, isAdmin)
1724
+ return adapter.doesUserNeedAccess(content, userPermissions)
1707
1725
  })
1708
1726
  }
1709
1727
 
1710
- function doesUserNeedAccessToContent(result, userPermissions, isAdmin) {
1711
- if (isAdmin ?? false) {
1712
- return false
1713
- }
1714
- const permissions = new Set(result?.permission_id ?? [])
1715
- if (permissions.size === 0) {
1716
- return false
1717
- }
1718
- for (let permission of permissions) {
1719
- if (userPermissions.has(permission)) {
1720
- return false
1721
- }
1722
- }
1723
- return true
1728
+ function doesUserNeedAccessToContent(result, userPermissions) {
1729
+ // Legacy function - now delegates to adapter
1730
+ // Kept for backwards compatibility if used elsewhere
1731
+ const adapter = getPermissionsAdapter()
1732
+ return adapter.doesUserNeedAccess(result, userPermissions)
1724
1733
  }
1725
1734
 
1726
1735
  /**
@@ -1992,6 +2001,7 @@ export async function fetchTabData(
1992
2001
  includedFields = [],
1993
2002
  progressIds = undefined,
1994
2003
  progress = 'all',
2004
+ showMembershipRestrictedContent = false,
1995
2005
  } = {}
1996
2006
  ) {
1997
2007
  const start = (page - 1) * limit
@@ -2028,7 +2038,7 @@ export async function fetchTabData(
2028
2038
  let filter = ''
2029
2039
 
2030
2040
  filter = `brand == "${brand}" && (defined(railcontent_id)) ${includedFieldsFilter} ${progressFilter}`
2031
- const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
2041
+ const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true, showMembershipRestrictedContent: true }).buildFilter()
2032
2042
  const childrenFields = await getChildFieldsForContentType('tab-data')
2033
2043
  const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()
2034
2044
  entityFieldsString = ` ${fieldsString}
@@ -2043,14 +2053,14 @@ export async function fetchTabData(
2043
2053
  ),
2044
2054
  length_in_seconds
2045
2055
  ),`
2046
- const filterWithRestrictions = await new FilterBuilder(filter, {}).buildFilter()
2056
+ const filterWithRestrictions = await new FilterBuilder(filter, {showMembershipRestrictedContent: true}).buildFilter()
2047
2057
  query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
2048
2058
  sortOrder: sortOrder,
2049
2059
  start: start,
2050
2060
  end: end,
2051
2061
  })
2052
2062
 
2053
- let results = await fetchSanity(query, true)
2063
+ let results = await fetchSanity(query, true, {processNeedAccess: true});
2054
2064
 
2055
2065
  if (['recent', 'incomplete', 'completed'].includes(progress) && results.entity.length > 1) {
2056
2066
  const orderMap = new Map(progressIds.map((id, index) => [id, index]))
@@ -2162,3 +2172,56 @@ export async function fetchMethodV2Structure(brand) {
2162
2172
  }`
2163
2173
  return await fetchSanity(query, false)
2164
2174
  }
2175
+
2176
+ /**
2177
+ * Fetch content owned by the user (excluding membership content).
2178
+ * Shows only content accessible through purchases/entitlements, not through membership.
2179
+ *
2180
+ * @param {string} brand - The brand to filter content by
2181
+ * @param {Object} options - Fetch options
2182
+ * @param {Array<string>} options.type - Content type(s) to filter (optional array, default: [])
2183
+ * @param {number} options.page - Page number (default: 1)
2184
+ * @param {number} options.limit - Items per page (default: 10)
2185
+ * @param {string} options.sort - Sort field and direction (default: '-published_on')
2186
+ * @returns {Promise<Object>} Object with 'entity' (content array) and 'total' (count)
2187
+ */
2188
+ export async function fetchOwnedContent(
2189
+ brand,
2190
+ {
2191
+ type = [],
2192
+ page = 1,
2193
+ limit = 10,
2194
+ sort = '-published_on',
2195
+ } = {}
2196
+ ) {
2197
+ const start = (page - 1) * limit
2198
+ const end = start + limit
2199
+
2200
+ // Determine the sort order
2201
+ const sortOrder = getSortOrder(sort, brand)
2202
+
2203
+ // Build the type filter
2204
+ let typeFilter = ''
2205
+ if (type.length > 0) {
2206
+ const typesString = type.map(t => `'${t}'`).join(', ')
2207
+ typeFilter = `&& _type in [${typesString}]`
2208
+ }
2209
+
2210
+ // Build the base filter
2211
+ const filter = `brand == "${brand}" ${typeFilter}`
2212
+
2213
+ // Apply owned content filter
2214
+ const filterWithRestrictions = await new FilterBuilder(filter, {
2215
+ showOnlyOwnedContent: true, // Key parameter: exclude membership content
2216
+ }).buildFilter()
2217
+
2218
+ const fieldsString = DEFAULT_FIELDS.join(',')
2219
+
2220
+ const query = buildEntityAndTotalQuery(filterWithRestrictions, fieldsString, {
2221
+ sortOrder: sortOrder,
2222
+ start: start,
2223
+ end: end,
2224
+ })
2225
+
2226
+ return fetchSanity(query, true)
2227
+ }
@@ -39,4 +39,5 @@
39
39
  * @property {Object} localStorage - Cache to use for localStorage
40
40
  * @property {boolean} isMA - Variable that tells if the library is used by MA or FEW
41
41
  * @property {string} localTimezoneString - The local timezone string in format: America/Vancouver
42
+ * @property {('v1'|'v2')} [permissionsVersion='v1'] - Permissions system version to use ('v1' or 'v2')
42
43
  */
@@ -4,6 +4,8 @@
4
4
  import { HttpClient } from '../../infrastructure/http/HttpClient'
5
5
  import { HttpError } from '../../infrastructure/http/interfaces/HttpError'
6
6
  import { globalConfig } from '../config.js'
7
+ import { Onboarding } from './onboarding'
8
+ import { AuthResponse } from './types'
7
9
 
8
10
  /**
9
11
  * @param {string} email - The email address to check the account status for.
@@ -42,6 +44,11 @@ export interface AccountSetupProps {
42
44
  from?: string
43
45
  }
44
46
 
47
+ export interface AccountSetupResponse {
48
+ auth: AuthResponse
49
+ onboarding: Onboarding
50
+ }
51
+
45
52
  /**
46
53
  * @param {Object} props - The parameters for setting up the account.
47
54
  * @property {string} email - The email address for the account.
@@ -51,11 +58,11 @@ export interface AccountSetupProps {
51
58
  * @property {string} [revenuecatAppUserId] - The RevenueCat App User ID for MA environments. Required for MA requests
52
59
  * @property {string} [deviceName] - The device name for MA environments. Required for MA requests
53
60
  *
54
- * @returns {Promise<void>} - A promise that resolves when the account setup is complete or an HttpError if the request fails.
61
+ * @returns {Promise<AccountSetupResponse>} - A promise that resolves when the account setup is complete or an HttpError if the request fails.
55
62
  * @throws {Error} - Throws an error if required parameters are missing based on the environment.
56
63
  * @throws {HttpError} - Throws an HttpError if the HTTP request fails.
57
64
  */
58
- export async function setupAccount(props: AccountSetupProps): Promise<void> {
65
+ export async function setupAccount(props: AccountSetupProps): Promise<AccountSetupResponse> {
59
66
  const httpClient = new HttpClient(globalConfig.baseUrl)
60
67
  if ((!globalConfig.isMA || props.from === 'mobile-ios-app') && !props.token) {
61
68
  throw new Error('Token is required for non-MA environments')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -20,7 +20,7 @@ let lastUpdatedKey = `userPermissions_lastUpdated`
20
20
  * @returns {Promise<UserPermissions>} - The user permissions data.
21
21
  */
22
22
  export async function fetchUserPermissions() {
23
- if (!userPermissionsPromise || wasLastUpdateOlderThanXSeconds(10, lastUpdatedKey)) {
23
+ if (!userPermissionsPromise || await wasLastUpdateOlderThanXSeconds(10, lastUpdatedKey)) {
24
24
  userPermissionsPromise = fetchUserPermissionsData()
25
25
  setLastUpdatedTime(lastUpdatedKey)
26
26
  }
File without changes
File without changes
@@ -0,0 +1,133 @@
1
+ export interface BrandMethodLevels {
2
+ drumeo: string
3
+ pianote: string
4
+ guitareo: string
5
+ singeo: string
6
+ }
7
+
8
+ export interface BrandTotalXp {
9
+ drumeo: string
10
+ pianote: string
11
+ guitareo: string
12
+ singeo: string
13
+ }
14
+
15
+ export interface BrandTimePracticed {
16
+ drumeo: number
17
+ pianote: number
18
+ guitareo: number
19
+ singeo: number
20
+ }
21
+
22
+ export interface User {
23
+ id: number
24
+ email: string
25
+ display_name: string
26
+ first_name: string
27
+ last_name: string
28
+ gender: string | null
29
+ country: string
30
+ region: string | null
31
+ city: string | null
32
+ birthday: string
33
+ phone_number: string | null
34
+ profile_picture_url: string
35
+ timezone: string
36
+ permission_level: string
37
+ last_used_brand: string
38
+ membership_level: string
39
+ membership_start_date: string | null
40
+ membership_expiration_date: string
41
+ is_lifetime_member: number
42
+ revenuecat_origin_app_user_id: string | null
43
+ is_drumeo_lifetime_member: number
44
+ access_level: string
45
+ total_xp: number
46
+ brand_method_levels: BrandMethodLevels
47
+ brand_total_xp: BrandTotalXp
48
+ brand_minutes_practiced: BrandTimePracticed
49
+ brand_seconds_practiced: BrandTimePracticed
50
+ guitar_playing_since_year: number | null
51
+ drumeo_onboarding_skip_setup: number
52
+ pianote_onboarding_skip_setup: number
53
+ guitareo_onboarding_skip_setup: number
54
+ singeo_onboarding_skip_setup: number
55
+ drumeo_trial_section_hide: number
56
+ pianote_trial_section_hide: number
57
+ guitareo_trial_section_hide: number
58
+ singeo_trial_section_hide: number
59
+ notify_on_lesson_comment_like: number
60
+ notifications_summary_frequency_minutes: number | null
61
+ notify_on_forum_post_reply: number
62
+ notify_on_forum_followed_thread_reply: number
63
+ notify_on_forum_post_like: number
64
+ notify_weekly_update: number
65
+ notify_on_lesson_comment_reply: number
66
+ challenges_enrollment_notifications: number | null
67
+ challenges_community_notifications: number | null
68
+ challenges_solo_notifications: number | null
69
+ send_mobile_app_push_notifications: number
70
+ send_email_notifications: number
71
+ use_legacy_video_player: number
72
+ use_student_view: boolean
73
+ show_admin_toggle: boolean
74
+ drumeo_ship_magazine: number
75
+ magazine_shipping_address_id: string | null
76
+ ios_latest_review_display_date: string | null
77
+ ios_count_review_display: number
78
+ google_latest_review_display_date: string | null
79
+ google_count_review_display: number
80
+ biography: string | null
81
+ support_note: string | null
82
+ created_at: string
83
+ updated_at: string
84
+ is_pack_owner: number
85
+ has_recharge_subscription: number
86
+ recharge_interval: string | null
87
+ has_apple_subscription: number
88
+ has_google_subscription: number
89
+ requires_password_update: number
90
+ cio_synced_workspaces: number
91
+ recharge_renewal_date: string | null
92
+ trial_expiration_date: string | null
93
+ is_trial: number
94
+ legacy_expiration_date: string | null
95
+ needs_logout: boolean
96
+ primary_brand: string
97
+ first_access_at: string
98
+ is_challenge_owner: number
99
+ login_as_users: boolean
100
+ }
101
+
102
+ export interface AuthResponse {
103
+ token: string
104
+ user: User
105
+ }
106
+
107
+ export interface UserPermissions {
108
+ permissions: string[]
109
+ isAdmin: boolean
110
+ isABasicMember: boolean
111
+ }
112
+
113
+ export interface StreakDTO {
114
+ type: 'week' | 'day'
115
+ length: number
116
+ start_date: Date | null
117
+ end_date: Date | null
118
+ }
119
+
120
+ export interface OtherStatsDTO {
121
+ longest_day_streak: StreakDTO
122
+ longest_week_streak: StreakDTO
123
+ total_practice_time: number
124
+ comment_likes: number
125
+ forum_post_likes: number
126
+ experience_points: number
127
+ }
128
+
129
+ export interface BlockedUserDTO {
130
+ id: number
131
+ display_name: string
132
+ profile_picture_url: string | null
133
+ }
File without changes
File without changes
@@ -1115,6 +1115,7 @@ export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}) {
1115
1115
  switch (item.type) {
1116
1116
  case 'playlist':
1117
1117
  return processPlaylistItem(item)
1118
+ case 'learning-path-v2':
1118
1119
  case 'method':
1119
1120
  return item
1120
1121
  default:
File without changes
@@ -1,6 +1,11 @@
1
1
  import { initializeTestService } from './initializeTests.js'
2
2
  import {getContentRows, getNewAndUpcoming, getScheduleContentRows, getTabResults} from '../src/services/content.js'
3
3
 
4
+ // Mock fetchContentProgress before other modules load
5
+ jest.mock('../src/services/railcontent.js', () => ({
6
+ ...jest.requireActual('../src/services/railcontent.js'),
7
+ fetchContentProgress: jest.fn().mockResolvedValue({ version: 1, data: {} })
8
+ }))
4
9
 
5
10
  const railContentModule = require('../src/services/railcontent.js')
6
11
 
File without changes
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
  import { initializeTestService } from './initializeTests.js'
2
2
  import { getLessonContentRows, getTabResults } from '../src/services/content.js'
3
- import {getActiveDiscussions} from "../src/services/forum";
3
+ import {getActiveDiscussions} from "../src/services/forums/forums";
4
4
 
5
5
  describe('forum', function () {
6
6
  beforeEach(() => {
File without changes
File without changes
@@ -45,7 +45,9 @@ export async function initializeTestService(useLive = false, isAdmin = false) {
45
45
  isMA: true,
46
46
  }
47
47
  initializeService(config)
48
- let mock = jest.spyOn(railContentModule, 'fetchUserPermissionsData')
49
- let testData = { permissions: [78, 91, 92], isAdmin: isAdmin }
50
- mock.mockImplementation(() => testData)
48
+
49
+ // Mock user permissions
50
+ let permissionsMock = jest.spyOn(railContentModule, 'fetchUserPermissionsData')
51
+ let permissionsData = { permissions: [108, 91, 92], isAdmin: isAdmin }
52
+ permissionsMock.mockImplementation(() => permissionsData)
51
53
  }
File without changes
File without changes
File without changes
File without changes
File without changes
package/test/log.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -73,6 +73,11 @@ treeElements.forEach((treeNode) => {
73
73
  if (fs.lstatSync(filePath).isFile()) {
74
74
  addFunctionsToFileExports(filePath, treeNode)
75
75
  } else if (fs.lstatSync(filePath).isDirectory()) {
76
+ // Skip the permissions directory - it has its own index.ts barrel export
77
+ if (treeNode === 'permissions') {
78
+ return
79
+ }
80
+
76
81
  const subDir = fs.readdirSync(filePath)
77
82
  subDir.forEach((subFile) => {
78
83
  const filePath = path.join(servicesDir, treeNode, subFile)