musora-content-services 2.84.0 → 2.85.1

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 (219) 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 +15 -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 +2 -5
  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 +24 -126
  25. package/docs/content-org_playlists-types.js.html +2 -2
  26. package/docs/content-org_playlists.js.html +2 -2
  27. package/docs/content.js.html +10 -88
  28. package/docs/content_artist.ts.html +0 -0
  29. package/docs/content_content.ts.html +0 -0
  30. package/docs/content_genre.ts.html +0 -0
  31. package/docs/content_instructor.ts.html +0 -0
  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 +3 -22
  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 +12 -26
  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 +0 -0
  65. package/docs/module-Awards.html +6 -106
  66. package/docs/module-Categories.html +0 -0
  67. package/docs/module-Config.html +4 -5
  68. package/docs/module-Content-Services-V2.html +9 -440
  69. package/docs/module-ForumCategories.html +0 -0
  70. package/docs/module-ForumDiscussions.html +0 -0
  71. package/docs/module-Forums.html +43 -607
  72. package/docs/module-Genre.html +0 -0
  73. package/docs/module-GuidedCourses.html +2 -2
  74. package/docs/module-Instructor.html +0 -0
  75. package/docs/module-Interests.html +2 -2
  76. package/docs/module-LearningPaths.html +12 -640
  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 +2 -2
  81. package/docs/module-ProgressRow.html +2 -2
  82. package/docs/module-Railcontent-Services.html +298 -31
  83. package/docs/module-Sanity-Services.html +1901 -530
  84. package/docs/module-Sessions.html +2 -2
  85. package/docs/module-Threads.html +0 -0
  86. package/docs/module-UserActivity.html +5 -5
  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 +3 -3
  93. package/docs/railcontent.js.html +20 -8
  94. package/docs/sanity.js.html +313 -199
  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 +5 -17
  107. package/docs/user_account.ts.html +13 -14
  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 +3 -3
  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/filterBuilder.js +0 -0
  126. package/src/index.d.ts +2 -2
  127. package/src/index.js +2 -2
  128. package/src/infrastructure/http/HttpClient.ts +0 -0
  129. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  130. package/src/infrastructure/http/index.ts +0 -0
  131. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  132. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  133. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  134. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  135. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  136. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  137. package/src/lib/httpHelper.js +0 -0
  138. package/src/lib/lastUpdated.js +0 -0
  139. package/src/services/api/types.js +0 -0
  140. package/src/services/api/types.ts +0 -0
  141. package/src/services/config.js +0 -0
  142. package/src/services/content/artist.ts +0 -0
  143. package/src/services/content/content.ts +0 -0
  144. package/src/services/content/genre.ts +0 -0
  145. package/src/services/content/instructor.ts +0 -0
  146. package/src/services/content-org/content-org.js +0 -0
  147. package/src/services/content-org/guided-courses.ts +0 -0
  148. package/src/services/content-org/learning-paths.ts +0 -0
  149. package/src/services/content-org/playlists-types.js +0 -0
  150. package/src/services/content-org/playlists.js +0 -0
  151. package/src/services/content.js +0 -0
  152. package/src/services/contentAggregator.js +10 -23
  153. package/src/services/contentLikes.js +0 -0
  154. package/src/services/contentProgress.js +250 -147
  155. package/src/services/dataContext.js +0 -0
  156. package/src/services/dateUtils.js +0 -0
  157. package/src/services/eventsAPI.js +0 -0
  158. package/src/services/forums/categories.ts +0 -0
  159. package/src/services/forums/forums.ts +0 -0
  160. package/src/services/forums/posts.ts +0 -0
  161. package/src/services/forums/threads.ts +7 -7
  162. package/src/services/forums/types.ts +1 -0
  163. package/src/services/gamification/awards.ts +0 -0
  164. package/src/services/gamification/gamification.js +0 -0
  165. package/src/services/imageSRCBuilder.js +0 -0
  166. package/src/services/imageSRCVerify.js +0 -0
  167. package/src/services/liveTesting.ts +0 -0
  168. package/src/services/permissions/PermissionsAdapter.ts +0 -0
  169. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  170. package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
  171. package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
  172. package/src/services/permissions/README.md +0 -0
  173. package/src/services/permissions/index.ts +0 -0
  174. package/src/services/progress-row/method-card.js +0 -0
  175. package/src/services/railcontent.js +18 -6
  176. package/src/services/recommendations.js +0 -0
  177. package/src/services/sanity.js +18 -2
  178. package/src/services/types.js +0 -0
  179. package/src/services/user/account.ts +0 -0
  180. package/src/services/user/chat.js +0 -0
  181. package/src/services/user/interests.js +0 -0
  182. package/src/services/user/management.js +0 -0
  183. package/src/services/user/memberships.ts +0 -0
  184. package/src/services/user/notifications.js +0 -0
  185. package/src/services/user/onboarding.ts +0 -0
  186. package/src/services/user/payments.ts +0 -0
  187. package/src/services/user/permissions.js +0 -0
  188. package/src/services/user/profile.js +0 -0
  189. package/src/services/user/sessions.js +0 -0
  190. package/src/services/user/types.d.ts +0 -0
  191. package/src/services/user/types.js +0 -0
  192. package/src/services/user/user-management-system.js +0 -0
  193. package/src/services/userActivity.js +3 -14
  194. package/test/HttpClient.test.js +0 -0
  195. package/test/content.test.js +0 -0
  196. package/test/contentLikes.test.js +0 -0
  197. package/test/contentProgress.test.js +0 -0
  198. package/test/dataContext.test.js +0 -0
  199. package/test/forum.test.js +0 -0
  200. package/test/imageSRCBuilder.test.js +0 -0
  201. package/test/imageSRCVerify.test.js +0 -0
  202. package/test/initializeTests.js +0 -0
  203. package/test/learningPaths.test.js +0 -0
  204. package/test/lib/lastUpdated.test.js +0 -0
  205. package/test/live/contentProgressLive.test.js +0 -0
  206. package/test/live/railcontentLive.test.js +0 -0
  207. package/test/localStorageMock.js +0 -0
  208. package/test/log.js +0 -0
  209. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  210. package/test/mockData/mockData_progress_content.json +0 -0
  211. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  212. package/test/mockData/mockData_user_practices.json +0 -0
  213. package/test/notifications.test.js +0 -0
  214. package/test/progressRows.test.js +0 -0
  215. package/test/sanityQueryService.test.js +0 -0
  216. package/test/streakMessage.test.js +0 -0
  217. package/test/user/permissions.test.js +0 -0
  218. package/test/userActivity.test.js +0 -0
  219. package/tools/generate-index.cjs +0 -0
