musora-content-services 2.116.0 → 2.117.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
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.117.0](https://github.com/railroadmedia/musora-content-services/compare/v2.116.0...v2.117.0) (2026-01-13)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEH-1442:** old method migration + course collection updates ([#673](https://github.com/railroadmedia/musora-content-services/issues/673)) ([24dd6bf](https://github.com/railroadmedia/musora-content-services/commit/24dd6bf6a1604ef3ee639979cf2ffab5abc05a24))
11
+
5
12
  ## [2.116.0](https://github.com/railroadmedia/musora-content-services/compare/v2.115.3...v2.116.0) (2026-01-12)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.116.0",
3
+ "version": "2.117.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,7 @@
1
+
2
+ export const MEMBERSHIP_PERMISSIONS = {
3
+ free: 134,
4
+ base: 91,
5
+ plus: 92,
6
+ lifetime: 108,
7
+ }
@@ -62,6 +62,7 @@ export const DEFAULT_FIELDS = [
62
62
  "'permission_id': permission_v2",
63
63
  'child_count',
64
64
  '"parent_id": parent_content_data[0].id',
65
+ '"grandparent_id": parent_content_data[1].id',
65
66
  ]
66
67
 
67
68
  // these are identical... why
@@ -157,13 +158,12 @@ export const showsTypes = {
157
158
  }
158
159
 
159
160
  export const coachLessonsTypes = [
161
+ 'course-collection',
160
162
  'course',
161
163
  'course-lesson',
162
164
  'coach-stream',
163
165
  'student-focus',
164
166
  'quick-tips',
165
- 'pack',
166
- 'semester-pack',
167
167
  'question-and-answer',
168
168
  'song-tutorial',
169
169
  'song-tutorial-lesson',
@@ -201,7 +201,11 @@ export const individualLessonsTypes = [
201
201
  ...studentArchivesLessonTypes,
202
202
  ]
203
203
 
204
- export const coursesLessonTypes = ['course', 'course-collection', 'guided-course']
204
+ export const coursesLessonTypes = [
205
+ 'course',
206
+ 'course-collection',
207
+ 'guided-course',
208
+ ]
205
209
 
206
210
  export const skillLessonTypes = ['skill-pack']
207
211
 
@@ -250,8 +254,7 @@ export const lessonTypesMapping = {
250
254
  export const getNextLessonLessonParentTypes = [
251
255
  'course',
252
256
  'guided-course',
253
- 'pack',
254
- 'pack-bundle',
257
+ 'course-collection',
255
258
  'song-tutorial',
256
259
  'learning-path-v2',
257
260
  'skill-pack',
@@ -266,7 +269,7 @@ export const progressTypesMapping = {
266
269
  ...studentArchivesLessonTypes,
267
270
  'documentary-lesson',
268
271
  'live',
269
- 'pack-bundle-lesson',
272
+ 'course-lesson'
270
273
  ],
271
274
  course: ['course'],
272
275
  show: showsLessonTypes,
@@ -274,7 +277,7 @@ export const progressTypesMapping = {
274
277
  songs: transcriptionsLessonTypes,
275
278
  'play along': playAlongLessonTypes,
276
279
  'guided course': ['guided-course'],
277
- pack: ['pack', 'semester-pack'],
280
+ 'course collection': ['course-collection'],
278
281
  'learning path': ['learning-path-v2'],
279
282
  'skill pack': skillLessonTypes,
280
283
  'jam track': jamTrackLessonTypes,
@@ -311,12 +314,12 @@ export const recentTypes = {
311
314
  lessons: [
312
315
  ...individualLessonsTypes,
313
316
  'course-lesson',
314
- 'pack-bundle-lesson',
315
317
  'guided-course-lesson',
316
318
  'quick-tips',
317
319
  ],
318
320
  songs: [...SONG_TYPES],
319
321
  home: [
322
+ ...skillLessonTypes,
320
323
  ...individualLessonsTypes,
321
324
  ...tutorialsLessonTypes,
322
325
  ...skillLessonTypes,
@@ -326,7 +329,7 @@ export const recentTypes = {
326
329
  'learning-path-v2',
327
330
  'live',
328
331
  'course',
329
- 'pack',
332
+ 'course-collection',
330
333
  ],
331
334
  }
332
335
 
@@ -339,7 +342,6 @@ export const ownedContentTypes = {
339
342
  ...coursesLessonTypes,
340
343
  ...skillLessonTypes,
341
344
  ...entertainmentLessonTypes,
342
- 'pack',
343
345
  ],
344
346
  songs: [
345
347
  ...tutorialsLessonTypes,
@@ -396,9 +398,6 @@ export let contentTypeConfig = {
396
398
  'guided-course': {
397
399
  includeChildFields: true,
398
400
  },
399
- 'course-collection': {
400
- individualLessonsTypes: true,
401
- },
402
401
  course: {
403
402
  fields: [
404
403
  '"lesson_count": child_count',
@@ -415,6 +414,9 @@ export let contentTypeConfig = {
415
414
  ],
416
415
  slug: 'courses',
417
416
  },
417
+ 'course-lesson': {
418
+ fields: [`"resources": ${resourcesField}`],
419
+ },
418
420
  download: {
419
421
  fields: [
420
422
  `"resource": ${resourcesField}`,
@@ -490,7 +492,7 @@ export let contentTypeConfig = {
490
492
  ],
491
493
  slug: 'play-alongs',
492
494
  },
493
- pack: {
495
+ 'course-collection': {
494
496
  fields: [
495
497
  '"lesson_count": coalesce(count(child[]->.child[]->), 0)',
496
498
  `"description": ${descriptionField}`,
@@ -516,24 +518,9 @@ export let contentTypeConfig = {
516
518
  slug: 'rudiments',
517
519
  },
518
520
  routine: {
519
- fields: [`"description": ${descriptionField}`, 'high_soundslice_slug', 'low_soundslice_slug'],
521
+ fields: [`"description": ${descriptionField}`, 'soundslice_slug'],
520
522
  slug: 'routines',
521
523
  },
522
- 'pack-children': {
523
- fields: [
524
- 'child_count',
525
- `"resources": ${resourcesField}`,
526
- '"image": logo_image_url.asset->url',
527
- '"thumbnail": thumbnail.asset->url',
528
- '"light_mode_logo": light_mode_logo_url.asset->url',
529
- '"dark_mode_logo": dark_mode_logo_url.asset->url',
530
- `"description": ${descriptionField}`,
531
- ],
532
- childFields: [`"description": ${descriptionField}`],
533
- },
534
- 'pack-bundle-lesson': {
535
- fields: [`"resources": ${resourcesField}`],
536
- },
537
524
  foundation: {
538
525
  fields: [
539
526
  `"description": ${descriptionField}`,
package/src/index.d.ts CHANGED
@@ -257,7 +257,6 @@ import {
257
257
  buildEntityAndTotalQuery,
258
258
  fetchAll,
259
259
  fetchAllFilterOptions,
260
- fetchAllPacks,
261
260
  fetchBrandsByContentIds,
262
261
  fetchByRailContentId,
263
262
  fetchByRailContentIds,
@@ -267,6 +266,7 @@ import {
267
266
  fetchCommentModContentData,
268
267
  fetchContentRows,
269
268
  fetchContentTypeCounts,
269
+ fetchCourseCollectionData,
270
270
  fetchHierarchy,
271
271
  fetchLearningPathHierarchy,
272
272
  fetchLeaving,
@@ -280,7 +280,6 @@ import {
280
280
  fetchNewReleases,
281
281
  fetchOtherSongVersions,
282
282
  fetchOwnedContent,
283
- fetchPackAll,
284
283
  fetchPackData,
285
284
  fetchPlayAlongsCount,
286
285
  fetchRecent,
@@ -479,7 +478,6 @@ declare module 'musora-content-services' {
479
478
  extractSanityUrl,
480
479
  fetchAll,
481
480
  fetchAllFilterOptions,
482
- fetchAllPacks,
483
481
  fetchArtistBySlug,
484
482
  fetchArtistLessons,
485
483
  fetchArtists,
@@ -499,6 +497,7 @@ declare module 'musora-content-services' {
499
497
  fetchContentPageUserData,
500
498
  fetchContentRows,
501
499
  fetchContentTypeCounts,
500
+ fetchCourseCollectionData,
502
501
  fetchCustomerPayments,
503
502
  fetchEnrollmentPageMetadata,
504
503
  fetchFollowedThreads,
@@ -531,7 +530,6 @@ declare module 'musora-content-services' {
531
530
  fetchNotifications,
532
531
  fetchOtherSongVersions,
533
532
  fetchOwnedContent,
534
- fetchPackAll,
535
533
  fetchPackData,
536
534
  fetchPlayAlongsCount,
537
535
  fetchPlaylist,
package/src/index.js CHANGED
@@ -261,7 +261,6 @@ import {
261
261
  buildEntityAndTotalQuery,
262
262
  fetchAll,
263
263
  fetchAllFilterOptions,
264
- fetchAllPacks,
265
264
  fetchBrandsByContentIds,
266
265
  fetchByRailContentId,
267
266
  fetchByRailContentIds,
@@ -271,6 +270,7 @@ import {
271
270
  fetchCommentModContentData,
272
271
  fetchContentRows,
273
272
  fetchContentTypeCounts,
273
+ fetchCourseCollectionData,
274
274
  fetchHierarchy,
275
275
  fetchLearningPathHierarchy,
276
276
  fetchLeaving,
@@ -284,7 +284,6 @@ import {
284
284
  fetchNewReleases,
285
285
  fetchOtherSongVersions,
286
286
  fetchOwnedContent,
287
- fetchPackAll,
288
287
  fetchPackData,
289
288
  fetchPlayAlongsCount,
290
289
  fetchRecent,
@@ -478,7 +477,6 @@ export {
478
477
  extractSanityUrl,
479
478
  fetchAll,
480
479
  fetchAllFilterOptions,
481
- fetchAllPacks,
482
480
  fetchArtistBySlug,
483
481
  fetchArtistLessons,
484
482
  fetchArtists,
@@ -498,6 +496,7 @@ export {
498
496
  fetchContentPageUserData,
499
497
  fetchContentRows,
500
498
  fetchContentTypeCounts,
499
+ fetchCourseCollectionData,
501
500
  fetchCustomerPayments,
502
501
  fetchEnrollmentPageMetadata,
503
502
  fetchFollowedThreads,
@@ -530,7 +529,6 @@ export {
530
529
  fetchNotifications,
531
530
  fetchOtherSongVersions,
532
531
  fetchOwnedContent,
533
- fetchPackAll,
534
532
  fetchPackData,
535
533
  fetchPlayAlongsCount,
536
534
  fetchPlaylist,
@@ -11,7 +11,7 @@ import {
11
11
  fetchUpcomingEvents,
12
12
  fetchScheduledReleases,
13
13
  fetchReturning,
14
- fetchLeaving, fetchScheduledAndNewReleases, fetchContentRows, fetchOwnedContent
14
+ fetchLeaving, fetchScheduledAndNewReleases, fetchContentRows, fetchOwnedContent, fetchCourseCollectionData
15
15
  } from './sanity.js'
16
16
  import {TabResponseType, Tabs, capitalizeFirstLetter} from '../contentMetaData.js'
17
17
  import {recommendations, rankCategories, rankItems} from "./recommendations";
@@ -19,6 +19,8 @@ import {addContextToContent} from "./contentAggregator.js";
19
19
  import {globalConfig} from "./config";
20
20
  import {getUserData} from "./user/management";
21
21
  import {filterTypes, ownedContentTypes} from "../contentTypeConfig";
22
+ import {getPermissionsAdapter} from "./permissions/index.ts";
23
+ import {MEMBERSHIP_PERMISSIONS} from "../constants/membership-permissions.ts";
22
24
 
23
25
 
24
26
  export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
@@ -458,24 +460,33 @@ export async function getRecommendedForYou(brand, rowId = null, {
458
460
  * .then(content => console.log(content))
459
461
  * .catch(error => console.error(error));
460
462
  */
461
- export async function getLegacyMethods(brand) {
462
-
463
- // TODO: Replace with real data from Sanity when available with permissions
464
-
465
- return [
466
- {
467
- id: 1,
468
- title: '2020 Method',
469
- type: 'pack',
470
- child_count: 12,
471
- },
472
- {
473
- id: 2,
474
- title: '2016 Foundations',
475
- type: 'pack',
476
- child_count: 12,
477
- },
478
- ]
463
+ export async function getLegacyMethods(brand)
464
+ {
465
+ const brandMap = {
466
+ drumeo: [241247],
467
+ pianote: [
468
+ 276693,
469
+ 215952 //Foundations 2019
470
+ ],
471
+ singeo: [308514],
472
+ guitareo: [333652],
473
+ }
474
+ const ids = brandMap[brand] ?? null;
475
+ if (!ids) return [];
476
+ const adapter = getPermissionsAdapter()
477
+ const userPermissionsData = await adapter.fetchUserPermissions()
478
+ const userPermissions = userPermissionsData.permissions
479
+ // Users should only have access to this if they have an active membership AS WELL as the content access
480
+ // This is hardcoded behaviour and isn't found elsewhere
481
+ const hasMembership = userPermissionsData.isAdmin
482
+ || userPermissions.includes(MEMBERSHIP_PERMISSIONS.base)
483
+ || userPermissions.includes(MEMBERSHIP_PERMISSIONS.plus)
484
+ const hasContentPermission = userPermissions.includes(100000000 + ids[0])
485
+ if (hasMembership && hasContentPermission) {
486
+ return Promise.all(ids.map(id => fetchCourseCollectionData(id)))
487
+ } else {
488
+ return []
489
+ }
479
490
  }
480
491
 
481
492
  /**
@@ -509,7 +520,7 @@ export async function getLegacyMethods(brand) {
509
520
  * @example
510
521
  * // Fetch owned content filtered by types
511
522
  * getOwnedContent('drumeo', {
512
- * type: ['course', 'pack'],
523
+ * type: ['course', 'course-collection'],
513
524
  * page: 1,
514
525
  * limit: 10
515
526
  * })
@@ -20,7 +20,7 @@ import {getContentAwardsByIds} from "./awards/award-query.js";
20
20
  * {} <- fetchLessonContent || on playback page (main window)
21
21
  * in the examples below, dataField would be set to `children`
22
22
  * [{id, children}, {id, children,}] <- getTabData || catalog Page
23
- * {childen, } <- getPackData || pack index page
23
+ * {children, } <- getCourseCollectionData || Course Collection index page
24
24
  *
25
25
  *
26
26
  * @param dataPromise - promise or method that provides sanity data
@@ -94,7 +94,7 @@ export async function getNavigateTo(data, collection = null) {
94
94
  collection = normalizeCollection(collection)
95
95
  let navigateToData = {}
96
96
 
97
- const twoDepthContentTypes = ['pack'] // not adding method because it has its own logic (with active path)
97
+ const twoDepthContentTypes = ['course-collection'] // not adding method because it has its own logic (with active path)
98
98
  //TODO add parent hierarchy upwards as well
99
99
  // data structure is the same but instead of child{} we use parent{}
100
100
  for (const content of data) {
@@ -134,7 +134,7 @@ export async function getNavigateTo(data, collection = null) {
134
134
  const lastInteracted = await getLastInteractedOf(childrenIds, collection)
135
135
  const lastInteractedStatus = childrenStates[lastInteracted]
136
136
 
137
- if (['course', 'pack-bundle', 'skill-pack'].includes(content.type)) {
137
+ if (['course', 'skill-pack'].includes(content.type)) {
138
138
  if (lastInteractedStatus === STATE_STARTED) {
139
139
  // send to last interacted
140
140
  navigateToData[content.id] = buildNavigateTo(
@@ -169,7 +169,7 @@ export async function getNavigateTo(data, collection = null) {
169
169
  collection
170
170
  )
171
171
  if (childrenStates[lastInteractedChildId] === STATE_COMPLETED) {
172
- // 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
172
+ // TODO: course collections have an extra situation where we need to jump to the next course if all lessons in the last engaged course are completed
173
173
  }
174
174
  let lastInteractedChildNavToData = await getNavigateTo(firstChildren, collection)
175
175
  lastInteractedChildNavToData = lastInteractedChildNavToData[lastInteractedChildId]
@@ -169,7 +169,7 @@ function getDefaultCTATextForContent(content, contentType) {
169
169
  if (contentType === 'lesson') ctaText = 'Revisit Lesson'
170
170
  if (contentType === 'song tutorial' || collectionLessonTypes.includes(content.type))
171
171
  ctaText = 'Revisit Lessons'
172
- if (contentType === 'pack') ctaText = 'View Lessons'
172
+ if (contentType === 'course-collection') ctaText = 'View Lessons'
173
173
  }
174
174
  return ctaText
175
175
  }
@@ -639,8 +639,6 @@ export async function fetchAll(
639
639
  bypassStatusAndPublishedValidation = true
640
640
  } else if (type === 'lessons' || type === 'songs') {
641
641
  typeFilter = ``
642
- } else if (type === 'pack') {
643
- typeFilter = `&& (_type == 'pack' || _type == 'semester-pack')`
644
642
  } else {
645
643
  typeFilter = type ? `&& _type == '${type}'` : ''
646
644
  }
@@ -1163,50 +1161,6 @@ export async function fetchRelatedLessons(railContentId) {
1163
1161
  return await fetchSanity(query, false, { processNeedAccess: true })
1164
1162
  }
1165
1163
 
1166
- /**
1167
- * Fetch all packs.
1168
- * @param {string} brand - The brand for which to fetch packs.
1169
- * @param {string} [searchTerm=""] - The search term to filter packs.
1170
- * @param {string} [sort="-published_on"] - The field to sort the packs by.
1171
- * @param {number} [params.page=1] - The page number for pagination.
1172
- * @param {number} [params.limit=10] - The number of items per page.
1173
- * @returns {Promise<Array<Object>|null>} - The fetched pack content data or null if not found.
1174
- */
1175
- export async function fetchAllPacks(
1176
- brand,
1177
- sort = '-published_on',
1178
- searchTerm = '',
1179
- page = 1,
1180
- limit = 10
1181
- ) {
1182
- const sortOrder = getSortOrder(sort, brand)
1183
- const filter = `(_type == 'pack' || _type == 'semester-pack') && brand == '${brand}' && title match "${searchTerm}*"`
1184
- const filterParams = {}
1185
- const start = (page - 1) * limit
1186
- const end = start + limit
1187
-
1188
- const query = await buildQuery(
1189
- filter,
1190
- filterParams,
1191
- await getFieldsForContentTypeWithFilteredChildren('pack'),
1192
- {
1193
- sortOrder: sortOrder,
1194
- start,
1195
- end,
1196
- }
1197
- )
1198
- return fetchSanity(query, true)
1199
- }
1200
-
1201
- /**
1202
- * Fetch all content for a specific pack by Railcontent ID.
1203
- * @param {string} railcontentId - The Railcontent ID of the pack.
1204
- * @returns {Promise<Array<Object>|null>} - The fetched pack content data or null if not found.
1205
- */
1206
- export async function fetchPackAll(railcontentId, type = 'pack') {
1207
- return fetchByRailContentId(railcontentId, type)
1208
- }
1209
-
1210
1164
  export async function fetchLiveEvent(brand, forcedContentId = null) {
1211
1165
  const LIVE_EXTRA_MINUTES = 30
1212
1166
  //calendarIDs taken from addevent.php
@@ -1283,6 +1237,25 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1283
1237
  }
1284
1238
 
1285
1239
  /**
1240
+ * Fetch the data needed for the CourseCollection Overview screen.
1241
+ * @param {number} id - The Railcontent ID of the CourseCollection
1242
+ * @returns {Promise<Object|null>} - The CourseCollection information and lessons or null if not found.
1243
+ *
1244
+ * @example
1245
+ * fetchCourseCollectionData(404048)
1246
+ * .then(CourseCollection => console.log(CourseCollection))
1247
+ * .catch(error => console.error(error));
1248
+ */
1249
+ export async function fetchCourseCollectionData(id) {
1250
+ const builder = await new FilterBuilder(`railcontent_id == ${id}`).buildFilter()
1251
+ const query = `*[${builder}]{
1252
+ ${await getFieldsForContentTypeWithFilteredChildren('course-collection')}
1253
+ } [0...1]`
1254
+ return fetchSanity(query, false)
1255
+ }
1256
+
1257
+ /**
1258
+ * DEPRECATED: Use fetchCourseCollectionData
1286
1259
  * Fetch the data needed for the Pack Overview screen.
1287
1260
  * @param {number} id - The Railcontent ID of the pack
1288
1261
  * @returns {Promise<Object|null>} - The pack information and lessons or null if not found.
@@ -1293,11 +1266,7 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1293
1266
  * .catch(error => console.error(error));
1294
1267
  */
1295
1268
  export async function fetchPackData(id) {
1296
- const builder = await new FilterBuilder(`railcontent_id == ${id}`).buildFilter()
1297
- const query = `*[${builder}]{
1298
- ${await getFieldsForContentTypeWithFilteredChildren('pack')}
1299
- } [0...1]`
1300
- return fetchSanity(query, false)
1269
+ return fetchCourseCollectionData(id)
1301
1270
  }
1302
1271
 
1303
1272
  /**