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
package/src/index.js
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
getLegacyMethods,
|
|
69
69
|
getLessonContentRows,
|
|
70
70
|
getNewAndUpcoming,
|
|
71
|
+
getOwnedContent,
|
|
71
72
|
getRecent,
|
|
72
73
|
getRecommendedForYou,
|
|
73
74
|
getScheduleContentRows,
|
|
@@ -124,6 +125,7 @@ import {
|
|
|
124
125
|
|
|
125
126
|
import {
|
|
126
127
|
createForumCategory,
|
|
128
|
+
deleteForumCategory,
|
|
127
129
|
fetchForumCategories,
|
|
128
130
|
updateForumCategory
|
|
129
131
|
} from './services/forums/categories.ts';
|
|
@@ -270,6 +272,7 @@ import {
|
|
|
270
272
|
fetchNewReleases,
|
|
271
273
|
fetchNextPreviousLesson,
|
|
272
274
|
fetchOtherSongVersions,
|
|
275
|
+
fetchOwnedContent,
|
|
273
276
|
fetchPackAll,
|
|
274
277
|
fetchPackData,
|
|
275
278
|
fetchPlayAlongsCount,
|
|
@@ -441,6 +444,7 @@ export {
|
|
|
441
444
|
createThread,
|
|
442
445
|
deleteAccount,
|
|
443
446
|
deleteComment,
|
|
447
|
+
deleteForumCategory,
|
|
444
448
|
deleteItemsFromPlaylist,
|
|
445
449
|
deleteNotification,
|
|
446
450
|
deletePicture,
|
|
@@ -518,6 +522,7 @@ export {
|
|
|
518
522
|
fetchNotificationSettings,
|
|
519
523
|
fetchNotifications,
|
|
520
524
|
fetchOtherSongVersions,
|
|
525
|
+
fetchOwnedContent,
|
|
521
526
|
fetchPackAll,
|
|
522
527
|
fetchPackData,
|
|
523
528
|
fetchPlayAlongsCount,
|
|
@@ -580,6 +585,7 @@ export {
|
|
|
580
585
|
getNewAndUpcoming,
|
|
581
586
|
getNextLesson,
|
|
582
587
|
getOnboardingRecommendedContent,
|
|
588
|
+
getOwnedContent,
|
|
583
589
|
getPracticeNotes,
|
|
584
590
|
getPracticeSessions,
|
|
585
591
|
getProgressDateByIds,
|
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
|
}
|
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
|
}
|
|
@@ -74,13 +74,13 @@ export interface ArtistLessonOptions {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export interface LessonsByArtistResponse {
|
|
77
|
-
|
|
77
|
+
data: Artist[]
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Fetch the artist's lessons.
|
|
82
|
-
* @param {string} brand - The brand for which to fetch lessons.
|
|
83
82
|
* @param {string} slug - The slug of the artist
|
|
83
|
+
* @param {string} brand - The brand for which to fetch lessons.
|
|
84
84
|
* @param {string} contentType - The type of the lessons we need to get from the artist. If not defined, groq will get lessons from all content types
|
|
85
85
|
* @param {Object} params - Parameters for sorting, searching, pagination and filtering.
|
|
86
86
|
* @param {string} [params.sort="-published_on"] - The field to sort the lessons by.
|
|
@@ -92,13 +92,13 @@ export interface LessonsByArtistResponse {
|
|
|
92
92
|
* @returns {Promise<LessonsByArtistResponse|null>} - The lessons for the artist and some details about the artist (name and thumbnail).
|
|
93
93
|
*
|
|
94
94
|
* @example
|
|
95
|
-
* fetchArtistLessons('
|
|
95
|
+
* fetchArtistLessons('10 Years', 'drumeo', 'song', {'-published_on', '', 1, 10, ["difficulty,Intermediate"], [232168, 232824, 303375, 232194, 393125]})
|
|
96
96
|
* .then(lessons => console.log(lessons))
|
|
97
97
|
* .catch(error => console.error(error));
|
|
98
98
|
*/
|
|
99
99
|
export async function fetchArtistLessons(
|
|
100
|
-
brand: string,
|
|
101
100
|
slug: string,
|
|
101
|
+
brand: string,
|
|
102
102
|
contentType: string,
|
|
103
103
|
{
|
|
104
104
|
sort = '-published_on',
|
|
@@ -127,7 +127,7 @@ export async function fetchArtistLessons(
|
|
|
127
127
|
progressIds !== undefined ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
|
|
128
128
|
const now = getSanityDate(new Date())
|
|
129
129
|
const query = `{
|
|
130
|
-
"
|
|
130
|
+
"data":
|
|
131
131
|
*[_type == 'artist' && slug.current == '${slug}']
|
|
132
132
|
{'type': _type, name, 'thumbnail':thumbnail_url.asset->url,
|
|
133
133
|
'lessons_count': count(*[${addType} brand == '${brand}' && references(^._id)]),
|
|
@@ -135,5 +135,5 @@ export async function fetchArtistLessons(
|
|
|
135
135
|
[${start}...${end}]}
|
|
136
136
|
|order(${sortOrder})
|
|
137
137
|
}`
|
|
138
|
-
return fetchSanity(query, true)
|
|
138
|
+
return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
|
|
139
139
|
}
|
|
@@ -39,7 +39,7 @@ export async function fetchGenres(brand: string): Promise<Genre[]> {
|
|
|
39
39
|
'thumbnail': thumbnail_url.asset->url,
|
|
40
40
|
"lessons_count": count(*[${filter}])
|
|
41
41
|
} |order(lower(name)) `
|
|
42
|
-
return fetchSanity(query, true, { processNeedAccess: false, processPageType: false})
|
|
42
|
+
return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -68,7 +68,7 @@ export async function fetchGenreBySlug(slug: string, brand?: string): Promise<Ge
|
|
|
68
68
|
'thumbnail':thumbnail_url.asset->url,
|
|
69
69
|
"lessonsCount": count(*[${filter}])
|
|
70
70
|
}`
|
|
71
|
-
return fetchSanity(query, true, { processNeedAccess: false, processPageType: false})
|
|
71
|
+
return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export interface FetchGenreLessonsOptions {
|
|
@@ -81,13 +81,13 @@ export interface FetchGenreLessonsOptions {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export interface LessonsByGenreResponse {
|
|
84
|
-
|
|
84
|
+
data: Genre[]
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Fetch the genre's lessons.
|
|
89
|
-
* @param {string} brand - The brand for which to fetch lessons.
|
|
90
89
|
* @param {string} slug - The slug of the genre
|
|
90
|
+
* @param {string} brand - The brand for which to fetch lessons.
|
|
91
91
|
* @param {Object} params - Parameters for sorting, searching, pagination and filtering.
|
|
92
92
|
* @param {string} [params.sort="-published_on"] - The field to sort the lessons by.
|
|
93
93
|
* @param {string} [params.searchTerm=""] - The search term to filter the lessons.
|
|
@@ -95,16 +95,16 @@ export interface LessonsByGenreResponse {
|
|
|
95
95
|
* @param {number} [params.limit=10] - The number of items per page.
|
|
96
96
|
* @param {Array<string>} [params.includedFields=[]] - Additional filters to apply to the query in the format of a key,value array. eg. ['difficulty,Intermediate', 'genre,rock'].
|
|
97
97
|
* @param {Array<number>} [params.progressIds=[]] - The ids of the lessons that are in progress or completed
|
|
98
|
-
* @returns {Promise<LessonsByGenreResponse>} - The lessons for the artist and some details about the artist (name and thumbnail).
|
|
98
|
+
* @returns {Promise<LessonsByGenreResponse|null>} - The lessons for the artist and some details about the artist (name and thumbnail).
|
|
99
99
|
*
|
|
100
100
|
* @example
|
|
101
|
-
* fetchGenreLessons('
|
|
101
|
+
* fetchGenreLessons('Blues', 'drumeo', 'song', {'-published_on', '', 1, 10, ["difficulty,Intermediate"], [232168, 232824, 303375, 232194, 393125]})
|
|
102
102
|
* .then(lessons => console.log(lessons))
|
|
103
103
|
* .catch(error => console.error(error));
|
|
104
104
|
*/
|
|
105
105
|
export async function fetchGenreLessons(
|
|
106
|
-
brand: string,
|
|
107
106
|
slug: string,
|
|
107
|
+
brand: string,
|
|
108
108
|
contentType: string,
|
|
109
109
|
{
|
|
110
110
|
sort = '-published_on',
|
|
@@ -114,7 +114,7 @@ export async function fetchGenreLessons(
|
|
|
114
114
|
includedFields = [],
|
|
115
115
|
progressIds = [],
|
|
116
116
|
}: FetchGenreLessonsOptions = {}
|
|
117
|
-
): Promise<LessonsByGenreResponse> {
|
|
117
|
+
): Promise<LessonsByGenreResponse | null> {
|
|
118
118
|
const fieldsString = DEFAULT_FIELDS.join(',')
|
|
119
119
|
const start = (page - 1) * limit
|
|
120
120
|
const end = start + limit
|
|
@@ -127,12 +127,15 @@ export async function fetchGenreLessons(
|
|
|
127
127
|
progressIds !== undefined ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
|
|
128
128
|
const now = getSanityDate(new Date())
|
|
129
129
|
const query = `{
|
|
130
|
-
"
|
|
130
|
+
"data":
|
|
131
131
|
*[_type == 'genre' && slug.current == '${slug}']
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
{
|
|
133
|
+
'type': _type,
|
|
134
|
+
name,
|
|
135
|
+
'thumbnail': thumbnail_url.asset->url,
|
|
136
|
+
'lessons_count': count(*[${addType} brand == '${brand}' && references(^._id)]),
|
|
137
|
+
'lessons': *[${addType} brand == '${brand}' && references(^._id) && (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}')) ${searchFilter} ${includedFieldsFilter} ${progressFilter}]{${fieldsString}} [${start}...${end}]
|
|
138
|
+
}
|
|
136
139
|
|order(${sortOrder})
|
|
137
140
|
}`
|
|
138
141
|
return fetchSanity(query, true)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { FilterBuilder } from '../../filterBuilder.js'
|
|
5
5
|
import { filtersToGroq, getFieldsForContentType } from '../../contentTypeConfig.js'
|
|
6
|
-
import {
|
|
6
|
+
import { fetchSanity, getSanityDate, getSortOrder } from '../sanity.js'
|
|
7
7
|
import { Lesson } from './content'
|
|
8
8
|
|
|
9
9
|
export interface Instructor {
|
|
@@ -80,8 +80,7 @@ export interface FetchInstructorLessonsOptions {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export interface InstructorLessonsResponse {
|
|
83
|
-
|
|
84
|
-
total: number
|
|
83
|
+
data: Lesson[]
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
/**
|
|
@@ -96,9 +95,9 @@ export interface InstructorLessonsResponse {
|
|
|
96
95
|
* @param {number} [options.limit=10] - The number of items per page.
|
|
97
96
|
* @param {Array<string>} [options.includedFields=[]] - Additional filters to apply to the query in the format of a key,value array. eg. ['difficulty,Intermediate', 'genre,rock'].
|
|
98
97
|
*
|
|
99
|
-
* @returns {Promise<InstructorLessonsResponse>} - The lessons for the instructor or null if not found.
|
|
98
|
+
* @returns {Promise<InstructorLessonsResponse|null>} - The lessons for the instructor or null if not found.
|
|
100
99
|
* @example
|
|
101
|
-
* fetchInstructorLessons('instructor123')
|
|
100
|
+
* fetchInstructorLessons('instructor123', 'drumeo', { page: 2, limit: 10 })
|
|
102
101
|
* .then(lessons => console.log(lessons))
|
|
103
102
|
* .catch(error => console.error(error));
|
|
104
103
|
*/
|
|
@@ -112,20 +111,26 @@ export async function fetchInstructorLessons(
|
|
|
112
111
|
limit = 20,
|
|
113
112
|
includedFields = [],
|
|
114
113
|
}: FetchInstructorLessonsOptions = {}
|
|
115
|
-
): Promise<InstructorLessonsResponse> {
|
|
114
|
+
): Promise<InstructorLessonsResponse | null> {
|
|
116
115
|
const fieldsString = getFieldsForContentType() as string
|
|
117
116
|
const start = (page - 1) * limit
|
|
118
117
|
const end = start + limit
|
|
119
118
|
const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
|
|
120
119
|
const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
|
|
121
|
-
const filter = `brand == '${brand}' ${searchFilter} ${includedFieldsFilter} && references(*[_type=='instructor' && slug.current == '${slug}']._id)`
|
|
122
|
-
const filterWithRestrictions = await new FilterBuilder(filter).buildFilter()
|
|
123
120
|
|
|
124
121
|
sortOrder = getSortOrder(sortOrder, brand)
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
const now = getSanityDate(new Date())
|
|
123
|
+
const query = `{
|
|
124
|
+
"data":
|
|
125
|
+
*[_type == 'instructor' && slug.current == '${slug}']
|
|
126
|
+
{
|
|
127
|
+
'type': _type,
|
|
128
|
+
name,
|
|
129
|
+
'thumbnail': thumbnail_url.asset->url,
|
|
130
|
+
'lessons_count': count(*[brand == '${brand}' && references(^._id)]),
|
|
131
|
+
'lessons': *[brand == '${brand}' && references(^._id) && (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}')) ${searchFilter} ${includedFieldsFilter}]{${fieldsString}} [${start}...${end}]
|
|
132
|
+
}
|
|
133
|
+
|order(${sortOrder})
|
|
134
|
+
}`
|
|
135
|
+
return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
|
|
131
136
|
}
|
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)
|
|
@@ -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)
|
|
@@ -63,3 +63,22 @@ export async function updateForumCategory(
|
|
|
63
63
|
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
64
64
|
return httpClient.put<ForumCategory>(`${baseUrl}/v1/categories/${params.id}`, params)
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
export interface DeleteForumCategoryParams {
|
|
68
|
+
id: number
|
|
69
|
+
brand: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deletes a forum category.
|
|
74
|
+
*
|
|
75
|
+
* @param {DeleteForumCategoryParams} params - The parameters for deleting the forum category.
|
|
76
|
+
* @returns {Promise<void>} - A promise that resolves when the category is deleted.
|
|
77
|
+
* @throws {HttpError} - If the request fails.
|
|
78
|
+
*/
|
|
79
|
+
export async function deleteForumCategory(
|
|
80
|
+
params: DeleteForumCategoryParams
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
83
|
+
return httpClient.delete<void>(`${baseUrl}/v1/categories/${params.id}?brand=${params.brand}`)
|
|
84
|
+
}
|
|
@@ -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
|
+
}
|