@@ -6,7 +6,10 @@ import {
6
6
  postRecordWatchSession,
7
7
  } from './railcontent.js'
8
8
  import { DataContext, ContentProgressVersionKey } from './dataContext.js'
9
- import { fetchHierarchy } from './sanity.js'
9
+ import {
10
+ fetchHierarchy,
11
+ fetchMethodV2StructureFromId
12
+ } from './sanity.js'
10
13
  import { recordUserPractice, findIncompleteLesson } from './userActivity'
11
14
  import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
12
15
 
@@ -17,90 +20,41 @@ const DATA_KEY_PROGRESS = 'p'
17
20
  const DATA_KEY_RESUME_TIME = 't'
18
21
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
19
22
  const DATA_KEY_BRAND = 'b'
23
+ const DATA_KEY_COLLECTION = 'c'
24
+ const DATA_CONTENT_ID = 'i'
20
25
 
21
26
  export let dataContext = new DataContext(ContentProgressVersionKey, fetchContentProgress)
22
27
 
23
28
  let sessionData = []
24
29
 
25
- export async function getProgressPercentage(contentId) {
26
- return getById(contentId, DATA_KEY_PROGRESS, 0)
30
+ export async function getProgressPercentage(contentId, collection = null) {
31
+ return getById(contentId, collection, DATA_KEY_PROGRESS, 0)
27
32
  }
28
33
 
29
- export async function getProgressPercentageByIds(contentIds) {
30
- return getByIds(contentIds, DATA_KEY_PROGRESS, 0)
34
+ export async function getProgressPercentageByIds(contentIds, collection = null) {
35
+ return getByIds(contentIds, collection, DATA_KEY_PROGRESS, 0)
31
36
  }
32
37
 
33
- export async function getProgressState(contentId) {
34
- return getById(contentId, DATA_KEY_STATUS, '')
38
+ export async function getProgressState(contentId, collection = null) {
39
+ return getById(contentId, collection, DATA_KEY_STATUS, '')
35
40
  }
36
41
 
37
- export async function getProgressStateByIds(contentIds) {
38
- return getByIds(contentIds, DATA_KEY_STATUS, '')
42
+ export async function getProgressStateByIds(contentIds, collection = null) {
43
+ return getByIds(contentIds, collection, DATA_KEY_STATUS, '')
39
44
  }
40
45
 
41
- export async function getResumeTimeSeconds(contentId) {
42
- return getById(contentId, DATA_KEY_RESUME_TIME, 0)
46
+ export async function getResumeTimeSeconds(contentId, collection = null) {
47
+ return getById(contentId, collection, DATA_KEY_RESUME_TIME, 0)
43
48
  }
44
49
 
45
- export async function getResumeTimeSecondsByIds(contentIds) {
46
- return getByIds(contentIds, DATA_KEY_RESUME_TIME, 0)
50
+ export async function getResumeTimeSecondsByIds(contentIds, collection = null) {
51
+ return getByIds(contentIds, collection, DATA_KEY_RESUME_TIME, 0)
47
52
  }
48
53
 
49
- export async function getNextLesson(data) {
50
- let nextLessonData = {}
51
-
52
- for (const content of data) {
53
- // Skip null/undefined entries (can happen when GROQ dereference doesn't match filter)
54
- if (!content) continue
55
-
56
- const children = content.children?.filter(Boolean).map((child) => child.id) ?? []
57
- //only calculate nextLesson if needed, based on content type
58
- if (!getNextLessonLessonParentTypes.includes(content.type)) {
59
- nextLessonData[content.id] = null
60
- } else {
61
- //return first child if parent-content is complete or no progress
62
- const contentState = await getProgressState(content.id)
63
- if (contentState !== STATE_STARTED) {
64
- nextLessonData[content.id] = children[0]
65
- } else {
66
- const childrenStates = await getProgressStateByIds(children)
67
-
68
- //calculate last_engaged
69
- const lastInteracted = await getLastInteractedOf(children)
70
- const lastInteractedStatus = childrenStates[lastInteracted]
71
-
72
- //different nextLesson behaviour for different content types
73
- if (content.type === 'course' || content.type === 'pack-bundle' || content.type === 'skill-pack') {
74
- if (lastInteractedStatus === STATE_STARTED) {
75
- nextLessonData[content.id] = lastInteracted
76
- } else {
77
- nextLessonData[content.id] = findIncompleteLesson(
78
- childrenStates,
79
- lastInteracted,
80
- content.type
81
- )
82
- }
83
- } else if (content.type === 'guided-course' || content.type === 'song-tutorial') {
84
- nextLessonData[content.id] = findIncompleteLesson(
85
- childrenStates,
86
- lastInteracted,
87
- content.type
88
- )
89
- } else if (content.type === 'pack') {
90
- const packBundles = content.children ?? []
91
- const packBundleProgressData = await getNextLesson(packBundles)
92
- const parentId = await getLastInteractedOf(packBundles.map((bundle) => bundle.id))
93
- nextLessonData[content.id] = packBundleProgressData[parentId]
94
- }
95
- }
96
- }
97
- }
98
- return nextLessonData
99
- }
100
-
101
- export async function getNavigateTo(data) {
54
+ export async function getNavigateTo(data, collection = null) {
102
55
  let navigateToData = {}
103
- const twoDepthContentTypes = ['pack'] //TODO add method when we know what it's called
56
+
57
+ const twoDepthContentTypes = ['pack'] // not adding method because it has its own logic (with active path)
104
58
  //TODO add parent hierarchy upwards as well
105
59
  // data structure is the same but instead of child{} we use parent{}
106
60
  for (const content of data) {
@@ -125,31 +79,32 @@ export async function getNavigateTo(data) {
125
79
  children.set(child.id, child)
126
80
  })
127
81
  // return first child (or grand child) if parent-content is complete or no progress
128
- const contentState = await getProgressState(content.id)
82
+ const contentState = await getProgressState(content.id, collection)
129
83
  if (contentState !== STATE_STARTED) {
130
84
  const firstChild = validChildren[0]
131
85
  let lastInteractedChildNavToData = await getNavigateTo([firstChild])
132
86
  lastInteractedChildNavToData = lastInteractedChildNavToData[firstChild.id] ?? null
133
- navigateToData[content.id] = buildNavigateTo(firstChild, lastInteractedChildNavToData)
87
+ navigateToData[content.id] = buildNavigateTo(firstChild, lastInteractedChildNavToData, collection) //no G-child for LP
134
88
  } else {
135
- const childrenStates = await getProgressStateByIds(childrenIds)
136
- const lastInteracted = await getLastInteractedOf(childrenIds)
89
+ const childrenStates = await getProgressStateByIds(childrenIds, collection)
90
+ const lastInteracted = await getLastInteractedOf(childrenIds, collection)
137
91
  const lastInteractedStatus = childrenStates[lastInteracted]
138
92
 
139
- if (content.type === 'course' || content.type === 'pack-bundle' || content.type === 'skill-pack') {
140
- if (lastInteractedStatus === STATE_STARTED) {
141
- navigateToData[content.id] = buildNavigateTo(children.get(lastInteracted))
142
- } else {
93
+ if (['course', 'pack-bundle', 'skill-pack'].includes(content.type)) {
94
+ if (lastInteractedStatus === STATE_STARTED) { // send to last interacted
95
+ navigateToData[content.id] = buildNavigateTo(children.get(lastInteracted), null, collection)
96
+ } else { // send to first incomplete after last interacted
143
97
  let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
144
- navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild))
98
+ navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild), null, collection)
145
99
  }
146
- } else if (content.type === 'guided-course' || content.type === 'song-tutorial') {
100
+ } else if (['song-tutorial', 'guided-course', 'learning-path-v2'].includes(content.type)) { // send to first incomplete
147
101
  let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
148
- navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild))
149
- } else if (twoDepthContentTypes.includes(content.type)) {
102
+ navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild), null, collection)
103
+ } else if (twoDepthContentTypes.includes(content.type)) { // send to navigateTo child of last interacted child
150
104
  const firstChildren = content.children ?? []
151
105
  const lastInteractedChildId = await getLastInteractedOf(
152
- firstChildren.map((child) => child.id)
106
+ firstChildren.map((child) => child.id),
107
+ collection
153
108
  )
154
109
  if (childrenStates[lastInteractedChildId] === STATE_COMPLETED) {
155
110
  // TODO: packs have an extra situation where we need to jump to the next course if all lessons in the last engaged course are completed
@@ -158,7 +113,8 @@ export async function getNavigateTo(data) {
158
113
  lastInteractedChildNavToData = lastInteractedChildNavToData[lastInteractedChildId]
159
114
  navigateToData[content.id] = buildNavigateTo(
160
115
  children.get(lastInteractedChildId),
161
- lastInteractedChildNavToData
116
+ lastInteractedChildNavToData,
117
+ collection
162
118
  )
163
119
  }
164
120
  }
@@ -167,7 +123,7 @@ export async function getNavigateTo(data) {
167
123
  return navigateToData
168
124
  }
169
125
 
170
- function buildNavigateTo(content, child = null) {
126
+ function buildNavigateTo(content, child = null, collection = null) {
171
127
  if (!content) {
172
128
  return null
173
129
  }
@@ -180,16 +136,18 @@ function buildNavigateTo(content, child = null) {
180
136
  published_on: content.published_on ?? null,
181
137
  status: content.status ?? '',
182
138
  child: child,
139
+ collection: collection,
183
140
  }
184
141
  }
185
142
 
186
143
  /**
187
144
  * filter through contents, only keeping the most recent
188
145
  * @param {array} contentIds
146
+ * @param {object|null} collection
189
147
  * @returns {Promise<number>}
190
148
  */
191
- export async function getLastInteractedOf(contentIds) {
192
- const data = await getByIds(contentIds, DATA_KEY_LAST_UPDATED_TIME, 0)
149
+ export async function getLastInteractedOf(contentIds, collection = null) {
150
+ const data = await getByIds(contentIds, collection, DATA_KEY_LAST_UPDATED_TIME, 0)
193
151
  const sorted = Object.keys(data)
194
152
  .map(function (key) {
195
153
  return parseInt(key)
@@ -205,40 +163,43 @@ export async function getLastInteractedOf(contentIds) {
205
163
  return sorted[0]
206
164
  }
207
165
 
208
- export async function getProgressDateByIds(contentIds) {
166
+ export async function getProgressDateByIds(contentIds, collection = null) {
209
167
  let data = await dataContext.getData()
210
168
  let progress = {}
211
- contentIds?.forEach(
212
- (id) =>
213
- (progress[id] = {
214
- last_update: data[id]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
215
- progress: data[id]?.[DATA_KEY_PROGRESS] ?? 0,
216
- status: data[id]?.[DATA_KEY_STATUS] ?? '',
217
- })
218
- )
169
+ contentIds?.forEach((id) => {
170
+ const key = generateRecordKey(id, collection)
171
+ progress[id] = {
172
+ last_update: data[key]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
173
+ progress: data[key]?.[DATA_KEY_PROGRESS] ?? 0,
174
+ status: data[key]?.[DATA_KEY_STATUS] ?? '',
175
+ }
176
+ })
219
177
  return progress
220
178
  }
221
179
 
222
- async function getById(contentId, dataKey, defaultValue) {
180
+ async function getById(contentId, collection, dataKey, defaultValue) {
223
181
  let data = await dataContext.getData()
224
- return data[contentId]?.[dataKey] ?? defaultValue
182
+ const contentKey = generateRecordKey(contentId, collection)
183
+ return data[contentKey]?.[dataKey] ?? defaultValue
225
184
  }
226
185
 
227
- async function getByIds(contentIds, dataKey, defaultValue) {
186
+ async function getByIds(contentIds, collection, dataKey, defaultValue) {
228
187
  let data = await dataContext.getData()
229
188
  let progress = {}
230
- contentIds?.forEach((id) => (progress[id] = data[id]?.[dataKey] ?? defaultValue))
189
+ contentIds?.forEach((id) => (progress[id] = data[generateRecordKey(id, collection)]?.[dataKey] ?? defaultValue))
231
190
  return progress
232
191
  }
233
192
 
234
- export async function getAllStarted(limit = null) {
193
+ export async function getAllStarted(limit = null, collection = null) {
235
194
  const data = await dataContext.getData()
195
+
236
196
  let ids = Object.keys(data)
237
- .filter(function (key) {
238
- return data[parseInt(key)][DATA_KEY_STATUS] === STATE_STARTED
197
+ .filter(function (id) {
198
+ const key = generateRecordKey(id, collection)
199
+ return data[key][DATA_KEY_STATUS] === STATE_STARTED
239
200
  })
240
- .map(function (key) {
241
- return parseInt(key)
201
+ .map(function (id) {
202
+ return parseInt(id)
242
203
  })
243
204
  .sort(function (a, b) {
244
205
  let v1 = data[a][DATA_KEY_LAST_UPDATED_TIME]
@@ -253,14 +214,16 @@ export async function getAllStarted(limit = null) {
253
214
  return ids
254
215
  }
255
216
 
256
- export async function getAllCompleted(limit = null) {
217
+ export async function getAllCompleted(limit = null, collection = null) {
257
218
  const data = await dataContext.getData()
219
+
258
220
  let ids = Object.keys(data)
259
- .filter(function (key) {
260
- return data[parseInt(key)][DATA_KEY_STATUS] === STATE_COMPLETED
221
+ .filter(function (id) {
222
+ const key = generateRecordKey(id, collection)
223
+ return data[key][DATA_KEY_STATUS] === STATE_COMPLETED
261
224
  })
262
- .map(function (key) {
263
- return parseInt(key)
225
+ .map(function (id) {
226
+ return parseInt(id)
264
227
  })
265
228
  .sort(function (a, b) {
266
229
  let v1 = data[a][DATA_KEY_LAST_UPDATED_TIME]
@@ -280,21 +243,24 @@ export async function getAllStartedOrCompleted({
280
243
  onlyIds = true,
281
244
  brand = null,
282
245
  excludedIds = [],
246
+ collection = null,
283
247
  } = {}) {
284
248
  const data = await dataContext.getData()
285
249
  const oneMonthAgoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
286
250
 
287
251
  const excludedSet = new Set(excludedIds.map((id) => parseInt(id))) // ensure IDs are numbers
288
-
289
252
  let filtered = Object.entries(data)
290
253
  .filter(([key, item]) => {
291
- const id = parseInt(key)
292
254
  const isRelevantStatus =
293
255
  item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
294
256
  const isRecent = item[DATA_KEY_LAST_UPDATED_TIME] >= oneMonthAgoInSeconds
295
257
  const isCorrectBrand = !brand || !item.b || item.b === brand
296
- const isNotExcluded = !excludedSet.has(id)
297
- return isRelevantStatus && isCorrectBrand && isNotExcluded
258
+ const isNotExcluded = !excludedSet.has(extractContentIdFromRecordKey(key))
259
+ const matchesCollection =
260
+ (!collection && !item[DATA_KEY_COLLECTION]) ||
261
+ (item[DATA_KEY_COLLECTION]?.type === collection?.type &&
262
+ item[DATA_KEY_COLLECTION]?.id === collection?.id)
263
+ return matchesCollection && isRelevantStatus && isCorrectBrand && isNotExcluded
298
264
  })
299
265
  .sort(([, a], [, b]) => {
300
266
  const v1 = a[DATA_KEY_LAST_UPDATED_TIME]
@@ -303,21 +269,28 @@ export async function getAllStartedOrCompleted({
303
269
  else if (v1 < v2) return 1
304
270
  return 0
305
271
  })
272
+ //maps to content_id
273
+ .reduce((acc, [key, item]) => {
274
+ const newKey = extractContentIdFromRecordKey(key)
275
+ acc[newKey] = item
276
+ return acc
277
+ }, {})
306
278
 
307
279
  if (limit) {
308
- filtered = filtered.slice(0, limit)
280
+ filtered = Object.fromEntries(Object.entries(filtered).slice(0, limit))
309
281
  }
310
282
 
311
283
  if (onlyIds) {
312
- return filtered.map(([key]) => parseInt(key))
284
+ return Object.entries(filtered).map(([key, data]) => parseInt(key))
313
285
  } else {
314
286
  const progress = {}
315
- filtered.forEach(([key, item]) => {
287
+ Object.entries(filtered).forEach(([key, item]) => {
316
288
  const id = parseInt(key)
317
289
  progress[id] = {
318
290
  last_update: item?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
319
291
  progress: item?.[DATA_KEY_PROGRESS] ?? 0,
320
292
  status: item?.[DATA_KEY_STATUS] ?? '',
293
+ collection: item?.[DATA_KEY_COLLECTION],
321
294
  brand: item?.b ?? '',
322
295
  }
323
296
  })
@@ -342,17 +315,24 @@ export async function getAllStartedOrCompleted({
342
315
  * const progressMap = await getStartedOrCompletedProgressOnly({ brand: 'drumeo' });
343
316
  * console.log(progressMap[123]); // => 52
344
317
  */
345
- export async function getStartedOrCompletedProgressOnly({ brand = null } = {}) {
318
+ export async function getStartedOrCompletedProgressOnly({
319
+ brand = null,
320
+ collection = null
321
+ } = {}) {
346
322
  const data = await dataContext.getData()
347
323
  const result = {}
348
324
 
349
325
  Object.entries(data).forEach(([key, item]) => {
350
- const id = parseInt(key)
326
+ const id = extractContentIdFromRecordKey(key)
351
327
  const isRelevantStatus =
352
328
  item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
353
329
  const isCorrectBrand = !brand || item.b === brand
330
+ const matchesCollection =
331
+ (!collection && !item[DATA_KEY_COLLECTION]) ||
332
+ (item[DATA_KEY_COLLECTION]?.type === collection?.type &&
333
+ item[DATA_KEY_COLLECTION]?.id === collection?.id)
354
334
 
355
- if (isRelevantStatus && isCorrectBrand) {
335
+ if (matchesCollection && isRelevantStatus && isCorrectBrand) {
356
336
  result[id] = item?.[DATA_KEY_PROGRESS] ?? 0
357
337
  }
358
338
  })
@@ -360,36 +340,37 @@ export async function getStartedOrCompletedProgressOnly({ brand = null } = {}) {
360
340
  return result
361
341
  }
362
342
 
363
- export async function contentStatusCompleted(contentId) {
343
+ export async function contentStatusCompleted(contentId, collection = null) {
364
344
  return await dataContext.update(
365
345
  async function (localContext) {
366
- let hierarchy = await fetchHierarchy(contentId)
367
- completeStatusInLocalContext(localContext, contentId, hierarchy)
346
+ let hierarchy = await getContentHierarchy(contentId, collection)
347
+ completeStatusInLocalContext(localContext, contentId, hierarchy, collection)
368
348
  },
369
349
  async function () {
370
- return postContentComplete(contentId)
350
+ return postContentComplete(contentId, collection)
371
351
  }
372
352
  )
373
353
  }
374
- export async function contentStatusStarted(contentId) {
354
+ export async function contentStatusStarted(contentId, collection = null) {
375
355
  return await dataContext.update(
376
356
  async function (localContext) {
377
- let hierarchy = await fetchHierarchy(contentId)
378
- startStatusInLocalContext(localContext, contentId, hierarchy)
357
+ let hierarchy = await getContentHierarchy(contentId, collection)
358
+ startStatusInLocalContext(localContext, contentId, hierarchy, collection)
379
359
  },
380
360
  async function () {
381
- return postContentStart(contentId)
361
+ return postContentStart(contentId, collection)
382
362
  }
383
363
  )
384
364
  }
385
365
 
386
- function saveContentProgress(localContext, contentId, progress, currentSeconds, hierarchy) {
366
+ function saveContentProgress(localContext, contentId, progress, currentSeconds, hierarchy, collection = null) {
387
367
  if (progress === 100) {
388
- completeStatusInLocalContext(localContext, contentId, hierarchy)
368
+ completeStatusInLocalContext(localContext, contentId, hierarchy, collection)
389
369
  return
390
370
  }
391
371
 
392
- let data = localContext.data[contentId] ?? {}
372
+ const key = generateRecordKey(contentId, collection)
373
+ let data = localContext.data[key] ?? {}
393
374
  const currentProgress = data[DATA_KEY_STATUS]
394
375
  if (!currentProgress || currentProgress !== STATE_COMPLETED) {
395
376
  data[DATA_KEY_PROGRESS] = progress
@@ -397,32 +378,43 @@ function saveContentProgress(localContext, contentId, progress, currentSeconds,
397
378
  }
398
379
  data[DATA_KEY_RESUME_TIME] = currentSeconds
399
380
  data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
400
- localContext.data[contentId] = data
381
+ localContext.data[key] = data
401
382
 
402
383
  bubbleProgress(hierarchy, contentId, localContext)
403
384
  }
404
385
 
405
- function completeStatusInLocalContext(localContext, contentId, hierarchy) {
406
- setStartedOrCompletedStatusInLocalContext(localContext, contentId, true, hierarchy)
386
+ function completeStatusInLocalContext(localContext, contentId, hierarchy, collection = null) {
387
+ setStartedOrCompletedStatusInLocalContext(localContext, contentId, true, hierarchy, collection)
407
388
  }
408
389
 
409
- function startStatusInLocalContext(localContext, contentId, hierarchy) {
410
- setStartedOrCompletedStatusInLocalContext(localContext, contentId, false, hierarchy)
390
+ function startStatusInLocalContext(localContext, contentId, hierarchy, collection = null) {
391
+ setStartedOrCompletedStatusInLocalContext(localContext, contentId, false, hierarchy, collection)
411
392
  }
412
393
 
413
394
  function setStartedOrCompletedStatusInLocalContext(
414
395
  localContext,
415
396
  contentId,
416
397
  isCompleted,
417
- hierarchy
398
+ hierarchy,
399
+ collection = null
418
400
  ) {
419
- let data = localContext.data[contentId] ?? {}
401
+ const key = generateRecordKey(contentId, collection)
402
+ let data = localContext.data[key] ?? {}
420
403
  data[DATA_KEY_PROGRESS] = isCompleted ? 100 : 0
421
404
  data[DATA_KEY_STATUS] = isCompleted ? STATE_COMPLETED : STATE_STARTED
422
405
  data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
423
- localContext.data[contentId] = data
406
+ data[DATA_KEY_COLLECTION] = collection
407
+ data[DATA_CONTENT_ID] = contentId
408
+
409
+ localContext.data[key] = data
424
410
 
425
411
  if (!hierarchy) return
412
+
413
+ if (collection && collection.type === 'learning-path') {
414
+ bubbleOrTrickleLearningPathProgress(hierarchy, contentId, localContext, isCompleted, collection)
415
+ return
416
+ }
417
+
426
418
  let children = hierarchy.children[contentId] ?? []
427
419
  for (let i = 0; i < children.length; i++) {
428
420
  let childId = children[i]
@@ -440,29 +432,52 @@ function getChildrenToDepth(parentId, hierarchy, depth = 1) {
440
432
  return allChildrenIds
441
433
  }
442
434
 
443
- export async function contentStatusReset(contentId) {
435
+ export async function contentStatusReset(contentId, collection = null) {
444
436
  await dataContext.update(
445
437
  async function (localContext) {
446
- let hierarchy = await fetchHierarchy(contentId)
447
- resetStatusInLocalContext(localContext, contentId, hierarchy)
438
+ let hierarchy = await getContentHierarchy(contentId, collection)
439
+ resetStatusInLocalContext(localContext, contentId, hierarchy, collection)
448
440
  },
449
441
  async function () {
450
- return postContentReset(contentId)
442
+ return postContentReset(contentId, collection)
451
443
  }
452
444
  )
453
445
  }
454
446
 
455
- function resetStatusInLocalContext(localContext, contentId, hierarchy) {
456
- let allChildIds = getChildrenToDepth(contentId, hierarchy, 5)
457
- allChildIds.push(contentId)
447
+ function resetStatusInLocalContext(localContext, contentId, hierarchy, collection = null) {
448
+ let keys = []
449
+
450
+ console.log('all', [localContext, contentId, hierarchy, collection])
451
+ keys.push(generateRecordKey(contentId, collection))
452
+
453
+ let allChildIds
454
+ let learningPathId = null
455
+ let childrenIds = []
456
+ if (collection && collection.type === 'learning-path') {
457
+ [learningPathId, childrenIds] = findLearningPathAndChildren(hierarchy, contentId)
458
+ allChildIds = (learningPathId === contentId) ? childrenIds : []
459
+ } else {
460
+ allChildIds = getChildrenToDepth(contentId, hierarchy, 5)
461
+ }
462
+
458
463
  allChildIds.forEach((id) => {
459
- const index = Object.keys(localContext.data).indexOf(id.toString())
464
+ keys.push(generateRecordKey(id, collection))
465
+ })
466
+
467
+ keys.forEach((key) => {
468
+ const index = Object.keys(localContext.data).indexOf(key.toString())
460
469
  if (index > -1) {
461
470
  // only splice array when item is found
462
- delete localContext.data[id]
471
+ delete localContext.data[key]
463
472
  }
464
473
  })
465
- bubbleProgress(hierarchy, contentId, localContext)
474
+ if (collection && collection.type === 'learning-path') { // manual bubbling for LP
475
+ if (learningPathId !== contentId) {
476
+ bubbleLearningPathProgress(hierarchy, contentId, localContext, collection)
477
+ }
478
+ } else {
479
+ bubbleProgress(hierarchy, contentId, localContext)
480
+ }
466
481
  }
467
482
 
468
483
  /**
@@ -477,7 +492,9 @@ function resetStatusInLocalContext(localContext, contentId, hierarchy) {
477
492
  * @param {string} sessionId - This function records a sessionId to pass into future updates to progress on the same video
478
493
  * @param {int} instrumentId - enum value of instrument id
479
494
  * @param {int} categoryId - enum value of category id
495
+ * @param {object|null} collection - optional collection info { type: 'learning-path', id: 123 }
480
496
  */
497
+ // NOTE: have not set up collection because its not super important for testing and this will change soon with watermelon
481
498
  export async function recordWatchSession(
482
499
  contentId,
483
500
  mediaType,
@@ -487,8 +504,14 @@ export async function recordWatchSession(
487
504
  secondsPlayed,
488
505
  sessionId = null,
489
506
  instrumentId = null,
490
- categoryId = null
507
+ categoryId = null,
508
+ collection = null
491
509
  ) {
510
+ if (collection && collection.type === 'learning-path') {
511
+ console.log('Learning Path lesson watch sessions are not set up yet without watermelon')
512
+ return sessionId
513
+ }
514
+
492
515
  let mediaTypeId = getMediaTypeId(mediaType, mediaCategory)
493
516
  let updateLocalProgress = mediaTypeId === 1 || mediaTypeId === 2 //only update for video playback
494
517
  if (!sessionId) {
@@ -580,3 +603,83 @@ function bubbleProgress(hierarchy, contentId, localContext) {
580
603
  localContext.data[parentId] = data
581
604
  bubbleProgress(hierarchy, parentId, localContext)
582
605
  }
606
+
607
+ function bubbleLearningPathProgress(hierarchy, contentId, localContext, collection) {
608
+ const [parentId, childrenIds] = findLearningPathAndChildren(hierarchy, contentId)
609
+
610
+ if (!parentId || parentId === contentId) return
611
+
612
+ const parentKey = generateRecordKey(parentId, collection)
613
+ let data = localContext.data[parentKey] ?? {}
614
+
615
+ let childProgress = childrenIds.map(function (childId) {
616
+ const childKey = generateRecordKey(childId, collection)
617
+ return localContext.data[childKey]?.[DATA_KEY_PROGRESS] ?? 0
618
+ })
619
+ let progress = Math.round(childProgress.reduce((a, b) => a + b, 0) / childProgress.length)
620
+
621
+ const contentKey = generateRecordKey(contentId, collection)
622
+ const brand = localContext.data[contentKey]?.[DATA_KEY_BRAND] ?? null
623
+
624
+ data[DATA_KEY_PROGRESS] = progress
625
+ data[DATA_KEY_STATUS] = progress === 100 ? STATE_COMPLETED : STATE_STARTED
626
+ data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
627
+ data[DATA_KEY_BRAND] = brand
628
+ data[DATA_KEY_COLLECTION] = collection
629
+ data[DATA_CONTENT_ID] = parentId
630
+
631
+ localContext.data[parentKey] = data
632
+ }
633
+
634
+ function generateRecordKey(contentId, collection) {
635
+ return collection ? `${contentId}:${collection.type}:${collection.id}` : contentId
636
+ }
637
+
638
+ function extractContentIdFromRecordKey(key) {
639
+ return parseInt(key.split(':')[0])
640
+ }
641
+
642
+ async function getContentHierarchy(contentId, collection = null) {
643
+ if (collection && collection.type === 'learning-path') {
644
+ return fetchMethodV2StructureFromId(contentId)
645
+ }
646
+ return await fetchHierarchy(contentId)
647
+ }
648
+
649
+ function findLearningPathAndChildren(data, contentId) {
650
+ let learningPathId = null
651
+ let children = []
652
+
653
+ if (!data?.learningPaths) return [ learningPathId, children ]
654
+
655
+ for (const lp of data.learningPaths) {
656
+ if (lp.id === contentId) {
657
+ learningPathId = contentId
658
+ children = lp.children ?? []
659
+ break
660
+ }
661
+ if (Array.isArray(lp.children) && lp.children.includes(contentId)) {
662
+ learningPathId = lp.id
663
+ children = lp.children ?? []
664
+ break
665
+ }
666
+ }
667
+
668
+ return [learningPathId, children]
669
+ }
670
+
671
+ function bubbleOrTrickleLearningPathProgress(hierarchy, contentId, localContext, isCompleted, collection) {
672
+ const [parentId, childrenIds] = findLearningPathAndChildren(hierarchy, contentId)
673
+
674
+ if (parentId !== contentId) { // if contentId is not a learning path
675
+ bubbleLearningPathProgress(hierarchy, contentId, localContext, collection)
676
+ return
677
+ }
678
+
679
+ if (childrenIds) {
680
+ for (let i = 0; i < childrenIds.length; i++) {
681
+ let childId = childrenIds[i]
682
+ setStartedOrCompletedStatusInLocalContext(localContext, childId, isCompleted, null, collection)
683
+ }
684
+ }
685
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes