musora-content-services 2.81.0 → 2.83.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 +24 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Forums.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/TestUser.html +2 -2
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +5 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_guided-courses.ts.html +2 -2
- package/docs/content-org_learning-paths.ts.html +2 -2
- package/docs/content-org_playlists-types.js.html +2 -2
- package/docs/content-org_playlists.js.html +2 -2
- package/docs/content.js.html +88 -10
- package/docs/content_artist.ts.html +8 -8
- package/docs/content_genre.ts.html +18 -15
- package/docs/content_instructor.ts.html +21 -16
- package/docs/forums_categories.ts.html +21 -2
- package/docs/forums_forums.ts.html +2 -2
- package/docs/forums_posts.ts.html +2 -2
- package/docs/forums_threads.ts.html +2 -2
- package/docs/gamification_awards.ts.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/global.html +2 -2
- package/docs/index.html +2 -2
- package/docs/liveTesting.ts.html +2 -2
- package/docs/module-Accounts.html +2 -2
- package/docs/module-Artist.html +8 -8
- package/docs/module-Awards.html +2 -2
- package/docs/module-Config.html +5 -4
- package/docs/module-Content-Services-V2.html +440 -9
- package/docs/module-Forums.html +607 -43
- package/docs/module-Genre.html +9 -9
- package/docs/module-GuidedCourses.html +2 -2
- package/docs/module-Instructor.html +6 -6
- package/docs/module-Interests.html +2 -2
- package/docs/module-LearningPaths.html +2 -2
- package/docs/module-Onboarding.html +2 -2
- package/docs/module-Payments.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +2 -2
- package/docs/module-ProgressRow.html +2 -2
- package/docs/module-Railcontent-Services.html +2 -2
- package/docs/module-Sanity-Services.html +320 -15
- package/docs/module-Sessions.html +2 -2
- package/docs/module-UserActivity.html +2 -2
- package/docs/module-UserChat.html +2 -2
- package/docs/module-UserManagement.html +2 -2
- package/docs/module-UserMemberships.html +2 -2
- package/docs/module-UserNotifications.html +2 -2
- package/docs/module-UserProfile.html +2 -2
- package/docs/progress-row_method-card.js.html +2 -2
- package/docs/railcontent.js.html +2 -2
- package/docs/sanity.js.html +105 -42
- package/docs/userActivity.js.html +2 -2
- package/docs/user_account.ts.html +2 -2
- package/docs/user_chat.js.html +2 -2
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +2 -2
- package/docs/user_memberships.ts.html +2 -2
- package/docs/user_notifications.js.html +2 -2
- package/docs/user_onboarding.ts.html +2 -2
- package/docs/user_payments.ts.html +2 -2
- package/docs/user_permissions.js.html +3 -3
- package/docs/user_profile.js.html +2 -2
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +2 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/package.json +1 -1
- package/src/contentTypeConfig.js +33 -1
- package/src/filterBuilder.js +22 -12
- package/src/index.d.ts +6 -0
- package/src/index.js +6 -0
- package/src/lib/lastUpdated.js +4 -4
- package/src/services/config.js +3 -0
- package/src/services/content/artist.ts +6 -6
- package/src/services/content/genre.ts +16 -13
- package/src/services/content/instructor.ts +19 -14
- package/src/services/content.js +86 -8
- package/src/services/contentAggregator.js +4 -4
- package/src/services/contentProgress.js +16 -3
- package/src/services/forums/categories.ts +19 -0
- package/src/services/permissions/PermissionsAdapter.ts +111 -0
- package/src/services/permissions/PermissionsAdapterFactory.ts +71 -0
- package/src/services/permissions/PermissionsV1Adapter.ts +232 -0
- package/src/services/permissions/PermissionsV2Adapter.ts +226 -0
- package/src/services/permissions/README.md +139 -0
- package/src/services/permissions/index.ts +65 -0
- package/src/services/sanity.js +103 -40
- package/src/services/types.js +1 -0
- package/src/services/user/permissions.js +1 -1
- package/test/content.test.js +5 -0
- package/test/forum.test.js +1 -1
- package/test/initializeTests.js +5 -3
- package/tools/generate-index.cjs +5 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Permissions
|
|
3
|
+
*
|
|
4
|
+
* Permissions abstraction layer for Musora Content Services.
|
|
5
|
+
*
|
|
6
|
+
* This module provides a flexible abstraction that allows swapping between
|
|
7
|
+
* different permission implementations (v1 and v2) without changing code.
|
|
8
|
+
*
|
|
9
|
+
* ## Quick Start
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { getPermissionsAdapter } from './permissions/index.js'
|
|
13
|
+
*
|
|
14
|
+
* // Get the adapter (automatically selects v1 or v2 based on config)
|
|
15
|
+
* const adapter = getPermissionsAdapter()
|
|
16
|
+
*
|
|
17
|
+
* // Fetch user permissions
|
|
18
|
+
* const permissions = await adapter.fetchUserPermissions()
|
|
19
|
+
*
|
|
20
|
+
* // Check if user needs access to content
|
|
21
|
+
* const needsAccess = adapter.doesUserNeedAccess(content, permissions)
|
|
22
|
+
*
|
|
23
|
+
* // Generate GROQ filter for queries
|
|
24
|
+
* const filter = adapter.generatePermissionsFilter(permissions, {
|
|
25
|
+
* prefix: '',
|
|
26
|
+
* showMembershipRestrictedContent: false
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* ## Switching Versions
|
|
31
|
+
*
|
|
32
|
+
* Set `permissionsVersion` in `initializeService()`:
|
|
33
|
+
* - `permissionsVersion: 'v1'` - Use current permissions system (default)
|
|
34
|
+
* - `permissionsVersion: 'v2'` - Use new permissions system (when ready)
|
|
35
|
+
*
|
|
36
|
+
* ## Architecture
|
|
37
|
+
*
|
|
38
|
+
* - **PermissionsAdapter**: Abstract base class defining the contract
|
|
39
|
+
* - **PermissionsV1Adapter**: Implementation for current permissions system
|
|
40
|
+
* - **PermissionsV2Adapter**: Implementation for new permissions system (placeholder)
|
|
41
|
+
* - **PermissionsAdapterFactory**: Factory for getting the appropriate adapter
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// Export the base class (runtime value)
|
|
45
|
+
export { PermissionsAdapter } from './PermissionsAdapter'
|
|
46
|
+
|
|
47
|
+
// Export TypeScript types only (not runtime values)
|
|
48
|
+
export type {
|
|
49
|
+
UserPermissions,
|
|
50
|
+
PermissionFilterOptions,
|
|
51
|
+
ContentItem,
|
|
52
|
+
} from './PermissionsAdapter'
|
|
53
|
+
|
|
54
|
+
// Export adapter implementations (runtime values)
|
|
55
|
+
export { PermissionsV1Adapter } from './PermissionsV1Adapter'
|
|
56
|
+
export { PermissionsV2Adapter } from './PermissionsV2Adapter'
|
|
57
|
+
|
|
58
|
+
// Export factory functions and version utilities (runtime values)
|
|
59
|
+
export {
|
|
60
|
+
getPermissionsAdapter,
|
|
61
|
+
getPermissionsVersion,
|
|
62
|
+
} from './PermissionsAdapterFactory'
|
|
63
|
+
|
|
64
|
+
// Export PermissionsVersion type only (not runtime value)
|
|
65
|
+
export type { PermissionsVersion } from './PermissionsAdapterFactory'
|
package/src/services/sanity.js
CHANGED
|
@@ -29,7 +29,7 @@ import { globalConfig } from './config.js'
|
|
|
29
29
|
|
|
30
30
|
import { fetchNextContentDataForParent, fetchHandler } from './railcontent.js'
|
|
31
31
|
import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
|
|
32
|
-
import {
|
|
32
|
+
import { getPermissionsAdapter } from './permissions/index.ts'
|
|
33
33
|
import { getAllCompleted, getAllStarted, getAllStartedOrCompleted } from './contentProgress.js'
|
|
34
34
|
import { fetchRecentActivitiesActiveTabs } from './userActivity.js'
|
|
35
35
|
|
|
@@ -491,7 +491,7 @@ export async function fetchByRailContentIds(
|
|
|
491
491
|
}
|
|
492
492
|
return results.map(liveProcess)
|
|
493
493
|
}
|
|
494
|
-
const results = await fetchSanity(query, true, { customPostProcess: customPostProcess })
|
|
494
|
+
const results = await fetchSanity(query, true, { customPostProcess: customPostProcess, processNeedAccess: true })
|
|
495
495
|
|
|
496
496
|
const sortFuction = function compare(a, b) {
|
|
497
497
|
const indexA = ids.indexOf(a['id'])
|
|
@@ -511,10 +511,8 @@ export async function fetchContentRows(brand, pageName, contentRowSlug) {
|
|
|
511
511
|
if (pageName === 'lessons') pageName = 'lesson'
|
|
512
512
|
if (pageName === 'songs') pageName = 'song'
|
|
513
513
|
const rowString = contentRowSlug ? ` && slug.current == "${contentRowSlug.toLowerCase()}"` : ''
|
|
514
|
-
const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {
|
|
515
|
-
|
|
516
|
-
}).buildFilter()
|
|
517
|
-
const childFilter = await new FilterBuilder('', { isChildrenFilter: true }).buildFilter()
|
|
514
|
+
const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {pullFutureContent: true, showMembershipRestrictedContent: true}).buildFilter()
|
|
515
|
+
const childFilter = await new FilterBuilder('', {isChildrenFilter: true, showMembershipRestrictedContent: true}).buildFilter()
|
|
518
516
|
const query = `*[_type == 'recommended-content-row' && brand == '${brand}' && type == '${pageName}'${rowString}]{
|
|
519
517
|
brand,
|
|
520
518
|
name,
|
|
@@ -527,7 +525,7 @@ export async function fetchContentRows(brand, pageName, contentRowSlug) {
|
|
|
527
525
|
'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
|
|
528
526
|
},
|
|
529
527
|
}`
|
|
530
|
-
return fetchSanity(query, true)
|
|
528
|
+
return fetchSanity(query, true, {processNeedAccess: true})
|
|
531
529
|
}
|
|
532
530
|
|
|
533
531
|
/**
|
|
@@ -816,7 +814,9 @@ export async function fetchAllFilterOptions(
|
|
|
816
814
|
|
|
817
815
|
const includedFieldsFilter = filters?.length ? filtersToGroq(filters) : undefined
|
|
818
816
|
const progressFilter = progressIds ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
|
|
819
|
-
const
|
|
817
|
+
const adapter = getPermissionsAdapter()
|
|
818
|
+
const userPermissionsData = await adapter.fetchUserPermissions()
|
|
819
|
+
const isAdmin = adapter.isAdmin(userPermissionsData)
|
|
820
820
|
|
|
821
821
|
const constructCommonFilter = (excludeFilter) => {
|
|
822
822
|
const filterWithoutOption = excludeFilter
|
|
@@ -1085,7 +1085,7 @@ export async function jumpToContinueContent(railcontentId) {
|
|
|
1085
1085
|
* .catch(error => console.error(error));
|
|
1086
1086
|
*/
|
|
1087
1087
|
export async function fetchLessonContent(railContentId, { addParent = false } = {}) {
|
|
1088
|
-
const filterParams = { isSingle: true, pullFutureContent: true }
|
|
1088
|
+
const filterParams = { isSingle: true, pullFutureContent: true, showMembershipRestrictedContent: true }
|
|
1089
1089
|
|
|
1090
1090
|
const parentQuery = addParent
|
|
1091
1091
|
? `"parent_content_data": *[railcontent_id in [...(^.parent_content_data[].id)]]{
|
|
@@ -1146,7 +1146,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
1146
1146
|
return result
|
|
1147
1147
|
}
|
|
1148
1148
|
|
|
1149
|
-
return fetchSanity(query, false, { customPostProcess: chapterProcess })
|
|
1149
|
+
return fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true })
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
1152
|
/**
|
|
@@ -1237,16 +1237,21 @@ async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, cou
|
|
|
1237
1237
|
export async function fetchSiblingContent(railContentId, brand = null) {
|
|
1238
1238
|
const filterGetParent = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
|
|
1239
1239
|
pullFutureContent: true,
|
|
1240
|
+
showMembershipRestrictedContent: true // Show parent even without permissions
|
|
1240
1241
|
}).buildFilter()
|
|
1241
1242
|
const filterForParentList = await new FilterBuilder(
|
|
1242
1243
|
`references(^._id) && _type == ^.parent_type`,
|
|
1243
1244
|
{
|
|
1244
1245
|
pullFutureContent: true,
|
|
1245
1246
|
isParentFilter: true,
|
|
1247
|
+
showMembershipRestrictedContent: true // Show parent even without permissions
|
|
1246
1248
|
}
|
|
1247
1249
|
).buildFilter()
|
|
1248
1250
|
|
|
1249
|
-
const childrenFilter = await new FilterBuilder(``, {
|
|
1251
|
+
const childrenFilter = await new FilterBuilder(``, {
|
|
1252
|
+
isChildrenFilter: true,
|
|
1253
|
+
showMembershipRestrictedContent: true // Show all lessons in sidebar, need_access applied on individual page
|
|
1254
|
+
}).buildFilter()
|
|
1250
1255
|
|
|
1251
1256
|
const brandString = brand ? ` && brand == "${brand}"` : ''
|
|
1252
1257
|
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission[]->railcontent_id, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
|
|
@@ -1260,7 +1265,7 @@ export async function fetchSiblingContent(railContentId, brand = null) {
|
|
|
1260
1265
|
"related_lessons" : *[${filterGetParent}][0].child[${childrenFilter}]->{${queryFields}}
|
|
1261
1266
|
}`
|
|
1262
1267
|
|
|
1263
|
-
let result = await fetchSanity(query, false)
|
|
1268
|
+
let result = await fetchSanity(query, false, { processNeedAccess: true })
|
|
1264
1269
|
|
|
1265
1270
|
//there's no way in sanity to retrieve the index of an array, so we must calculate after fetch
|
|
1266
1271
|
if (result['for-calculations'] && result['for-calculations']['parents-list']) {
|
|
@@ -1288,10 +1293,12 @@ export async function fetchRelatedLessons(railContentId) {
|
|
|
1288
1293
|
const defaultFilterFields = `_type==^._type && brand == ^.brand && railcontent_id != ${railContentId}`
|
|
1289
1294
|
|
|
1290
1295
|
const filterSameArtist = await new FilterBuilder(
|
|
1291
|
-
`${defaultFilterFields} && references(^.artist->_id)
|
|
1296
|
+
`${defaultFilterFields} && references(^.artist->_id)`,
|
|
1297
|
+
{ showMembershipRestrictedContent: true }
|
|
1292
1298
|
).buildFilter()
|
|
1293
1299
|
const filterSameGenre = await new FilterBuilder(
|
|
1294
|
-
`${defaultFilterFields} && references(^.genre[]->_id)
|
|
1300
|
+
`${defaultFilterFields} && references(^.genre[]->_id)`,
|
|
1301
|
+
{ showMembershipRestrictedContent: true }
|
|
1295
1302
|
).buildFilter()
|
|
1296
1303
|
|
|
1297
1304
|
const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
|
|
@@ -1303,7 +1310,7 @@ export async function fetchRelatedLessons(railContentId) {
|
|
|
1303
1310
|
...(*[${filterSameGenre}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
|
|
1304
1311
|
])[0...10]}`
|
|
1305
1312
|
|
|
1306
|
-
return await fetchSanity(query, false)
|
|
1313
|
+
return await fetchSanity(query, false, { processNeedAccess: true })
|
|
1307
1314
|
}
|
|
1308
1315
|
|
|
1309
1316
|
/**
|
|
@@ -1633,13 +1640,13 @@ export async function fetchSanity(
|
|
|
1633
1640
|
body: JSON.stringify({ query: query }),
|
|
1634
1641
|
}
|
|
1635
1642
|
|
|
1643
|
+
const adapter = getPermissionsAdapter()
|
|
1636
1644
|
let promisesResult = await Promise.all([
|
|
1637
1645
|
fetch(url, options),
|
|
1638
|
-
processNeedAccess ? fetchUserPermissions() : null,
|
|
1646
|
+
processNeedAccess ? adapter.fetchUserPermissions() : null,
|
|
1639
1647
|
])
|
|
1640
1648
|
const response = promisesResult[0]
|
|
1641
|
-
const userPermissions = promisesResult[1]
|
|
1642
|
-
const isAdmin = promisesResult[1]?.isAdmin
|
|
1649
|
+
const userPermissions = promisesResult[1]
|
|
1643
1650
|
|
|
1644
1651
|
if (!response.ok) {
|
|
1645
1652
|
throw new Error(`Sanity API error: ${response.status} - ${response.statusText}`)
|
|
@@ -1651,7 +1658,7 @@ export async function fetchSanity(
|
|
|
1651
1658
|
throw new Error('No results found')
|
|
1652
1659
|
}
|
|
1653
1660
|
results = processNeedAccess
|
|
1654
|
-
? await needsAccessDecorator(results, userPermissions
|
|
1661
|
+
? await needsAccessDecorator(results, userPermissions)
|
|
1655
1662
|
: results
|
|
1656
1663
|
results = processPageType
|
|
1657
1664
|
? pageTypeDecorator(results)
|
|
@@ -1669,7 +1676,17 @@ export async function fetchSanity(
|
|
|
1669
1676
|
function contentResultsDecorator(results, fieldName, callback) {
|
|
1670
1677
|
if (Array.isArray(results)) {
|
|
1671
1678
|
results.forEach((result) => {
|
|
1672
|
-
|
|
1679
|
+
// Check if this is a content row structure
|
|
1680
|
+
if (result.content && Array.isArray(result.content)) {
|
|
1681
|
+
// Content rows structure: array of rows, each with a content array
|
|
1682
|
+
result.content.forEach((contentItem) => {
|
|
1683
|
+
if (contentItem) {
|
|
1684
|
+
contentItem[fieldName] = callback(contentItem)
|
|
1685
|
+
}
|
|
1686
|
+
})
|
|
1687
|
+
} else {
|
|
1688
|
+
result[fieldName] = callback(result)
|
|
1689
|
+
}
|
|
1673
1690
|
})
|
|
1674
1691
|
} else if (results.entity && Array.isArray(results.entity)) {
|
|
1675
1692
|
// Group By
|
|
@@ -1699,28 +1716,20 @@ function pageTypeDecorator(results) {
|
|
|
1699
1716
|
})
|
|
1700
1717
|
}
|
|
1701
1718
|
|
|
1702
|
-
|
|
1719
|
+
|
|
1720
|
+
function needsAccessDecorator(results, userPermissions) {
|
|
1703
1721
|
if (globalConfig.sanityConfig.useDummyRailContentMethods) return results
|
|
1704
|
-
|
|
1722
|
+
const adapter = getPermissionsAdapter()
|
|
1705
1723
|
return contentResultsDecorator(results, 'need_access', function (content) {
|
|
1706
|
-
return
|
|
1724
|
+
return adapter.doesUserNeedAccess(content, userPermissions)
|
|
1707
1725
|
})
|
|
1708
1726
|
}
|
|
1709
1727
|
|
|
1710
|
-
function doesUserNeedAccessToContent(result, userPermissions
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
if (permissions.size === 0) {
|
|
1716
|
-
return false
|
|
1717
|
-
}
|
|
1718
|
-
for (let permission of permissions) {
|
|
1719
|
-
if (userPermissions.has(permission)) {
|
|
1720
|
-
return false
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
return true
|
|
1728
|
+
function doesUserNeedAccessToContent(result, userPermissions) {
|
|
1729
|
+
// Legacy function - now delegates to adapter
|
|
1730
|
+
// Kept for backwards compatibility if used elsewhere
|
|
1731
|
+
const adapter = getPermissionsAdapter()
|
|
1732
|
+
return adapter.doesUserNeedAccess(result, userPermissions)
|
|
1724
1733
|
}
|
|
1725
1734
|
|
|
1726
1735
|
/**
|
|
@@ -1992,6 +2001,7 @@ export async function fetchTabData(
|
|
|
1992
2001
|
includedFields = [],
|
|
1993
2002
|
progressIds = undefined,
|
|
1994
2003
|
progress = 'all',
|
|
2004
|
+
showMembershipRestrictedContent = false,
|
|
1995
2005
|
} = {}
|
|
1996
2006
|
) {
|
|
1997
2007
|
const start = (page - 1) * limit
|
|
@@ -2028,7 +2038,7 @@ export async function fetchTabData(
|
|
|
2028
2038
|
let filter = ''
|
|
2029
2039
|
|
|
2030
2040
|
filter = `brand == "${brand}" && (defined(railcontent_id)) ${includedFieldsFilter} ${progressFilter}`
|
|
2031
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
2041
|
+
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true, showMembershipRestrictedContent: true }).buildFilter()
|
|
2032
2042
|
const childrenFields = await getChildFieldsForContentType('tab-data')
|
|
2033
2043
|
const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()
|
|
2034
2044
|
entityFieldsString = ` ${fieldsString}
|
|
@@ -2043,14 +2053,14 @@ export async function fetchTabData(
|
|
|
2043
2053
|
),
|
|
2044
2054
|
length_in_seconds
|
|
2045
2055
|
),`
|
|
2046
|
-
const filterWithRestrictions = await new FilterBuilder(filter, {}).buildFilter()
|
|
2056
|
+
const filterWithRestrictions = await new FilterBuilder(filter, {showMembershipRestrictedContent: true}).buildFilter()
|
|
2047
2057
|
query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
|
|
2048
2058
|
sortOrder: sortOrder,
|
|
2049
2059
|
start: start,
|
|
2050
2060
|
end: end,
|
|
2051
2061
|
})
|
|
2052
2062
|
|
|
2053
|
-
let results = await fetchSanity(query, true)
|
|
2063
|
+
let results = await fetchSanity(query, true, {processNeedAccess: true});
|
|
2054
2064
|
|
|
2055
2065
|
if (['recent', 'incomplete', 'completed'].includes(progress) && results.entity.length > 1) {
|
|
2056
2066
|
const orderMap = new Map(progressIds.map((id, index) => [id, index]))
|
|
@@ -2162,3 +2172,56 @@ export async function fetchMethodV2Structure(brand) {
|
|
|
2162
2172
|
}`
|
|
2163
2173
|
return await fetchSanity(query, false)
|
|
2164
2174
|
}
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* Fetch content owned by the user (excluding membership content).
|
|
2178
|
+
* Shows only content accessible through purchases/entitlements, not through membership.
|
|
2179
|
+
*
|
|
2180
|
+
* @param {string} brand - The brand to filter content by
|
|
2181
|
+
* @param {Object} options - Fetch options
|
|
2182
|
+
* @param {Array<string>} options.type - Content type(s) to filter (optional array, default: [])
|
|
2183
|
+
* @param {number} options.page - Page number (default: 1)
|
|
2184
|
+
* @param {number} options.limit - Items per page (default: 10)
|
|
2185
|
+
* @param {string} options.sort - Sort field and direction (default: '-published_on')
|
|
2186
|
+
* @returns {Promise<Object>} Object with 'entity' (content array) and 'total' (count)
|
|
2187
|
+
*/
|
|
2188
|
+
export async function fetchOwnedContent(
|
|
2189
|
+
brand,
|
|
2190
|
+
{
|
|
2191
|
+
type = [],
|
|
2192
|
+
page = 1,
|
|
2193
|
+
limit = 10,
|
|
2194
|
+
sort = '-published_on',
|
|
2195
|
+
} = {}
|
|
2196
|
+
) {
|
|
2197
|
+
const start = (page - 1) * limit
|
|
2198
|
+
const end = start + limit
|
|
2199
|
+
|
|
2200
|
+
// Determine the sort order
|
|
2201
|
+
const sortOrder = getSortOrder(sort, brand)
|
|
2202
|
+
|
|
2203
|
+
// Build the type filter
|
|
2204
|
+
let typeFilter = ''
|
|
2205
|
+
if (type.length > 0) {
|
|
2206
|
+
const typesString = type.map(t => `'${t}'`).join(', ')
|
|
2207
|
+
typeFilter = `&& _type in [${typesString}]`
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// Build the base filter
|
|
2211
|
+
const filter = `brand == "${brand}" ${typeFilter}`
|
|
2212
|
+
|
|
2213
|
+
// Apply owned content filter
|
|
2214
|
+
const filterWithRestrictions = await new FilterBuilder(filter, {
|
|
2215
|
+
showOnlyOwnedContent: true, // Key parameter: exclude membership content
|
|
2216
|
+
}).buildFilter()
|
|
2217
|
+
|
|
2218
|
+
const fieldsString = DEFAULT_FIELDS.join(',')
|
|
2219
|
+
|
|
2220
|
+
const query = buildEntityAndTotalQuery(filterWithRestrictions, fieldsString, {
|
|
2221
|
+
sortOrder: sortOrder,
|
|
2222
|
+
start: start,
|
|
2223
|
+
end: end,
|
|
2224
|
+
})
|
|
2225
|
+
|
|
2226
|
+
return fetchSanity(query, true)
|
|
2227
|
+
}
|
package/src/services/types.js
CHANGED
|
@@ -39,4 +39,5 @@
|
|
|
39
39
|
* @property {Object} localStorage - Cache to use for localStorage
|
|
40
40
|
* @property {boolean} isMA - Variable that tells if the library is used by MA or FEW
|
|
41
41
|
* @property {string} localTimezoneString - The local timezone string in format: America/Vancouver
|
|
42
|
+
* @property {('v1'|'v2')} [permissionsVersion='v1'] - Permissions system version to use ('v1' or 'v2')
|
|
42
43
|
*/
|
|
@@ -20,7 +20,7 @@ let lastUpdatedKey = `userPermissions_lastUpdated`
|
|
|
20
20
|
* @returns {Promise<UserPermissions>} - The user permissions data.
|
|
21
21
|
*/
|
|
22
22
|
export async function fetchUserPermissions() {
|
|
23
|
-
if (!userPermissionsPromise || wasLastUpdateOlderThanXSeconds(10, lastUpdatedKey)) {
|
|
23
|
+
if (!userPermissionsPromise || await wasLastUpdateOlderThanXSeconds(10, lastUpdatedKey)) {
|
|
24
24
|
userPermissionsPromise = fetchUserPermissionsData()
|
|
25
25
|
setLastUpdatedTime(lastUpdatedKey)
|
|
26
26
|
}
|
package/test/content.test.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { initializeTestService } from './initializeTests.js'
|
|
2
2
|
import {getContentRows, getNewAndUpcoming, getScheduleContentRows, getTabResults} from '../src/services/content.js'
|
|
3
3
|
|
|
4
|
+
// Mock fetchContentProgress before other modules load
|
|
5
|
+
jest.mock('../src/services/railcontent.js', () => ({
|
|
6
|
+
...jest.requireActual('../src/services/railcontent.js'),
|
|
7
|
+
fetchContentProgress: jest.fn().mockResolvedValue({ version: 1, data: {} })
|
|
8
|
+
}))
|
|
4
9
|
|
|
5
10
|
const railContentModule = require('../src/services/railcontent.js')
|
|
6
11
|
|
package/test/forum.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { initializeTestService } from './initializeTests.js'
|
|
2
2
|
import { getLessonContentRows, getTabResults } from '../src/services/content.js'
|
|
3
|
-
import {getActiveDiscussions} from "../src/services/
|
|
3
|
+
import {getActiveDiscussions} from "../src/services/forums/forums";
|
|
4
4
|
|
|
5
5
|
describe('forum', function () {
|
|
6
6
|
beforeEach(() => {
|
package/test/initializeTests.js
CHANGED
|
@@ -45,7 +45,9 @@ export async function initializeTestService(useLive = false, isAdmin = false) {
|
|
|
45
45
|
isMA: true,
|
|
46
46
|
}
|
|
47
47
|
initializeService(config)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
|
|
49
|
+
// Mock user permissions
|
|
50
|
+
let permissionsMock = jest.spyOn(railContentModule, 'fetchUserPermissionsData')
|
|
51
|
+
let permissionsData = { permissions: [108, 91, 92], isAdmin: isAdmin }
|
|
52
|
+
permissionsMock.mockImplementation(() => permissionsData)
|
|
51
53
|
}
|
package/tools/generate-index.cjs
CHANGED
|
@@ -73,6 +73,11 @@ treeElements.forEach((treeNode) => {
|
|
|
73
73
|
if (fs.lstatSync(filePath).isFile()) {
|
|
74
74
|
addFunctionsToFileExports(filePath, treeNode)
|
|
75
75
|
} else if (fs.lstatSync(filePath).isDirectory()) {
|
|
76
|
+
// Skip the permissions directory - it has its own index.ts barrel export
|
|
77
|
+
if (treeNode === 'permissions') {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
const subDir = fs.readdirSync(filePath)
|
|
77
82
|
subDir.forEach((subFile) => {
|
|
78
83
|
const filePath = path.join(servicesDir, treeNode, subFile)
|