musora-content-services 2.81.0 → 2.82.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/.claude/settings.local.json +14 -0
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +17 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/Content.html +0 -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 +3 -2
- package/docs/content.js.html +88 -10
- package/docs/content_artist.ts.html +2 -2
- package/docs/content_content.ts.html +0 -0
- package/docs/content_genre.ts.html +2 -2
- package/docs/content_instructor.ts.html +2 -2
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/forums_categories.ts.html +2 -2
- package/docs/forums_discussions.js.html +0 -0
- package/docs/forums_forum.js.html +0 -0
- 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.js.html +0 -0
- package/docs/gamification_awards.ts.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/gamification_types.js.html +0 -0
- 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 +2 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Categories.html +0 -0
- package/docs/module-Config.html +5 -4
- package/docs/module-Content-Services-V2.html +440 -9
- package/docs/module-ForumCategories.html +0 -0
- package/docs/module-ForumDiscussions.html +0 -0
- package/docs/module-Forums.html +2 -2
- package/docs/module-Genre.html +2 -2
- package/docs/module-GuidedCourses.html +2 -2
- package/docs/module-Instructor.html +2 -2
- 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 +16 -16
- package/docs/module-ProgressRow.html +2 -2
- package/docs/module-Railcontent-Services.html +2 -2
- package/docs/module-Sanity-Services.html +1796 -55
- package/docs/module-Sessions.html +2 -2
- package/docs/module-Threads.html +0 -0
- 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 +130 -28
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- 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.js.html +0 -0
- 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 +2 -2
- 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/jest.config.js +0 -0
- package/jsdoc.json +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +0 -0
- package/src/contentTypeConfig.js +33 -1
- package/src/filterBuilder.js +22 -12
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/httpHelper.js +0 -0
- package/src/lib/lastUpdated.js +4 -4
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/config.js +3 -0
- package/src/services/content/artist.ts +0 -0
- package/src/services/content/content.ts +0 -0
- package/src/services/content/genre.ts +0 -0
- package/src/services/content/instructor.ts +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/guided-courses.ts +0 -0
- package/src/services/content-org/learning-paths.ts +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/content.js +86 -8
- package/src/services/contentAggregator.js +4 -4
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +16 -3
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/eventsAPI.js +0 -0
- package/src/services/forums/categories.ts +0 -0
- package/src/services/forums/forums.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/forums/threads.ts +0 -0
- package/src/services/forums/types.ts +0 -0
- package/src/services/gamification/awards.ts +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/liveTesting.ts +0 -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/progress-row/method-card.js +0 -0
- package/src/services/railcontent.js +0 -0
- package/src/services/recommendations.js +0 -0
- package/src/services/sanity.js +103 -40
- package/src/services/types.js +1 -0
- package/src/services/user/account.ts +0 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/memberships.ts +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/onboarding.ts +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/permissions.js +1 -1
- package/src/services/user/profile.js +0 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.d.ts +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +0 -0
- package/test/HttpClient.test.js +0 -0
- package/test/content.test.js +5 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +1 -1
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/initializeTests.js +5 -3
- package/test/learningPaths.test.js +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/notifications.test.js +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +5 -0
package/src/lib/lastUpdated.js
CHANGED
|
@@ -15,8 +15,8 @@ const excludeFromGeneratedIndex = ['wasLastUpdateOlderThanXSeconds', 'setLastUpd
|
|
|
15
15
|
*
|
|
16
16
|
* @returns {boolean} - True if the last update was older than X seconds, false otherwise.
|
|
17
17
|
*/
|
|
18
|
-
export function wasLastUpdateOlderThanXSeconds(seconds, key) {
|
|
19
|
-
let lastUpdated = globalConfig.localStorage.getItem(key)
|
|
18
|
+
export async function wasLastUpdateOlderThanXSeconds(seconds, key) {
|
|
19
|
+
let lastUpdated = await globalConfig.localStorage.getItem(key)
|
|
20
20
|
if (!lastUpdated) return false
|
|
21
21
|
const verifyServerTime = seconds * 1000
|
|
22
22
|
return new Date().getTime() - lastUpdated > verifyServerTime
|
|
@@ -29,6 +29,6 @@ export function wasLastUpdateOlderThanXSeconds(seconds, key) {
|
|
|
29
29
|
*
|
|
30
30
|
* @returns {void}
|
|
31
31
|
*/
|
|
32
|
-
export function setLastUpdatedTime(key) {
|
|
33
|
-
globalConfig.localStorage.setItem(key, new Date().getTime()?.toString())
|
|
32
|
+
export async function setLastUpdatedTime(key) {
|
|
33
|
+
await globalConfig.localStorage.setItem(key, new Date().getTime()?.toString())
|
|
34
34
|
}
|
|
File without changes
|
|
File without changes
|
package/src/services/config.js
CHANGED
|
@@ -12,6 +12,7 @@ export let globalConfig = {
|
|
|
12
12
|
localStorage: null,
|
|
13
13
|
isMA: false,
|
|
14
14
|
localTimezoneString: null, // In format: America/Vancouver
|
|
15
|
+
permissionsVersion: 'v1', // 'v1' or 'v2'
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -52,6 +53,7 @@ const excludeFromGeneratedIndex = []
|
|
|
52
53
|
* baseUrl: 'https://web-staging-one.musora.com',
|
|
53
54
|
* localStorage: localStorage,
|
|
54
55
|
* isMA: false,
|
|
56
|
+
* permissionsVersion: 'v1', // Optional: 'v1' (default) or 'v2'
|
|
55
57
|
* });
|
|
56
58
|
*/
|
|
57
59
|
export function initializeService(config) {
|
|
@@ -62,4 +64,5 @@ export function initializeService(config) {
|
|
|
62
64
|
globalConfig.localStorage = config.localStorage
|
|
63
65
|
globalConfig.isMA = config.isMA || false
|
|
64
66
|
globalConfig.localTimezoneString = config.localTimezoneString || null
|
|
67
|
+
globalConfig.permissionsVersion = config.permissionsVersion || 'v2'
|
|
65
68
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/content.js
CHANGED
|
@@ -11,31 +11,52 @@ import {
|
|
|
11
11
|
fetchUpcomingEvents,
|
|
12
12
|
fetchScheduledReleases,
|
|
13
13
|
fetchReturning,
|
|
14
|
-
fetchLeaving, fetchScheduledAndNewReleases, fetchContentRows
|
|
14
|
+
fetchLeaving, fetchScheduledAndNewReleases, fetchContentRows, fetchOwnedContent
|
|
15
15
|
} from './sanity.js'
|
|
16
16
|
import {TabResponseType, Tabs, capitalizeFirstLetter} from '../contentMetaData.js'
|
|
17
17
|
import {recommendations, rankCategories, rankItems} from "./recommendations";
|
|
18
18
|
import {addContextToContent} from "./contentAggregator.js";
|
|
19
|
+
import {globalConfig} from "./config";
|
|
20
|
+
import {getUserData} from "./user/management";
|
|
21
|
+
import {filterTypes, ownedContentTypes} from "../contentTypeConfig";
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
|
|
22
|
-
|
|
25
|
+
const [recentContentIds, rawContentRows, userData] = await Promise.all([
|
|
23
26
|
fetchRecent(brand, pageName, { progress: 'recent', limit: 10 }),
|
|
24
|
-
getContentRows(brand, pageName)
|
|
27
|
+
getContentRows(brand, pageName),
|
|
28
|
+
getUserData()
|
|
25
29
|
])
|
|
26
30
|
|
|
27
|
-
contentRows = Array.isArray(
|
|
31
|
+
const contentRows = Array.isArray(rawContentRows) ? rawContentRows : []
|
|
32
|
+
|
|
33
|
+
// Only fetch owned content if user has no active membership
|
|
34
|
+
if (!userData?.has_active_membership) {
|
|
35
|
+
const type = ownedContentTypes[pageName] || []
|
|
36
|
+
|
|
37
|
+
const ownedContent = await fetchOwnedContent(brand, { type })
|
|
38
|
+
if (ownedContent?.entity && ownedContent.entity.length > 0) {
|
|
39
|
+
contentRows.unshift({
|
|
40
|
+
id: 'owned',
|
|
41
|
+
title: 'Owned ' + capitalizeFirstLetter(pageName),
|
|
42
|
+
items: ownedContent.entity
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add recent content row
|
|
28
48
|
contentRows.unshift({
|
|
29
49
|
id: 'recent',
|
|
30
50
|
title: 'Recent ' + capitalizeFirstLetter(pageName),
|
|
31
51
|
items: recentContentIds || []
|
|
32
|
-
})
|
|
52
|
+
})
|
|
33
53
|
|
|
34
54
|
const results = await Promise.all(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
contentRows.map(async (row) => {
|
|
56
|
+
return { id: row.id, title: row.title, items: row.items }
|
|
57
|
+
})
|
|
38
58
|
)
|
|
59
|
+
|
|
39
60
|
return results
|
|
40
61
|
}
|
|
41
62
|
|
|
@@ -213,6 +234,7 @@ export async function getContentRows(brand, pageName, contentRowSlug = null, {
|
|
|
213
234
|
}
|
|
214
235
|
slugNameMap[category.slug] = category.name
|
|
215
236
|
}
|
|
237
|
+
|
|
216
238
|
const start = (page - 1) * limit
|
|
217
239
|
const end = start + limit
|
|
218
240
|
const sortedData = await rankCategories(brand, recData)
|
|
@@ -451,3 +473,59 @@ export async function getLegacyMethods(brand) {
|
|
|
451
473
|
},
|
|
452
474
|
]
|
|
453
475
|
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Fetches content owned by the user (excluding membership content).
|
|
479
|
+
* Shows only content accessible through purchases/entitlements, not through membership.
|
|
480
|
+
*
|
|
481
|
+
* @param {string} brand - The brand for which to fetch owned content.
|
|
482
|
+
* @param {Object} [params={}] - Parameters for pagination and sorting.
|
|
483
|
+
* @param {Array<string>} [params.type=[]] - Content type(s) to filter (optional array).
|
|
484
|
+
* @param {number} [params.page=1] - The page number for pagination.
|
|
485
|
+
* @param {number} [params.limit=10] - The number of items per page.
|
|
486
|
+
* @param {string} [params.sort='-published_on'] - The field to sort the data by.
|
|
487
|
+
* @returns {Promise<Object>} - The fetched owned content with entity array and total count.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* // Fetch all owned content with default pagination
|
|
491
|
+
* getOwnedContent('drumeo')
|
|
492
|
+
* .then(content => console.log(content))
|
|
493
|
+
* .catch(error => console.error(error));
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* // Fetch owned content with custom pagination and sorting
|
|
497
|
+
* getOwnedContent('drumeo', {
|
|
498
|
+
* page: 2,
|
|
499
|
+
* limit: 20,
|
|
500
|
+
* sort: '-published_on'
|
|
501
|
+
* })
|
|
502
|
+
* .then(content => console.log(content))
|
|
503
|
+
* .catch(error => console.error(error));
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* // Fetch owned content filtered by types
|
|
507
|
+
* getOwnedContent('drumeo', {
|
|
508
|
+
* type: ['course', 'pack'],
|
|
509
|
+
* page: 1,
|
|
510
|
+
* limit: 10
|
|
511
|
+
* })
|
|
512
|
+
* .then(content => console.log(content))
|
|
513
|
+
* .catch(error => console.error(error));
|
|
514
|
+
*/
|
|
515
|
+
export async function getOwnedContent(brand, {
|
|
516
|
+
type = [],
|
|
517
|
+
page = 1,
|
|
518
|
+
limit = 10,
|
|
519
|
+
sort = '-published_on',
|
|
520
|
+
} = {}) {
|
|
521
|
+
const data = await fetchOwnedContent(brand, { type, page, limit, sort });
|
|
522
|
+
|
|
523
|
+
if (!data) {
|
|
524
|
+
return {
|
|
525
|
+
entity: [],
|
|
526
|
+
total: 0
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return data;
|
|
531
|
+
}
|
|
@@ -108,9 +108,9 @@ export async function addContextToContent(dataPromise, ...dataArgs) {
|
|
|
108
108
|
|
|
109
109
|
const addContext = async (item) => ({
|
|
110
110
|
...item,
|
|
111
|
-
...(addProgressPercentage ? { progressPercentage: progressData?.[item.id]
|
|
112
|
-
...(addProgressStatus ? { progressStatus: progressData?.[item.id]
|
|
113
|
-
...(addProgressTimestamp ? { progressTimestamp: progressData?.[item.id]
|
|
111
|
+
...(addProgressPercentage ? { progressPercentage: progressData?.[item.id]?.progress } : {}),
|
|
112
|
+
...(addProgressStatus ? { progressStatus: progressData?.[item.id]?.status } : {}),
|
|
113
|
+
...(addProgressTimestamp ? { progressTimestamp: progressData?.[item.id]?.last_update } : {}),
|
|
114
114
|
...(addIsLiked ? { isLiked: isLikedData?.[item.id] } : {}),
|
|
115
115
|
...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
|
|
116
116
|
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
|
|
@@ -144,7 +144,7 @@ export async function getNavigateToForPlaylists(data, { dataField = null } = {})
|
|
|
144
144
|
const progress = progressOnItems[itemId]
|
|
145
145
|
return progress && progress === 'completed'
|
|
146
146
|
})
|
|
147
|
-
let nextItem = accessibleItems[0] ?? null
|
|
147
|
+
let nextItem = accessibleItems[0] ?? playlist.items[0] ?? null
|
|
148
148
|
if (!allItemsCompleted) {
|
|
149
149
|
const lastItemProgress = progressOnItems[playlist.last_engaged_on]
|
|
150
150
|
const index = accessibleItems.findIndex((i) => i.content_id === playlist.last_engaged_on)
|
|
File without changes
|
|
@@ -50,7 +50,10 @@ export async function getNextLesson(data) {
|
|
|
50
50
|
let nextLessonData = {}
|
|
51
51
|
|
|
52
52
|
for (const content of data) {
|
|
53
|
-
|
|
53
|
+
// Skip null/undefined entries (can happen when GROQ dereference doesn't match filter)
|
|
54
|
+
if (!content) continue
|
|
55
|
+
|
|
56
|
+
const children = content.children?.filter(Boolean).map((child) => child.id) ?? []
|
|
54
57
|
//only calculate nextLesson if needed, based on content type
|
|
55
58
|
if (!getNextLessonLessonParentTypes.includes(content.type)) {
|
|
56
59
|
nextLessonData[content.id] = null
|
|
@@ -101,20 +104,30 @@ export async function getNavigateTo(data) {
|
|
|
101
104
|
//TODO add parent hierarchy upwards as well
|
|
102
105
|
// data structure is the same but instead of child{} we use parent{}
|
|
103
106
|
for (const content of data) {
|
|
107
|
+
// Skip null/undefined entries (can happen when GROQ dereference doesn't match filter)
|
|
108
|
+
if (!content) continue
|
|
109
|
+
|
|
104
110
|
//only calculate nextLesson if needed, based on content type
|
|
105
111
|
if (!getNextLessonLessonParentTypes.includes(content.type) || !content.children) {
|
|
106
112
|
navigateToData[content.id] = null
|
|
107
113
|
} else {
|
|
114
|
+
// Filter out null/undefined children (can happen with permission filters)
|
|
115
|
+
const validChildren = content.children.filter(Boolean)
|
|
116
|
+
if (validChildren.length === 0) {
|
|
117
|
+
navigateToData[content.id] = null
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
const children = new Map()
|
|
109
122
|
const childrenIds = []
|
|
110
|
-
|
|
123
|
+
validChildren.forEach((child) => {
|
|
111
124
|
childrenIds.push(child.id)
|
|
112
125
|
children.set(child.id, child)
|
|
113
126
|
})
|
|
114
127
|
// return first child (or grand child) if parent-content is complete or no progress
|
|
115
128
|
const contentState = await getProgressState(content.id)
|
|
116
129
|
if (contentState !== STATE_STARTED) {
|
|
117
|
-
const firstChild =
|
|
130
|
+
const firstChild = validChildren[0]
|
|
118
131
|
let lastInteractedChildNavToData = await getNavigateTo([firstChild])
|
|
119
132
|
lastInteractedChildNavToData = lastInteractedChildNavToData[firstChild.id] ?? null
|
|
120
133
|
navigateToData[content.id] = buildNavigateTo(firstChild, lastInteractedChildNavToData)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module PermissionsAdapter
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class defining the contract for permissions operations.
|
|
5
|
+
* This abstraction allows swapping between different permission implementations
|
|
6
|
+
* (v1 and v2) without changing consumer code.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* User permissions data structure
|
|
11
|
+
*/
|
|
12
|
+
export interface UserPermissions {
|
|
13
|
+
/** Array of permission IDs the user has access to */
|
|
14
|
+
permissions: string[]
|
|
15
|
+
/** Whether the user is an admin */
|
|
16
|
+
isAdmin: boolean
|
|
17
|
+
/** Whether the user has basic membership */
|
|
18
|
+
isABasicMember: boolean
|
|
19
|
+
/** User's access level (v2 - for future use) */
|
|
20
|
+
access_level?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for generating permission filters
|
|
25
|
+
*/
|
|
26
|
+
export interface PermissionFilterOptions {
|
|
27
|
+
/** GROQ prefix for nested queries (e.g., '^.' for parent, '@->' for children) */
|
|
28
|
+
prefix?: string
|
|
29
|
+
/**
|
|
30
|
+
* If true, show content that requires paid membership even if user doesn't have it.
|
|
31
|
+
* Used for upgrade prompts. V1: includes permissions [91, 92]. V2: shows content with membership_tier='plus'|'basic'|'free'
|
|
32
|
+
*/
|
|
33
|
+
showMembershipRestrictedContent?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* If true, show only content owned by user through purchases/entitlements, excluding membership content.
|
|
36
|
+
* V1: excludes permissions [91, 92]. V2: excludes content with membership_tier='plus'|'basic'
|
|
37
|
+
*/
|
|
38
|
+
showOnlyOwnedContent?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Content item with permission requirements
|
|
43
|
+
*/
|
|
44
|
+
export interface ContentItem {
|
|
45
|
+
/** Array of permission IDs required to access this content */
|
|
46
|
+
permission_id?: string[]
|
|
47
|
+
[key: string]: any
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Abstract base class for permissions adapters.
|
|
52
|
+
* Subclasses must implement all methods to provide version-specific logic.
|
|
53
|
+
*/
|
|
54
|
+
export abstract class PermissionsAdapter {
|
|
55
|
+
/**
|
|
56
|
+
* Fetch user permissions data.
|
|
57
|
+
*
|
|
58
|
+
* @returns The user's permissions data
|
|
59
|
+
* @abstract
|
|
60
|
+
*/
|
|
61
|
+
abstract fetchUserPermissions(): Promise<UserPermissions>
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if user needs access to specific content.
|
|
65
|
+
* Returns true if the user does NOT have access and needs to upgrade/purchase.
|
|
66
|
+
*
|
|
67
|
+
* @param content - The content item to check
|
|
68
|
+
* @param userPermissions - The user's permissions
|
|
69
|
+
* @returns True if user needs access, false if they already have it
|
|
70
|
+
* @abstract
|
|
71
|
+
*/
|
|
72
|
+
abstract doesUserNeedAccess(
|
|
73
|
+
content: ContentItem,
|
|
74
|
+
userPermissions: UserPermissions
|
|
75
|
+
): boolean
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate GROQ filter string for permissions.
|
|
79
|
+
* Returns null if no filter should be applied (e.g., admin user).
|
|
80
|
+
*
|
|
81
|
+
* @param userPermissions - The user's permissions
|
|
82
|
+
* @param options - Options for filter generation
|
|
83
|
+
* @returns GROQ filter string or null
|
|
84
|
+
* @abstract
|
|
85
|
+
*/
|
|
86
|
+
abstract generatePermissionsFilter(
|
|
87
|
+
userPermissions: UserPermissions,
|
|
88
|
+
options?: PermissionFilterOptions
|
|
89
|
+
): string | null
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get user's permission IDs.
|
|
93
|
+
* Useful for cases where you need raw permission IDs.
|
|
94
|
+
*
|
|
95
|
+
* @param userPermissions - The user's permissions
|
|
96
|
+
* @returns Array of permission IDs
|
|
97
|
+
* @abstract
|
|
98
|
+
*/
|
|
99
|
+
abstract getUserPermissionIds(userPermissions: UserPermissions): string[]
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if user is an admin.
|
|
103
|
+
* Admins typically bypass all permission checks.
|
|
104
|
+
*
|
|
105
|
+
* @param userPermissions - The user's permissions
|
|
106
|
+
* @returns True if user is admin
|
|
107
|
+
*/
|
|
108
|
+
isAdmin(userPermissions: UserPermissions): boolean {
|
|
109
|
+
return userPermissions?.isAdmin ?? false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module PermissionsAdapterFactory
|
|
3
|
+
*
|
|
4
|
+
* Factory for creating the appropriate permissions adapter based on configuration.
|
|
5
|
+
* Provides a single point of control for switching between v1 and v2 implementations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PermissionsAdapter } from './PermissionsAdapter'
|
|
9
|
+
import { PermissionsV1Adapter } from './PermissionsV1Adapter'
|
|
10
|
+
import { PermissionsV2Adapter } from './PermissionsV2Adapter'
|
|
11
|
+
import { globalConfig } from '../config.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Valid permissions version types
|
|
15
|
+
*/
|
|
16
|
+
export type PermissionsVersion = 'v1' | 'v2'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Singleton instance of the permissions adapter.
|
|
20
|
+
* Created once and reused throughout the application.
|
|
21
|
+
*/
|
|
22
|
+
let adapterInstance: PermissionsAdapter | null = null
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the appropriate permissions adapter based on configuration.
|
|
26
|
+
*
|
|
27
|
+
* This is a singleton - the same adapter instance is returned on every call.
|
|
28
|
+
* To switch versions, change globalConfig.permissionsVersion via initializeService().
|
|
29
|
+
*
|
|
30
|
+
* @returns The permissions adapter instance
|
|
31
|
+
* @throws Error if an invalid permissions version is configured
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* import { getPermissionsAdapter } from './permissions/PermissionsAdapterFactory.js'
|
|
35
|
+
*
|
|
36
|
+
* const adapter = getPermissionsAdapter()
|
|
37
|
+
* const permissions = await adapter.fetchUserPermissions()
|
|
38
|
+
*/
|
|
39
|
+
export function getPermissionsAdapter(): PermissionsAdapter {
|
|
40
|
+
if (adapterInstance) {
|
|
41
|
+
return adapterInstance
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const version = (globalConfig.permissionsVersion || 'v1') as PermissionsVersion
|
|
45
|
+
|
|
46
|
+
switch (version.toLowerCase()) {
|
|
47
|
+
case 'v1':
|
|
48
|
+
adapterInstance = new PermissionsV1Adapter()
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
case 'v2':
|
|
52
|
+
adapterInstance = new PermissionsV2Adapter()
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Invalid permissionsVersion: ${version}. Must be 'v1' or 'v2'.`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return adapterInstance
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the current permissions version being used.
|
|
66
|
+
*
|
|
67
|
+
* @returns The permissions version ('v1' or 'v2')
|
|
68
|
+
*/
|
|
69
|
+
export function getPermissionsVersion(): PermissionsVersion {
|
|
70
|
+
return (globalConfig.permissionsVersion || 'v1') as PermissionsVersion
|
|
71
|
+
}
|