musora-content-services 2.155.14 → 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 +12 -3
- package/package.json +1 -1
- package/src/services/sanity.js +62 -44
- package/src/services/user/onboarding.ts +22 -0
- package/.claude/settings.local.json +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +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
|
-
|
|
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.13](https://github.com/railroadmedia/musora-content-services/compare/v2.155.11...v2.155.13) (2026-04-29)
|
|
8
7
|
|
|
9
|
-
###
|
|
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
|
|
17
|
+
|
|
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))
|
|
10
19
|
|
|
11
20
|
### [2.155.11](https://github.com/railroadmedia/musora-content-services/compare/v2.155.10...v2.155.11) (2026-04-28)
|
|
12
21
|
|
package/package.json
CHANGED
package/src/services/sanity.js
CHANGED
|
@@ -34,14 +34,24 @@ import {
|
|
|
34
34
|
getLiveFields,
|
|
35
35
|
} from '../contentTypeConfig.js'
|
|
36
36
|
import { fetchSimilarItems } from './recommendations.js'
|
|
37
|
-
import {
|
|
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 {
|
|
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 = [
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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}
|
|
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(
|
|
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 (
|
|
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) =>
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 +=
|
|
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 +=
|
|
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
|
+
}
|