musora-content-services 2.155.15 → 2.157.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,13 +2,20 @@
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.155.15](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.155.15) (2026-04-29)
5
+ ## [2.157.0](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.157.0) (2026-04-29)
6
6
 
7
- ### [2.155.14](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.155.14) (2026-04-29)
8
7
 
9
- ### [2.155.13](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.155.13) (2026-04-29)
8
+ ### Features
9
+
10
+ * **MU2-1384:** enable related lessons for non-members ([#943](https://github.com/railroadmedia/musora-content-services/issues/943)) ([f091bd7](https://github.com/railroadmedia/musora-content-services/commit/f091bd7e7edfcfe8808684d97faff22f62ef6fe0))
11
+ * **MU2-1463:** initialize onboarding flow ([#929](https://github.com/railroadmedia/musora-content-services/issues/929)) ([4daae5f](https://github.com/railroadmedia/musora-content-services/commit/4daae5ff28599b53fa9ee5b9bea8bdb08b706978))
12
+
13
+ ## [2.156.0](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.156.0) (2026-04-29)
14
+
15
+
16
+ ### Features
10
17
 
11
- ### [2.155.12](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.155.12) (2026-04-29)
18
+ * **MU2-1384:** enable related lessons for non-members ([#943](https://github.com/railroadmedia/musora-content-services/issues/943)) ([f091bd7](https://github.com/railroadmedia/musora-content-services/commit/f091bd7e7edfcfe8808684d97faff22f62ef6fe0))
12
19
 
13
20
  ### [2.155.11](https://github.com/railroadmedia/musora-content-services/compare/v2.155.10...v2.155.11) (2026-04-28)
14
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.155.15",
3
+ "version": "2.157.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -34,14 +34,24 @@ import {
34
34
  getLiveFields,
35
35
  } from '../contentTypeConfig.js'
36
36
  import { fetchSimilarItems } from './recommendations.js'
37
- import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS, CONTENT_STATUSES } from '../contentMetaData.js'
37
+ import {
38
+ getSongType,
39
+ processMetadata,
40
+ ALWAYS_VISIBLE_TABS,
41
+ CONTENT_STATUSES,
42
+ } from '../contentMetaData.js'
38
43
  import { GET } from '../infrastructure/http/HttpClient.ts'
39
44
 
40
45
  import { globalConfig } from './config.js'
41
46
 
42
47
  import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
43
48
  import { getPermissionsAdapter } from './permissions/index.ts'
44
- import { getAllCompleted, getAllCompletedByIds, getAllStarted, getAllStartedOrCompleted } from './contentProgress.js'
49
+ import {
50
+ getAllCompleted,
51
+ getAllCompletedByIds,
52
+ getAllStarted,
53
+ getAllStartedOrCompleted,
54
+ } from './contentProgress.js'
45
55
  import { fetchRecentActivitiesActiveTabs } from './userActivity.js'
46
56
  import { query } from '../lib/sanity/query'
47
57
  import { Filters as f } from '../lib/sanity/filter'
@@ -53,7 +63,10 @@ import { MEMBERSHIP_PERMISSIONS } from '../constants/membership-permissions'
53
63
  *
54
64
  * @type {string[]}
55
65
  */
56
- const excludeFromGeneratedIndex = ['fetchRelatedByLicense', 'devFetchAllLearningPathsAndIntroVideoIdsForDelete']
66
+ const excludeFromGeneratedIndex = [
67
+ 'fetchRelatedByLicense',
68
+ 'devFetchAllLearningPathsAndIntroVideoIdsForDelete',
69
+ ]
57
70
 
58
71
  /**
59
72
  * Mapping from tab names to their underlying Sanity content types.
@@ -556,11 +569,11 @@ export async function fetchByRailContentIds(
556
569
  }
557
570
  let [results, hierarchies] = await Promise.all([
558
571
  fetchSanity(query, true, { customPostProcess: customPostProcess, processNeedAccess: true }),
559
- (contentType === 'download') ? getHierarchies(ids) : Promise.resolve(null),
572
+ contentType === 'download' ? getHierarchies(ids) : Promise.resolve(null),
560
573
  ])
561
574
 
562
575
  if (hierarchies) {
563
- results.forEach(r => r.hierarchy = hierarchies[r.id] ?? null)
576
+ results.forEach((r) => (r.hierarchy = hierarchies[r.id] ?? null))
564
577
  }
565
578
 
566
579
  const sortFuction = function compare(a, b) {
@@ -649,7 +662,7 @@ export async function fetchAll(
649
662
  useDefaultFields = true,
650
663
  customFields = [],
651
664
  progress = 'all',
652
- onlyPublished = true
665
+ onlyPublished = true,
653
666
  } = {}
654
667
  ) {
655
668
  let config = contentTypeConfig[type] ?? {}
@@ -808,9 +821,7 @@ const SORT_STRATEGIES = {
808
821
  },
809
822
 
810
823
  popularity: ({ brand, groupBy, isDesc }) => {
811
- const field = (groupBy === 'artist' || groupBy === 'genre')
812
- ? `popularity.${brand}`
813
- : 'popularity'
824
+ const field = groupBy === 'artist' || groupBy === 'genre' ? `popularity.${brand}` : 'popularity'
814
825
  return isDesc ? `coalesce(${field}, -1) desc` : `${field} asc`
815
826
  },
816
827
 
@@ -986,7 +997,7 @@ export async function fetchLessonContent(railContentId, { forDownload = false }
986
997
 
987
998
  let [contents, hierarchy] = await Promise.all([
988
999
  fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true }),
989
- forDownload ? getHierarchy(railContentId) : Promise.resolve(null)
1000
+ forDownload ? getHierarchy(railContentId) : Promise.resolve(null),
990
1001
  ])
991
1002
 
992
1003
  if (forDownload) {
@@ -1012,7 +1023,7 @@ export async function fetchLessonContent(railContentId, { forDownload = false }
1012
1023
  export async function fetchRelatedRecommendedContent(railContentId, brand, count = 10) {
1013
1024
  const recommendedItems = await fetchSimilarItems(railContentId, brand, count)
1014
1025
  if (recommendedItems && recommendedItems.length > 0) {
1015
- return fetchByRailContentIds(recommendedItems, 'tab-data', brand, true)
1026
+ return fetchByRailContentIds(recommendedItems, 'tab-data', brand)
1016
1027
  }
1017
1028
 
1018
1029
  return await fetchRelatedLessons(railContentId, brand).then((result) =>
@@ -1093,7 +1104,8 @@ export async function fetchSiblingContent(railContentId, brand = null) {
1093
1104
 
1094
1105
  const brandString = brand ? ` && brand == "${brand}"` : ''
1095
1106
  const queryFields = getFieldsForContentType()
1096
- const courseCollectionFields = await getFieldsForContentTypeWithFilteredChildren('course-collection')
1107
+ const courseCollectionFields =
1108
+ await getFieldsForContentTypeWithFilteredChildren('course-collection')
1097
1109
  const query = `*[railcontent_id == ${railContentId}${brandString}]{
1098
1110
  _type,
1099
1111
  parent_type,
@@ -1150,7 +1162,7 @@ export async function fetchRelatedLessons(railContentId) {
1150
1162
 
1151
1163
  const queryFields = getFieldsForContentType()
1152
1164
 
1153
- const query = `*[railcontent_id == ${railContentId} && (!defined(permission) || references(*[_type=='permission']._id))]{
1165
+ const query = `*[railcontent_id == ${railContentId}]{
1154
1166
  _type, parent_type, railcontent_id,
1155
1167
  "related_lessons" : array::unique([
1156
1168
  ...(*[${filterSameArtist}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
@@ -1190,7 +1202,9 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1190
1202
  )
1191
1203
  endDateTemp = new Date(endDateTemp.setMinutes(endDateTemp.getMinutes() - LIVE_EXTRA_MINUTES))
1192
1204
 
1193
- const liveEventFields = getLiveFields().concat(`'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}')`)
1205
+ const liveEventFields = getLiveFields().concat(
1206
+ `'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}')`
1207
+ )
1194
1208
  const fieldsString = liveEventFields.join(',')
1195
1209
 
1196
1210
  const baseFilter =
@@ -1202,7 +1216,7 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1202
1216
  && live_event_start_time <= '${getSanityDate(startDateTemp, false)}'
1203
1217
  && live_event_end_time >= '${getSanityDate(endDateTemp, false)}'`
1204
1218
 
1205
- const filter = await new FilterBuilder(baseFilter, {bypassPermissions: true}).buildFilter()
1219
+ const filter = await new FilterBuilder(baseFilter, { bypassPermissions: true }).buildFilter()
1206
1220
 
1207
1221
  // This query finds the first scheduled event (sorted by start_time) that ends after now()
1208
1222
  const query = `*[${filter}]{${fieldsString}} | order(live_event_start_time)[0...1]`
@@ -1313,7 +1327,7 @@ async function fetchTopLevelParentIds(railcontentIds) {
1313
1327
  if (!response) return null
1314
1328
 
1315
1329
  let responseMap = {}
1316
- response.forEach(item => {
1330
+ response.forEach((item) => {
1317
1331
  responseMap[item.railcontent_id] = item.top_parent ?? item.railcontent_id
1318
1332
  })
1319
1333
 
@@ -1348,7 +1362,7 @@ export async function getHierarchies(contentIds, collection) {
1348
1362
 
1349
1363
  function getHierarchyLookupsAndMetadataMany(hierarchies) {
1350
1364
  let hierarchyData = {}
1351
- Object.values(hierarchies).forEach(hierarchy => {
1365
+ Object.values(hierarchies).forEach((hierarchy) => {
1352
1366
  const topLevelId = hierarchy.railcontent_id ?? hierarchy.id
1353
1367
  hierarchyData[topLevelId] = getHierarchyLookupsAndMetadata(hierarchy)
1354
1368
  })
@@ -1379,9 +1393,13 @@ function getHierarchyLookupsAndMetadata(hierarchy) {
1379
1393
  function mapHierarchyDataToContentIds(hierarchyData, contentIds) {
1380
1394
  let data = {}
1381
1395
  // because of single parent rule we can simply find first hierarchy that contains the contentId in parent or children lookups
1382
- contentIds.forEach(contentId => {
1396
+ contentIds.forEach((contentId) => {
1383
1397
  for (let key in hierarchyData) {
1384
- if (key === contentId || hierarchyData[key].parents[contentId] || hierarchyData[key].children[contentId]) {
1398
+ if (
1399
+ key === contentId ||
1400
+ hierarchyData[key].parents[contentId] ||
1401
+ hierarchyData[key].children[contentId]
1402
+ ) {
1385
1403
  data[contentId] = hierarchyData[key]
1386
1404
  break
1387
1405
  }
@@ -1437,7 +1455,7 @@ async function fetchLearningPathHierarchyDataForIds(railcontentIds, collection)
1437
1455
  if (!response) return null
1438
1456
 
1439
1457
  let responseMap = {}
1440
- response.forEach(item => {
1458
+ response.forEach((item) => {
1441
1459
  responseMap[item.railcontent_id] = item
1442
1460
  })
1443
1461
 
@@ -1471,13 +1489,14 @@ async function fetchALaCarteHierarchyDataForIds(railcontentIds) {
1471
1489
  const response = await fetchSanity(query, true, { processNeedAccess: false })
1472
1490
  if (!response) return null
1473
1491
 
1474
- return Object.fromEntries(response.map(item => [item.railcontent_id, item]))
1492
+ return Object.fromEntries(response.map((item) => [item.railcontent_id, item]))
1475
1493
  }
1476
1494
 
1477
1495
  function buildHierarchyQuery(filter, rootSelector) {
1478
- const node = (depth) => depth === 0
1479
- ? HIERARCHY_NODE_FIELDS
1480
- : `${HIERARCHY_NODE_FIELDS}, 'children': child[${filter}]->{${node(depth - 1)}}`
1496
+ const node = (depth) =>
1497
+ depth === 0
1498
+ ? HIERARCHY_NODE_FIELDS
1499
+ : `${HIERARCHY_NODE_FIELDS}, 'children': child[${filter}]->{${node(depth - 1)}}`
1481
1500
 
1482
1501
  return `*[${rootSelector}]{ ${node(3)} }`
1483
1502
  }
@@ -1500,7 +1519,9 @@ function populateHierarchyLookups(currentLevel, data, parentId) {
1500
1519
 
1501
1520
  let assignments = currentLevel['assignments']
1502
1521
  if (assignments) {
1503
- let assignmentIds = assignments.map((assignment) => assignment[railcontentIdField]).filter(Boolean)
1522
+ let assignmentIds = assignments
1523
+ .map((assignment) => assignment[railcontentIdField])
1524
+ .filter(Boolean)
1504
1525
  if (assignmentIds.length > 0) {
1505
1526
  data.children[contentId] = (data.children[contentId] ?? []).concat(assignmentIds)
1506
1527
  assignmentIds.forEach((assignmentId) => {
@@ -1624,7 +1645,8 @@ function contentResultsDecorator(results, fieldName, callback) {
1624
1645
  const processChildren = (result, depth = 0) => {
1625
1646
  if (result.children && Array.isArray(result.children)) {
1626
1647
  result.children.forEach((child) => {
1627
- if (child && depth < 3) { // course-collections are only 3 depth
1648
+ if (child && depth < 3) {
1649
+ // course-collections are only 3 depth
1628
1650
  child[fieldName] = callback(child)
1629
1651
  processChildren(child, depth + 1)
1630
1652
  }
@@ -1673,7 +1695,7 @@ function contentResultsDecorator(results, fieldName, callback) {
1673
1695
  })
1674
1696
  } else if (results.lessons && results.livestreams && results.songs) {
1675
1697
  // `fetchScheduledAndNewReleases` response structure
1676
- ['lessons', 'livestreams', 'songs'].forEach((key) => {
1698
+ ;['lessons', 'livestreams', 'songs'].forEach((key) => {
1677
1699
  if (results[key] && Array.isArray(results[key])) {
1678
1700
  results[key].forEach((item) => {
1679
1701
  item[fieldName] = callback(item)
@@ -1681,7 +1703,6 @@ function contentResultsDecorator(results, fieldName, callback) {
1681
1703
  })
1682
1704
  }
1683
1705
  })
1684
-
1685
1706
  } else {
1686
1707
  results[fieldName] = callback(results)
1687
1708
  processChildren(results) // this on was always true
@@ -2140,36 +2161,33 @@ export async function fetchScheduledAndNewReleases(
2140
2161
  const now = getSanityDate(rawNow)
2141
2162
  const fifteenDaysAgo = getSanityDate(new Date(rawNow - 15 * 24 * 60 * 60 * 1000))
2142
2163
 
2143
- const parentsWithoutSong = parentRecentTypes.filter(type => type !== 'song')
2164
+ const parentsWithoutSong = parentRecentTypes.filter((type) => type !== 'song')
2144
2165
 
2145
2166
  const fields = await getFieldsForContentTypeWithFilteredChildren('new-and-scheduled')
2146
2167
 
2147
2168
  const lessonFilter = f.combine(
2148
- "show_in_new_feed == true",
2169
+ 'show_in_new_feed == true',
2149
2170
  f.brand(brand),
2150
2171
  f.typeIn(parentsWithoutSong),
2151
2172
  f.statusIn(['published']),
2152
2173
  f.publishedBefore(now),
2153
- f.publishedAfter(fifteenDaysAgo),
2174
+ f.publishedAfter(fifteenDaysAgo)
2154
2175
  )
2155
2176
 
2156
2177
  const songFilter = f.combine(
2157
- "show_in_new_feed == true",
2178
+ 'show_in_new_feed == true',
2158
2179
  f.brand(brand),
2159
2180
  f.type('song'),
2160
2181
  f.statusIn(['published']),
2161
2182
  f.publishedBefore(now),
2162
- f.publishedAfter(fifteenDaysAgo),
2183
+ f.publishedAfter(fifteenDaysAgo)
2163
2184
  )
2164
2185
 
2165
2186
  const livestreamFilter = f.combine(
2166
- "show_in_new_feed == true",
2167
- f.combineOr(
2168
- f.brand(brand),
2169
- 'live_global_event == true'
2170
- ),
2187
+ 'show_in_new_feed == true',
2188
+ f.combineOr(f.brand(brand), 'live_global_event == true'),
2171
2189
  f.statusIn(['scheduled']),
2172
- `live_event_start_time >= '${now}'`,
2190
+ `live_event_start_time >= '${now}'`
2173
2191
  )
2174
2192
 
2175
2193
  const lessonQuery = query()
@@ -2220,19 +2238,19 @@ function reorderScheduledAndNewReleases(r, limit) {
2220
2238
  livestreamLimit = 1
2221
2239
  songLimit = limit - lessonLimit - livestreamLimit
2222
2240
  } else {
2223
- lessonLimit = (limit > 0) ? 1 : 0
2241
+ lessonLimit = limit > 0 ? 1 : 0
2224
2242
  livestreamLimit = 0
2225
2243
  songLimit = limit - lessonLimit
2226
2244
  }
2227
2245
 
2228
2246
  const lessons = r.lessons.slice(0, lessonLimit)
2229
2247
  if (lessons.length < lessonLimit) {
2230
- songLimit += (lessonLimit - lessons.length)
2248
+ songLimit += lessonLimit - lessons.length
2231
2249
  }
2232
2250
 
2233
2251
  const livestreams = r.livestreams.slice(0, livestreamLimit)
2234
2252
  if (livestreams.length < livestreamLimit) {
2235
- songLimit += (livestreamLimit - livestreams.length)
2253
+ songLimit += livestreamLimit - livestreams.length
2236
2254
  }
2237
2255
 
2238
2256
  const songs = r.songs.slice(0, songLimit)
@@ -2608,7 +2626,7 @@ export async function hasAnyMethodV2IntroCompleted() {
2608
2626
  const filter = `_type == '${type}'`
2609
2627
 
2610
2628
  const query = `*[${filter}] { railcontent_id }`
2611
- const videos = await fetchSanity(query, true);
2629
+ const videos = await fetchSanity(query, true)
2612
2630
  const ids = (videos || []).map((v) => v.railcontent_id)
2613
2631
 
2614
2632
  const completedVideos = await getAllCompletedByIds(ids)
@@ -2616,6 +2634,6 @@ export async function hasAnyMethodV2IntroCompleted() {
2616
2634
  }
2617
2635
 
2618
2636
  function applyPermissionSort(sortOrder, permissionIds) {
2619
- const idsString = permissionIds.join(",")
2637
+ const idsString = permissionIds.join(',')
2620
2638
  return `select(count(permission_v2[@ in [${idsString}]]) > 0 => 1, 0) desc, ${sortOrder}`
2621
2639
  }
@@ -149,3 +149,25 @@ export async function getOnboardingRecommendedContent(
149
149
  ): Promise<OnboardingRecommendationResponse> {
150
150
  return POST(`/api/user-management-system/v1/onboardings/${onboardingId}/recommendation`, {})
151
151
  }
152
+
153
+ /**
154
+ * @param {StartOnboardingParams} params - The parameters for starting the onboarding process.
155
+ * @param {boolean} sendAccountSetupEmail - Whether to send an account setup email to the user.
156
+ *
157
+ * @returns {Promise<Onboarding>} - A promise that resolves when the onboarding process is started.
158
+ * @throws {HttpError} - If the HTTP request fails.
159
+ */
160
+ export async function initializeOnboardingFlow(
161
+ { email, brand, flow, steps = {}, marketingOptIn = false }: StartOnboardingParams,
162
+ sendAccountSetupEmail: boolean = false
163
+ ): Promise<Onboarding> {
164
+ return POST(`/api/user-management-system/v1/onboardings/flows`, {
165
+ email,
166
+ brand,
167
+ flow,
168
+ steps,
169
+ is_completed: false,
170
+ marketing_opt_in: marketingOptIn,
171
+ send_email: sendAccountSetupEmail,
172
+ })
173
+ }
@@ -1,10 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(rg:*)",
5
- "Bash(npm run lint:*)",
6
- "Bash(ls:*)"
7
- ],
8
- "deny": []
9
- }
10
- }