musora-content-services 2.104.5 → 2.104.8

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.
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(rg:*)",
5
- "Bash(npm run lint:*)"
4
+ "Bash(git show:*)",
5
+ "Bash(npm test:*)",
6
+ "Bash(done)",
7
+ "Bash(for line in 82 108 161 187 215 247 273 299 332 350 372)",
8
+ "Bash(do echo \"Line $line:\")",
9
+ "Bash(git add:*)",
10
+ "Bash(git commit:*)",
11
+ "Bash(git checkout:*)",
12
+ "Bash(git diff:*)"
6
13
  ],
7
- "deny": []
14
+ "deny": [],
15
+ "ask": []
8
16
  }
9
17
  }
File without changes
File without changes
package/.prettierignore CHANGED
File without changes
package/.prettierrc CHANGED
File without changes
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.104.8](https://github.com/railroadmedia/musora-content-services/compare/v2.104.7...v2.104.8) (2025-12-16)
6
+
7
+ ### [2.104.7](https://github.com/railroadmedia/musora-content-services/compare/v2.104.6...v2.104.7) (2025-12-16)
8
+
9
+ ### [2.104.6](https://github.com/railroadmedia/musora-content-services/compare/v2.104.5...v2.104.6) (2025-12-16)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * method card previous dailies ([#649](https://github.com/railroadmedia/musora-content-services/issues/649)) ([14f1c39](https://github.com/railroadmedia/musora-content-services/commit/14f1c390530a126d063eff3c7062e4fe2a09567c))
15
+ * **MU2-1315:** agi lessons permissions ([#657](https://github.com/railroadmedia/musora-content-services/issues/657)) ([b3ff00a](https://github.com/railroadmedia/musora-content-services/commit/b3ff00a2bb57e149981b18b5060ab2ec257db912))
16
+
5
17
  ### [2.104.5](https://github.com/railroadmedia/musora-content-services/compare/v2.104.4...v2.104.5) (2025-12-12)
6
18
 
7
19
 
package/jest.config.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.104.5",
3
+ "version": "2.104.8",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.d.ts CHANGED
@@ -396,6 +396,7 @@ import {
396
396
  } from './services/user/profile.js';
397
397
 
398
398
  import {
399
+ getAuthKey,
399
400
  login,
400
401
  loginWithAuthKey,
401
402
  logout
@@ -585,6 +586,7 @@ declare module 'musora-content-services' {
585
586
  getAllCompletedByIds,
586
587
  getAllStarted,
587
588
  getAllStartedOrCompleted,
589
+ getAuthKey,
588
590
  getAwardStatistics,
589
591
  getCompletedAwards,
590
592
  getContentAwards,
package/src/index.js CHANGED
@@ -400,6 +400,7 @@ import {
400
400
  } from './services/user/profile.js';
401
401
 
402
402
  import {
403
+ getAuthKey,
403
404
  login,
404
405
  loginWithAuthKey,
405
406
  logout
@@ -584,6 +585,7 @@ export {
584
585
  getAllCompletedByIds,
585
586
  getAllStarted,
586
587
  getAllStartedOrCompleted,
588
+ getAuthKey,
587
589
  getAwardStatistics,
588
590
  getCompletedAwards,
589
591
  getContentAwards,
@@ -1,6 +1,7 @@
1
1
  import { filtersToGroq } from '../../contentTypeConfig'
2
2
  import { getPermissionsAdapter } from '../../services/permissions/index'
3
3
  import type { UserPermissions } from '../../services/permissions/PermissionsAdapter'
4
+ import { Brands } from '../brands'
4
5
  import { filterOps } from './query'
5
6
 
6
7
  // ============================================
@@ -454,6 +455,18 @@ export class Filters {
454
455
  static progressIds(progressIds: number[]): string {
455
456
  return progressIds.length > 0 ? Filters.idIn(progressIds) : Filters.empty
456
457
  }
458
+
459
+ static async lessonCount(brand?: Brands | string): Promise<string> {
460
+ return Filters.count(
461
+ await Filters.combineAsync(
462
+ Filters.status(),
463
+ Filters.publishedDate(),
464
+ Filters.notDeprecated(),
465
+ Filters.referencesParent(),
466
+ brand ? Filters.brand(brand) : Filters.empty
467
+ )
468
+ )
469
+ }
457
470
  }
458
471
 
459
472
  // Default export
@@ -12,7 +12,7 @@ export interface Artist {
12
12
  slug: string
13
13
  name: string
14
14
  thumbnail: string
15
- lessonCount: number
15
+ lesson_count: number
16
16
  }
17
17
 
18
18
  export interface Artists {
@@ -33,35 +33,27 @@ export interface Artists {
33
33
  */
34
34
  export async function fetchArtists(
35
35
  brand: Brands | string,
36
- options: BuildQueryOptions = { sort: 'lower(name) asc' }
36
+ options: BuildQueryOptions
37
37
  ): Promise<Artists> {
38
- const lessonFilter = f.combine(f.brand(brand), f.referencesParent())
39
38
  const type = f.type('artist')
40
- const lessonCount = `count(*[${lessonFilter}])`
41
- const postFilter = `lessonCount > 0`
39
+ const postFilter = `lesson_count > 0`
40
+ const { sort = 'lower(name)', offset = 0, limit = 20 } = options
42
41
 
43
42
  const data = query()
44
43
  .and(type)
45
- .order(options?.sort || 'lower(name) asc')
46
- .slice(options?.offset || 0, options?.limit || 20)
44
+ .order(getSortOrder(sort, brand))
45
+ .slice(offset, limit)
47
46
  .select(
48
47
  'name',
49
48
  `"slug": slug.current`,
50
49
  `"thumbnail": thumbnail_url.asset->url`,
51
- `"lessonCount": ${lessonCount}`
50
+ `"lesson_count": ${await f.lessonCount(brand)}`
52
51
  )
53
52
  .postFilter(postFilter)
54
53
  .build()
55
54
 
56
- const total = query()
57
- .and(type)
58
- .select(`"lessonCount": ${lessonCount}`)
59
- .postFilter(postFilter)
60
- .build()
61
-
62
55
  const q = `{
63
56
  "data": ${data},
64
- "total": count(${total})
65
57
  }`
66
58
 
67
59
  return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
@@ -83,8 +75,6 @@ export async function fetchArtistBySlug(
83
75
  slug: string,
84
76
  brand?: Brands | string
85
77
  ): Promise<Artist | null> {
86
- const filter = f.combine(brand ? f.brand(brand) : f.empty, f.referencesParent())
87
-
88
78
  const q = query()
89
79
  .and(f.type('artist'))
90
80
  .and(f.slug(slug))
@@ -92,7 +82,7 @@ export async function fetchArtistBySlug(
92
82
  'name',
93
83
  `"slug": slug.current`,
94
84
  `"thumbnail": thumbnail_url.asset->url`,
95
- `"lessonCount": count(*[${filter}])`
85
+ `"lesson_count": ${await f.lessonCount(brand)}`
96
86
  )
97
87
  .first()
98
88
  .build()
@@ -146,15 +136,17 @@ export async function fetchArtistLessons(
146
136
  sort = getSortOrder(sort, brand)
147
137
 
148
138
  const restrictions = await f.combineAsync(
149
- f.contentFilter(),
150
- f.referencesIDWithFilter(f.combine(f.type('artist'), f.slug(slug)))
139
+ f.status(),
140
+ f.publishedDate(),
141
+ f.notDeprecated(),
142
+ f.referencesIDWithFilter(f.combine(f.type('artist'), f.slug(slug))),
143
+ f.brand(brand),
144
+ f.searchMatch('title', searchTerm),
145
+ f.includedFields(includedFields),
146
+ f.progressIds(progressIds)
151
147
  )
152
148
 
153
149
  const data = query()
154
- .and(f.brand(brand))
155
- .and(f.searchMatch('title', searchTerm))
156
- .and(f.includedFields(includedFields))
157
- .and(f.progressIds(progressIds))
158
150
  .and(restrictions)
159
151
  .order(sort)
160
152
  .slice(offset, limit)
@@ -168,5 +160,5 @@ export async function fetchArtistLessons(
168
160
  "total": count(${total})
169
161
  }`
170
162
 
171
- return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
163
+ return fetchSanity(q, true, { processNeedAccess: true, processPageType: false })
172
164
  }
@@ -11,8 +11,8 @@ import { Filters as f } from '../../lib/sanity/filter'
11
11
  export interface Genre {
12
12
  name: string
13
13
  slug: string
14
- lessons_count: number
15
14
  thumbnail: string
15
+ lesson_count: number
16
16
  }
17
17
 
18
18
  export interface Genres {
@@ -33,35 +33,27 @@ export interface Genres {
33
33
  */
34
34
  export async function fetchGenres(
35
35
  brand: Brands | string,
36
- options: BuildQueryOptions = { sort: 'lower(name) asc' }
36
+ options: BuildQueryOptions
37
37
  ): Promise<Genres> {
38
- const lesson = f.combine(f.brand(brand), f.referencesParent())
39
38
  const type = f.type('genre')
40
- const lessonCount = `count(*[${lesson}])`
41
- const postFilter = `lessonCount > 0`
39
+ const postFilter = `lesson_count > 0`
40
+ const { sort = 'lower(name)', offset = 0, limit = 20 } = options
42
41
 
43
42
  const data = query()
44
43
  .and(type)
45
- .order(options?.sort || 'lower(name) asc')
46
- .slice(options?.offset || 0, options?.limit || 20)
44
+ .order(getSortOrder(sort, brand))
45
+ .slice(offset, limit)
47
46
  .select(
48
47
  'name',
49
48
  `"slug": slug.current`,
50
49
  `"thumbnail": thumbnail_url.asset->url`,
51
- `"lessons_count": ${lessonCount}`
50
+ `"lesson_count": ${await f.lessonCount(brand)}`
52
51
  )
53
52
  .postFilter(postFilter)
54
53
  .build()
55
54
 
56
- const total = query()
57
- .and(type)
58
- .select(`"lessons_count": ${lessonCount}`)
59
- .postFilter(postFilter)
60
- .build()
61
-
62
55
  const q = `{
63
56
  "data": ${data},
64
- "total": count(${total})
65
57
  }`
66
58
 
67
59
  return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
@@ -83,8 +75,6 @@ export async function fetchGenreBySlug(
83
75
  slug: string,
84
76
  brand?: Brands | string
85
77
  ): Promise<Genre | null> {
86
- const filter = f.combine(brand ? f.brand(brand) : f.empty, f.referencesParent())
87
-
88
78
  const q = query()
89
79
  .and(f.type('genre'))
90
80
  .and(f.slug(slug))
@@ -92,7 +82,7 @@ export async function fetchGenreBySlug(
92
82
  'name',
93
83
  `"slug": slug.current`,
94
84
  `"thumbnail": thumbnail_url.asset->url`,
95
- `"lessons_count": count(*[${filter}])`
85
+ `"lesson_count": ${await f.lessonCount(brand)}`
96
86
  )
97
87
  .first()
98
88
  .build()
@@ -145,15 +135,17 @@ export async function fetchGenreLessons(
145
135
  sort = getSortOrder(sort, brand)
146
136
 
147
137
  const restrictions = await f.combineAsync(
148
- f.contentFilter(),
149
- f.referencesIDWithFilter(f.combine(f.type('genre'), f.slug(slug)))
138
+ f.status(),
139
+ f.publishedDate(),
140
+ f.notDeprecated(),
141
+ f.referencesIDWithFilter(f.combine(f.type('genre'), f.slug(slug))),
142
+ f.brand(brand),
143
+ f.searchMatch('title', searchTerm),
144
+ f.includedFields(includedFields),
145
+ f.progressIds(progressIds)
150
146
  )
151
147
 
152
148
  const data = query()
153
- .and(f.brand(brand))
154
- .and(f.searchMatch('title', searchTerm))
155
- .and(f.includedFields(includedFields))
156
- .and(f.progressIds(progressIds))
157
149
  .and(restrictions)
158
150
  .order(sort)
159
151
  .slice(offset, limit)
@@ -167,5 +159,5 @@ export async function fetchGenreLessons(
167
159
  "total": count(${total})
168
160
  }`
169
161
 
170
- return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
162
+ return fetchSanity(q, true, { processNeedAccess: true, processPageType: false })
171
163
  }
@@ -9,7 +9,7 @@ import { Brands } from '../../lib/brands'
9
9
  import { Filters as f } from '../../lib/sanity/filter'
10
10
 
11
11
  export interface Instructor {
12
- lessonCount: number
12
+ lesson_count: number
13
13
  slug: string
14
14
  name: string
15
15
  short_bio: string
@@ -37,32 +37,24 @@ export async function fetchInstructors(
37
37
  options: BuildQueryOptions
38
38
  ): Promise<Instructors> {
39
39
  const type = f.type('instructor')
40
- const lesson = f.combine(f.brand(brand), f.referencesParent())
41
- const lessonCount = `count(*[${lesson}])`
42
- const postFilter = `lessonCount > 0`
40
+ const postFilter = `lesson_count > 0`
41
+ const { sort = 'lower(name)', offset = 0, limit = 20 } = options
43
42
 
44
43
  const data = query()
45
44
  .and(type)
46
- .order(options?.sort || 'lower(name) asc')
47
- .slice(options?.offset || 0, options?.limit || 20)
45
+ .order(getSortOrder(sort, brand))
46
+ .slice(offset, limit)
48
47
  .select(
49
48
  'name',
50
49
  `"slug": slug.current`,
51
50
  `"thumbnail": thumbnail_url.asset->url`,
52
- `"lessonCount": ${lessonCount}`
51
+ `"lesson_count": ${await f.lessonCount(brand)}`
53
52
  )
54
53
  .postFilter(postFilter)
55
54
  .build()
56
55
 
57
- const total = query()
58
- .and(type)
59
- .select(`"lessonCount": ${lessonCount}`)
60
- .postFilter(postFilter)
61
- .build()
62
-
63
56
  const q = `{
64
57
  "data": ${data},
65
- "total": count(${total})
66
58
  }`
67
59
 
68
60
  return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
@@ -84,8 +76,6 @@ export async function fetchInstructorBySlug(
84
76
  slug: string,
85
77
  brand?: Brands | string
86
78
  ): Promise<Instructor | null> {
87
- const filter = f.combine(brand ? f.brand(brand) : f.empty, f.referencesParent())
88
-
89
79
  const q = query()
90
80
  .and(f.type('instructor'))
91
81
  .and(f.slug(slug))
@@ -94,7 +84,7 @@ export async function fetchInstructorBySlug(
94
84
  `"slug": slug.current`,
95
85
  'short_bio',
96
86
  `"thumbnail": thumbnail_url.asset->url`,
97
- `"lessonCount": count(*[${filter}])`
87
+ `"lesson_count": ${await f.lessonCount(brand)}`
98
88
  )
99
89
  .first()
100
90
  .build()
@@ -144,14 +134,16 @@ export async function fetchInstructorLessons(
144
134
  sort = getSortOrder(sort, brand)
145
135
 
146
136
  const restrictions = await f.combineAsync(
147
- f.contentFilter(),
148
- f.referencesIDWithFilter(f.combine(f.type('instructor'), f.slug(slug)))
137
+ f.status(),
138
+ f.publishedDate(),
139
+ f.notDeprecated(),
140
+ f.referencesIDWithFilter(f.combine(f.type('instructor'), f.slug(slug))),
141
+ f.brand(brand),
142
+ f.searchMatch('title', searchTerm),
143
+ f.includedFields(includedFields)
149
144
  )
150
145
 
151
146
  const data = query()
152
- .and(f.brand(brand))
153
- .and(f.searchMatch('title', searchTerm))
154
- .and(f.includedFields(includedFields))
155
147
  .and(restrictions)
156
148
  .order(sort)
157
149
  .slice(offset, limit)
@@ -165,5 +157,5 @@ export async function fetchInstructorLessons(
165
157
  "total": count(${total})
166
158
  }`
167
159
 
168
- return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
160
+ return fetchSanity(q, true, { processNeedAccess: true, processPageType: false })
169
161
  }
@@ -72,7 +72,7 @@ export async function getDailySession(brand: string, userDate: Date) {
72
72
  export async function updateDailySession(
73
73
  brand: string,
74
74
  userDate: Date,
75
- keepFirstLearningPath: boolean
75
+ keepFirstLearningPath: boolean = false
76
76
  ) {
77
77
  const stringDate = userDate.toISOString().split('T')[0]
78
78
  const url: string = `${LEARNING_PATHS_PATH}/daily-session/create`
@@ -194,6 +194,23 @@ export function mapContentToParent(lessons, parentContentType, parentContentId)
194
194
  })
195
195
  }
196
196
 
197
+ interface fetchLearningPathLessonsResponse {
198
+ id: number
199
+ thumbnail?: string
200
+ title: string
201
+ children: any[]
202
+ is_active_learning_path: boolean
203
+ active_learning_path_id?: number
204
+ active_learning_path_created_at?: string
205
+ upcoming_lessons?: any[]
206
+ completed_lessons?: any[]
207
+ learning_path_dailies?: any[]
208
+ next_learning_path_dailies?: any[]
209
+ next_learning_path_id?: number
210
+ previous_learning_path_dailies?: any[]
211
+ previous_learning_path_id?: number
212
+ }
213
+
197
214
  /** Fetches and organizes learning path lessons.
198
215
  *
199
216
  * @param {number} learningPathId - The learning path ID.
@@ -203,13 +220,17 @@ export function mapContentToParent(lessons, parentContentType, parentContentId)
203
220
  * @returns {number} result.id - The learning path ID.
204
221
  * @returns {string} result.thumbnail - Optional thumbnail URL for the learning path.
205
222
  * @returns {string} result.title - The title of the learning path.
206
- * @returns {boolean} result.is_active_learning_path - Whether the learning path is currently active.
207
223
  * @returns {Array} result.children - Array of all lessons.
208
- * @returns {Array} result.upcoming_lessons - Array of upcoming/additional lessons.
209
- * @returns {Array} result.todays_lessons - Array of today's lessons (max 3).
210
- * @returns {Array} result.next_learning_path_lessons - Array of next lessons to be taken.
211
- * @returns {Array} result.completed_lessons - Array of completed lessons.
212
- * @returns {Array} result.previous_learning_path_todays - Array of completed lessons.
224
+ * @returns {boolean} result.is_active_learning_path - Whether the learning path is currently active.
225
+ * @returns {number} result.active_learning_path_id - The active learning path ID from daily session.
226
+ * @returns {string} result.active_learning_path_created_at - The datetime the learning path was set as active.
227
+ * @returns {Array} result.upcoming_lessons - Array of upcoming lessons.
228
+ * @returns {Array} result.learning_path_dailies - Array of today's dailies in this learning path.
229
+ * @returns {Array} result.next_learning_path_dailies - Array of today's dailies in the next learning path.
230
+ * @returns {number} result.next_learning_path_id - the next learning path (after the active path).
231
+ * @returns {Array} result.completed_lessons - Array of completed lessons in this learning path.
232
+ * @returns {Array} result.previous_learning_path_dailies - Array of today's dailies in the previous learning path.
233
+ * @returns {number} result.previous_learning_path_id - the previous learning path (before the active path)
213
234
  */
214
235
  export async function fetchLearningPathLessons(
215
236
  learningPathId: number,
@@ -217,9 +238,9 @@ export async function fetchLearningPathLessons(
217
238
  userDate: Date
218
239
  ) {
219
240
  const learningPath = await getEnrichedLearningPath(learningPathId)
220
- let dailySession = await getDailySession(brand, userDate); // what if the call just fails, and a DS does exist?
241
+ let dailySession = await getDailySession(brand, userDate) as DailySessionResponse // what if the call just fails, and a DS does exist?
221
242
  if (!dailySession) {
222
- dailySession = await updateDailySession(brand, userDate, false)
243
+ dailySession = await updateDailySession(brand, userDate, false) as DailySessionResponse
223
244
  }
224
245
 
225
246
  const isActiveLearningPath = (dailySession?.active_learning_path_id || 0) == learningPathId
@@ -227,48 +248,64 @@ export async function fetchLearningPathLessons(
227
248
  return {
228
249
  ...learningPath,
229
250
  is_active_learning_path: isActiveLearningPath,
230
- }
251
+ } as fetchLearningPathLessonsResponse
231
252
  }
232
- // this assumes that the first entry is active_path, based on user flows
233
- const todayContentIds = dailySession.daily_session[0]?.content_ids || []
234
- const todayLearningPathId = dailySession.daily_session[0]?.learning_path_id
235
253
 
236
- const nextContentIds = dailySession.daily_session[1]?.content_ids || []
237
- const nextLearningPathId = dailySession.daily_session[1]?.learning_path_id
254
+ let todayContentIds = []
255
+ let todayLearningPathId = null
256
+ let nextContentIds = []
257
+ let nextLearningPathId = null
258
+ let previousContentIds = []
259
+ let previousLearningPathId = null
260
+
261
+
262
+
263
+ for (const session of dailySession.daily_session) {
264
+ if (session.learning_path_id === learningPathId) {
265
+ todayContentIds = session.content_ids || []
266
+ todayLearningPathId = session.learning_path_id
267
+ } else {
268
+ if (!todayLearningPathId) {
269
+ previousContentIds = session.content_ids || []
270
+ previousLearningPathId = session.learning_path_id
271
+ } else if (!nextLearningPathId) {
272
+ nextContentIds = session.content_ids || []
273
+ nextLearningPathId = session.learning_path_id
274
+ }
275
+ }
276
+ }
238
277
 
239
278
  const completedLessons = []
240
279
  let thisLPDailies = []
241
280
  let nextLPDailies = []
242
- let previousLearningPathTodays = []
281
+ let previousLPDailies = []
243
282
  const upcomingLessons = []
244
283
 
284
+ //previous/next never within LP
245
285
  learningPath.children.forEach((lesson: any) => {
246
286
  if (todayContentIds.includes(lesson.id)) {
247
287
  thisLPDailies.push(lesson)
248
- } else if (lesson.progressStatus === 'completed') {
288
+ } else if (lesson.progressStatus === STATE.COMPLETED) {
249
289
  completedLessons.push(lesson)
250
290
  } else {
251
291
  upcomingLessons.push(lesson)
252
292
  }
253
293
  })
254
294
 
255
- if (thisLPDailies.length == 0) {
256
- // Daily sessions first lessons are not part of the active learning path, but next lessons are
257
- // load todays lessons from previous learning path
258
- previousLearningPathTodays = await getLearningPathLessonsByIds(
259
- todayContentIds,
260
- todayLearningPathId
295
+ if (previousContentIds.length !== 0) {
296
+ previousLPDailies = await getLearningPathLessonsByIds(
297
+ previousContentIds,
298
+ previousLearningPathId
261
299
  )
262
- } else if ( // show next LP dailies if they exist
263
- nextContentIds.length > 0
264
- ) {
265
- // Daily sessions first lessons are the active learning path and the next lessons are not
266
- // load next lessons from next learning path
267
- const lessons = await getLearningPathLessonsByIds(nextContentIds, nextLearningPathId)
268
- nextLPDailies = lessons.map(lesson => ({
300
+ }
301
+ if (nextContentIds.length !== 0) {
302
+ nextLPDailies = await getLearningPathLessonsByIds(
303
+ nextContentIds,
304
+ nextLearningPathId
305
+ ).then(lessons => lessons.map(lesson => ({
269
306
  ...lesson,
270
- in_next_learning_path: STATE.COMPLETED === learningPath.progressStatus
271
- }))
307
+ in_next_learning_path: learningPath.progressStatus === STATE.COMPLETED
308
+ })))
272
309
  }
273
310
 
274
311
  return {
@@ -277,11 +314,12 @@ export async function fetchLearningPathLessons(
277
314
  active_learning_path_id: dailySession?.active_learning_path_id,
278
315
  active_learning_path_created_at: dailySession?.active_learning_path_created_at,
279
316
  upcoming_lessons: upcomingLessons,
280
- todays_lessons: thisLPDailies,
281
- next_learning_path_lessons: nextLPDailies,
282
- next_learning_path_id: nextLearningPathId,
283
317
  completed_lessons: completedLessons,
284
- previous_learning_path_todays: previousLearningPathTodays,
318
+ learning_path_dailies: thisLPDailies,
319
+ next_learning_path_dailies: nextLPDailies,
320
+ next_learning_path_id: nextLearningPathId,
321
+ previous_learning_path_dailies: previousLPDailies,
322
+ previous_learning_path_id: previousLearningPathId,
285
323
  }
286
324
  }
287
325
 
@@ -6,7 +6,7 @@ import { getActivePath, fetchLearningPathLessons } from '../content-org/learning
6
6
  import { getToday } from '../dateUtils.js'
7
7
  import { fetchMethodV2IntroVideo } from '../sanity'
8
8
  import { getProgressState } from '../contentProgress'
9
- import { COLLECTION_TYPE } from '../sync/models/ContentProgress'
9
+ import {COLLECTION_TYPE, STATE} from '../sync/models/ContentProgress'
10
10
 
11
11
  export async function getMethodCard(brand) {
12
12
  const introVideo = await fetchMethodV2IntroVideo(brand)
@@ -19,7 +19,7 @@ export async function getMethodCard(brand) {
19
19
 
20
20
  const activeLearningPath = await getActivePath(brand)
21
21
 
22
- if (introVideoProgressState !== 'completed' || !activeLearningPath) {
22
+ if (introVideoProgressState !== STATE.COMPLETED || !activeLearningPath) {
23
23
  //startLearningPath('drumeo', 422533)
24
24
  const timestamp = Math.floor(Date.now() / 1000)
25
25
  const instructorText =
@@ -43,43 +43,57 @@ export async function getMethodCard(brand) {
43
43
  progressTimestamp: timestamp,
44
44
  }
45
45
  } else {
46
- //TODO: Optimize loading of dailySessions/Path, should not need multiple requests
47
46
  const learningPath = await fetchLearningPathLessons(
48
47
  activeLearningPath.active_learning_path_id,
49
48
  brand,
50
49
  getToday()
51
50
  )
52
51
 
53
- const allCompleted = learningPath?.todays_lessons.every(
54
- (lesson) => lesson.progressStatus === 'completed'
55
- )
56
-
57
- const anyCompleted = learningPath?.todays_lessons.some(
58
- (lesson) => lesson.progressStatus === 'completed'
59
- )
60
-
61
- const noneCompleted = learningPath?.todays_lessons.every(
62
- (lesson) => lesson.progressStatus !== 'completed'
63
- )
52
+ if (!learningPath) {
53
+ return null
54
+ }
64
55
 
65
- const nextIncompleteLesson = learningPath?.todays_lessons.find(
66
- (lesson) => lesson.progressStatus !== 'completed'
67
- )
56
+ // need to calculate based on all dailies
57
+ const allDailies = [
58
+ ...learningPath.previous_learning_path_dailies,
59
+ ...learningPath.learning_path_dailies,
60
+ ...learningPath.next_learning_path_dailies
61
+ ]
62
+
63
+ let allDailiesCompleted = true;
64
+ let anyDailiesCompleted = false;
65
+ let noDailiesCompleted = true;
66
+ let nextIncompleteDaily = null;
67
+
68
+ for (const lesson of allDailies) {
69
+ if (lesson.progressStatus === STATE.COMPLETED) {
70
+ anyDailiesCompleted = true;
71
+ noDailiesCompleted = false;
72
+ } else {
73
+ allDailiesCompleted = false;
74
+ if (!nextIncompleteDaily) {
75
+ nextIncompleteDaily = lesson;
76
+ }
77
+ }
78
+ if (!allDailiesCompleted && anyDailiesCompleted && nextIncompleteDaily) {
79
+ break;
80
+ }
81
+ }
68
82
 
69
83
  // get the first incomplete lesson from upcoming and next learning path lessons
70
84
  const nextLesson = [
71
85
  ...learningPath?.upcoming_lessons,
72
- ...learningPath?.next_learning_path_lessons,
73
- ]?.find((lesson) => lesson.progressStatus !== 'completed')
86
+ ...learningPath?.next_learning_path_dailies,
87
+ ]?.find((lesson) => lesson.progressStatus !== STATE.COMPLETED)
74
88
 
75
89
  let ctaText, action
76
- if (noneCompleted) {
90
+ if (noDailiesCompleted) {
77
91
  ctaText = 'Start Session'
78
- action = getMethodActionCTA(nextIncompleteLesson)
79
- } else if (anyCompleted && !allCompleted) {
92
+ action = getMethodActionCTA(nextIncompleteDaily)
93
+ } else if (anyDailiesCompleted && !allDailiesCompleted) {
80
94
  ctaText = 'Continue Session'
81
- action = getMethodActionCTA(nextIncompleteLesson)
82
- } else if (allCompleted) {
95
+ action = getMethodActionCTA(nextIncompleteDaily)
96
+ } else if (allDailiesCompleted) {
83
97
  ctaText = nextLesson ? 'Start Next Lesson' : 'Browse Lessons'
84
98
  action = nextLesson
85
99
  ? getMethodActionCTA(nextLesson)
@@ -1306,7 +1306,7 @@ export async function fetchByReference(
1306
1306
  * @returns {Promise<int|null>}
1307
1307
  */
1308
1308
  export async function fetchTopLevelParentId(railcontentId) {
1309
- const parentFilter = "railcontent_id in [...(^.parent_content_data[].id)]"
1309
+ const parentFilter = 'railcontent_id in [...(^.parent_content_data[].id)]'
1310
1310
  const statusFilter = "&& status in ['scheduled', 'published', 'archived', 'unlisted']"
1311
1311
 
1312
1312
  const query = `*[railcontent_id == ${railcontentId}]{
@@ -1379,7 +1379,7 @@ export async function fetchHierarchy(railcontentId) {
1379
1379
  }
1380
1380
 
1381
1381
  function populateHierarchyLookups(currentLevel, data, parentId) {
1382
- const railcontentIdField = currentLevel.railcontent_id ? "railcontent_id" : "id";
1382
+ const railcontentIdField = currentLevel.railcontent_id ? 'railcontent_id' : 'id'
1383
1383
 
1384
1384
  let contentId = currentLevel[railcontentIdField]
1385
1385
  let children = currentLevel['children']
@@ -1534,6 +1534,10 @@ function contentResultsDecorator(results, fieldName, callback) {
1534
1534
  results.related_lessons.forEach((result) => {
1535
1535
  result[fieldName] = callback(result)
1536
1536
  })
1537
+ } else if (results.data && Array.isArray(results.data)) {
1538
+ results.data.forEach((result) => {
1539
+ result[fieldName] = callback(result)
1540
+ })
1537
1541
  } else {
1538
1542
  results[fieldName] = callback(results)
1539
1543
  }
@@ -2017,7 +2021,7 @@ export async function fetchMethodV2Structure(brand) {
2017
2021
  * @returns {Promise<*|null>}
2018
2022
  */
2019
2023
  export async function fetchMethodV2StructureFromId(contentId) {
2020
- const _type = "method-v2";
2024
+ const _type = 'method-v2'
2021
2025
  const query = `*[_type == '${_type}' && brand == *[railcontent_id == ${contentId}][0].brand][0...1]{
2022
2026
  'sanity_id': _id,
2023
2027
  brand,
@@ -2028,7 +2032,7 @@ export async function fetchMethodV2StructureFromId(contentId) {
2028
2032
  'children': child[]->railcontent_id
2029
2033
  }
2030
2034
  }`
2031
- return await fetchSanity(query, false);
2035
+ return await fetchSanity(query, false)
2032
2036
  }
2033
2037
 
2034
2038
  /**
@@ -97,6 +97,37 @@ export async function logout() {
97
97
  })
98
98
  }
99
99
 
100
+ /**
101
+ * @param {string|null} brand - Optional brand parameter (drumeo, pianote, guitareo, singeo)
102
+ * @returns {Promise<{data: string}>} Temporary auth key valid for 5 minutes
103
+ *
104
+ * @example
105
+ * getAuthKey('drumeo')
106
+ * .then(response => {
107
+ * const authKey = response.data
108
+ * const webViewUrl = `https://app.musora.com/page?user_id=${userId}&auth_key=${authKey}`
109
+ * })
110
+ * .catch(error => console.error(error));
111
+ */
112
+ export async function getAuthKey(brand = null) {
113
+ const baseUrl = `${globalConfig.baseUrl}/api/user-management-system`
114
+ const url = brand ? `${baseUrl}/v1/auth-key?brand=${brand}` : `${baseUrl}/v1/auth-key`
115
+
116
+ const response = await fetch(url, {
117
+ method: 'GET',
118
+ headers: {
119
+ Authorization: `Bearer 503154|sArOCAtYT3ejVnCdoZTj8ocEfQbfDWi5GTTtooQ107d93d29`,
120
+ 'Content-Type': 'application/json',
121
+ },
122
+ })
123
+
124
+ if (!response.ok) {
125
+ throw new Error(`Failed to get auth key: ${response.status}`)
126
+ }
127
+
128
+ return response.json()
129
+ }
130
+
100
131
  export async function loginWithAuthKey(userId, authKey, deviceName, deviceToken, platform) {
101
132
  const baseUrl = `${globalConfig.baseUrl}/api/user-management-system`
102
133
  return fetch(`${baseUrl}/v1/sessions/auth-key`, {
File without changes
File without changes
package/test/log.js CHANGED
File without changes