musora-content-services 2.79.1 → 2.80.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/docs/Content.html +269 -0
  3. package/docs/ContentOrganization.html +2 -2
  4. package/docs/Forums.html +2 -2
  5. package/docs/Gamification.html +2 -2
  6. package/docs/TestUser.html +2 -2
  7. package/docs/UserManagementSystem.html +2 -2
  8. package/docs/api_types.js.html +2 -2
  9. package/docs/config.js.html +2 -2
  10. package/docs/content-org_content-org.js.html +2 -2
  11. package/docs/content-org_guided-courses.ts.html +2 -2
  12. package/docs/content-org_learning-paths.ts.html +21 -14
  13. package/docs/content-org_playlists-types.js.html +2 -2
  14. package/docs/content-org_playlists.js.html +2 -2
  15. package/docs/content.js.html +2 -2
  16. package/docs/content_artist.ts.html +212 -0
  17. package/docs/content_content.ts.html +77 -0
  18. package/docs/content_genre.ts.html +211 -0
  19. package/docs/content_instructor.ts.html +203 -0
  20. package/docs/forums_categories.ts.html +3 -3
  21. package/docs/forums_forums.ts.html +2 -2
  22. package/docs/forums_posts.ts.html +2 -2
  23. package/docs/forums_threads.ts.html +2 -2
  24. package/docs/gamification_awards.ts.html +2 -2
  25. package/docs/gamification_gamification.js.html +2 -2
  26. package/docs/global.html +2 -2
  27. package/docs/index.html +2 -2
  28. package/docs/liveTesting.ts.html +2 -2
  29. package/docs/module-Accounts.html +2 -2
  30. package/docs/module-Artist.html +991 -0
  31. package/docs/module-Awards.html +2 -2
  32. package/docs/module-Config.html +2 -2
  33. package/docs/module-Content-Services-V2.html +2 -2
  34. package/docs/module-Forums.html +2 -2
  35. package/docs/module-Genre.html +981 -0
  36. package/docs/module-GuidedCourses.html +2 -2
  37. package/docs/module-Instructor.html +929 -0
  38. package/docs/module-Interests.html +2 -2
  39. package/docs/module-LearningPaths.html +6 -6
  40. package/docs/module-Onboarding.html +2 -2
  41. package/docs/module-Payments.html +2 -2
  42. package/docs/module-Permissions.html +2 -2
  43. package/docs/module-Playlists.html +2 -2
  44. package/docs/module-ProgressRow.html +2 -2
  45. package/docs/module-Railcontent-Services.html +2 -2
  46. package/docs/module-Sanity-Services.html +326 -1854
  47. package/docs/module-Sessions.html +2 -2
  48. package/docs/module-UserActivity.html +2 -2
  49. package/docs/module-UserChat.html +2 -2
  50. package/docs/module-UserManagement.html +2 -2
  51. package/docs/module-UserMemberships.html +2 -2
  52. package/docs/module-UserNotifications.html +2 -2
  53. package/docs/module-UserProfile.html +2 -2
  54. package/docs/progress-row_method-card.js.html +2 -2
  55. package/docs/railcontent.js.html +2 -2
  56. package/docs/sanity.js.html +107 -268
  57. package/docs/userActivity.js.html +2 -2
  58. package/docs/user_account.ts.html +2 -2
  59. package/docs/user_chat.js.html +2 -2
  60. package/docs/user_interests.js.html +2 -2
  61. package/docs/user_management.js.html +2 -2
  62. package/docs/user_memberships.ts.html +2 -2
  63. package/docs/user_notifications.js.html +2 -2
  64. package/docs/user_onboarding.ts.html +2 -2
  65. package/docs/user_payments.ts.html +2 -2
  66. package/docs/user_permissions.js.html +2 -2
  67. package/docs/user_profile.js.html +2 -2
  68. package/docs/user_sessions.js.html +2 -2
  69. package/docs/user_types.js.html +2 -2
  70. package/docs/user_user-management-system.js.html +2 -2
  71. package/jsdoc.json +1 -0
  72. package/package.json +1 -1
  73. package/src/contentTypeConfig.js +2 -2
  74. package/src/index.d.ts +28 -5
  75. package/src/index.js +28 -5
  76. package/src/services/content/artist.ts +139 -0
  77. package/src/services/content/content.ts +38 -0
  78. package/src/services/content/genre.ts +139 -0
  79. package/src/services/content/instructor.ts +131 -0
  80. package/src/services/sanity.js +105 -266
  81. package/.claude/settings.local.json +0 -8
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @module Artist
3
+ */
4
+ import { DEFAULT_FIELDS, filtersToGroq } from '../../contentTypeConfig.js'
5
+ import { FilterBuilder } from '../../filterBuilder.js'
6
+ import { fetchSanity, getSanityDate, getSortOrder } from '../sanity.js'
7
+ import { Lesson } from './content'
8
+
9
+ export interface Artist {
10
+ slug: string
11
+ name: string
12
+ lessons?: Lesson[]
13
+ lessonsCount: number
14
+ }
15
+
16
+ /**
17
+ * Fetch all artists with lessons available for a specific brand.
18
+ *
19
+ * @param {string} brand - The brand for which to fetch artists.
20
+ * @returns {Promise<Artist[]|null>} - A promise that resolves to an array of artist objects or null if not found.
21
+ *
22
+ * @example
23
+ * fetchArtists('drumeo')
24
+ * .then(artists => console.log(artists))
25
+ * .catch(error => console.error(error));
26
+ */
27
+ export async function fetchArtists(brand: string): Promise<Artist[] | null> {
28
+ const filter = await new FilterBuilder(
29
+ `_type == "song" && brand == "${brand}" && references(^._id)`,
30
+ { bypassPermissions: true }
31
+ ).buildFilter()
32
+ const query = `
33
+ *[_type == "artist"]{
34
+ name,
35
+ "slug": slug.current,
36
+ "lessonsCount": count(*[${filter}])
37
+ }[lessonsCount > 0] |order(lower(name)) `
38
+ return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
39
+ }
40
+
41
+ /**
42
+ * Fetch a single artist by their Sanity ID.
43
+ *
44
+ * @param {string} slug - The name of the artist to fetch.
45
+ * @param {string} [brand] - The brand for which to fetch the artist.
46
+ * @returns {Promise<Artist|null>} - A promise that resolves to an artist objects or null if not found.
47
+ *
48
+ * @example
49
+ * fetchArtists('drumeo')
50
+ * .then(artists => console.log(artists))
51
+ * .catch(error => console.error(error));
52
+ */
53
+ export async function fetchArtistBySlug(slug: string, brand?: string): Promise<Artist | null> {
54
+ const brandFilter = brand ? `brand == "${brand}" && ` : ''
55
+ const filter = await new FilterBuilder(`${brandFilter} _type == "song" && references(^._id)`, {
56
+ bypassPermissions: true,
57
+ }).buildFilter()
58
+ const query = `
59
+ *[_type == "artist" && slug.current == '${slug}']{
60
+ name,
61
+ "slug": slug.current,
62
+ "lessonsCount": count(*[${filter}])
63
+ }[lessonsCount > 0] |order(lower(name)) `
64
+ return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
65
+ }
66
+
67
+ export interface ArtistLessonOptions {
68
+ sort?: string
69
+ searchTerm?: string
70
+ page?: number
71
+ limit?: number
72
+ includedFields?: Array<string>
73
+ progressIds?: Array<number>
74
+ }
75
+
76
+ export interface LessonsByArtistResponse {
77
+ entity: Artist[]
78
+ }
79
+
80
+ /**
81
+ * Fetch the artist's lessons.
82
+ * @param {string} brand - The brand for which to fetch lessons.
83
+ * @param {string} slug - The slug of the artist
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
+ * @param {Object} params - Parameters for sorting, searching, pagination and filtering.
86
+ * @param {string} [params.sort="-published_on"] - The field to sort the lessons by.
87
+ * @param {string} [params.searchTerm=""] - The search term to filter the lessons.
88
+ * @param {number} [params.page=1] - The page number for pagination.
89
+ * @param {number} [params.limit=10] - The number of items per page.
90
+ * @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'].
91
+ * @param {Array<number>} [params.progressIds] - The ids of the lessons that are in progress or completed
92
+ * @returns {Promise<LessonsByArtistResponse|null>} - The lessons for the artist and some details about the artist (name and thumbnail).
93
+ *
94
+ * @example
95
+ * fetchArtistLessons('drumeo', '10 Years', 'song', {'-published_on', '', 1, 10, ["difficulty,Intermediate"], [232168, 232824, 303375, 232194, 393125]})
96
+ * .then(lessons => console.log(lessons))
97
+ * .catch(error => console.error(error));
98
+ */
99
+ export async function fetchArtistLessons(
100
+ brand: string,
101
+ slug: string,
102
+ contentType: string,
103
+ {
104
+ sort = '-published_on',
105
+ searchTerm = '',
106
+ page = 1,
107
+ limit = 10,
108
+ includedFields = [],
109
+ progressIds = undefined,
110
+ }: ArtistLessonOptions = {}
111
+ ): Promise<LessonsByArtistResponse | null> {
112
+ const fieldsString = DEFAULT_FIELDS.join(',')
113
+ const start = (page - 1) * limit
114
+ const end = start + limit
115
+ const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
116
+ const sortOrder = getSortOrder(sort, brand)
117
+ const addType =
118
+ contentType && Array.isArray(contentType)
119
+ ? `_type in ['${contentType.join("', '")}'] &&`
120
+ : contentType
121
+ ? `_type == '${contentType}' && `
122
+ : ''
123
+ const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
124
+
125
+ // limits the results to supplied progressIds for started & completed filters
126
+ const progressFilter =
127
+ progressIds !== undefined ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
128
+ const now = getSanityDate(new Date())
129
+ const query = `{
130
+ "entity":
131
+ *[_type == 'artist' && slug.current == '${slug}']
132
+ {'type': _type, name, 'thumbnail':thumbnail_url.asset->url,
133
+ 'lessons_count': count(*[${addType} brand == '${brand}' && references(^._id)]),
134
+ 'lessons': *[${addType} brand == '${brand}' && references(^._id) && (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}')) ${searchFilter} ${includedFieldsFilter} ${progressFilter}]{${fieldsString}}
135
+ [${start}...${end}]}
136
+ |order(${sortOrder})
137
+ }`
138
+ return fetchSanity(query, true)
139
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @namespace Content
3
+ * @property {module:Instructor} Instructor
4
+ * @property {module:Genre} Genre
5
+ * @property {module:Artist} Artist
6
+ */
7
+
8
+ export type LessonType = 'workout' | 'challenge-part' | 'song' | string
9
+ export type LessonPageType = 'lesson' | 'song' | string
10
+
11
+ export interface ArtistObject {
12
+ name: string
13
+ thumbnail: string | null
14
+ }
15
+
16
+ export interface Lesson {
17
+ artist: ArtistObject | string | null
18
+ artist_name: string
19
+ brand: string
20
+ child_count: number | null
21
+ difficulty: number | null
22
+ difficulty_string: string | null
23
+ genre: string[] | string | null
24
+ id: number
25
+ image: string
26
+ length_in_seconds: number
27
+ parent_id: number | null
28
+ permission_id: number[]
29
+ published_on: string
30
+ sanity_id: string
31
+ slug: string
32
+ status: string
33
+ thumbnail: string
34
+ title: string
35
+ type: LessonType
36
+ need_access: boolean
37
+ page_type: LessonPageType
38
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @module Genre
3
+ */
4
+ import { DEFAULT_FIELDS, filtersToGroq } from '../../contentTypeConfig.js'
5
+ import { fetchSanity, getSanityDate, getSortOrder } from '../sanity.js'
6
+ import { FilterBuilder } from '../../filterBuilder.js'
7
+ import { Lesson } from './content'
8
+
9
+ export interface Genre {
10
+ lessons?: Lesson[]
11
+ lessons_count: number
12
+ name: string
13
+ slug: string
14
+ thumbnail: string
15
+ type: 'genre'
16
+ }
17
+
18
+ /**
19
+ * Fetch all genres with lessons available for a specific brand.
20
+ *
21
+ * @param {string} [brand] - The brand for which to fetch the genre for. Lesson count will be filtered by this brand if provided.
22
+ * @returns {Promise<Genre[]>} - A promise that resolves to an genre object or null if not found.
23
+ *
24
+ * @example
25
+ * fetchGenres('drumeo')
26
+ * .then(genres => console.log(genres))
27
+ * .catch(error => console.error(error));
28
+ */
29
+ export async function fetchGenres(brand: string): Promise<Genre[]> {
30
+ const filter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
31
+ bypassPermissions: true,
32
+ }).buildFilter()
33
+
34
+ const query = `
35
+ *[_type == 'genre'] {
36
+ 'type': _type,
37
+ name,
38
+ "slug": slug.current,
39
+ 'thumbnail': thumbnail_url.asset->url,
40
+ "lessons_count": count(*[${filter}])
41
+ } |order(lower(name)) `
42
+ return fetchSanity(query, true, { processNeedAccess: false, processPageType: false})
43
+ }
44
+
45
+ /**
46
+ * Fetch a single genre by their slug and brand
47
+ *
48
+ * @param {string} slug - The slug of the genre to fetch.
49
+ * @param {string} [brand] - The brand for which to fetch the genre. Lesson count will be filtered by this brand if provided.
50
+ * @returns {Promise<Genre[]|null>} - A promise that resolves to an genre object or null if not found.
51
+ *
52
+ * @example
53
+ * fetchGenreBySlug('drumeo')
54
+ * .then(genres => console.log(genres))
55
+ * .catch(error => console.error(error));
56
+ */
57
+ export async function fetchGenreBySlug(slug: string, brand?: string): Promise<Genre | null> {
58
+ const brandFilter = brand ? `brand == "${brand}" && ` : ''
59
+ const filter = await new FilterBuilder(`${brandFilter} references(^._id)`, {
60
+ bypassPermissions: true,
61
+ }).buildFilter()
62
+
63
+ const query = `
64
+ *[_type == 'genre' && slug.current == '${slug}'] {
65
+ 'type': _type, name,
66
+ name,
67
+ "slug": slug.current,
68
+ 'thumbnail':thumbnail_url.asset->url,
69
+ "lessonsCount": count(*[${filter}])
70
+ }`
71
+ return fetchSanity(query, true, { processNeedAccess: false, processPageType: false})
72
+ }
73
+
74
+ export interface FetchGenreLessonsOptions {
75
+ sort?: string
76
+ searchTerm?: string
77
+ page?: number
78
+ limit?: number
79
+ includedFields?: Array<string>
80
+ progressIds?: Array<number>
81
+ }
82
+
83
+ export interface LessonsByGenreResponse {
84
+ entity: Genre[]
85
+ }
86
+
87
+ /**
88
+ * Fetch the genre's lessons.
89
+ * @param {string} brand - The brand for which to fetch lessons.
90
+ * @param {string} slug - The slug of the genre
91
+ * @param {Object} params - Parameters for sorting, searching, pagination and filtering.
92
+ * @param {string} [params.sort="-published_on"] - The field to sort the lessons by.
93
+ * @param {string} [params.searchTerm=""] - The search term to filter the lessons.
94
+ * @param {number} [params.page=1] - The page number for pagination.
95
+ * @param {number} [params.limit=10] - The number of items per page.
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
+ * @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).
99
+ *
100
+ * @example
101
+ * fetchGenreLessons('drumeo', 'Blues', 'song', {'-published_on', '', 1, 10, ["difficulty,Intermediate"], [232168, 232824, 303375, 232194, 393125]})
102
+ * .then(lessons => console.log(lessons))
103
+ * .catch(error => console.error(error));
104
+ */
105
+ export async function fetchGenreLessons(
106
+ brand: string,
107
+ slug: string,
108
+ contentType: string,
109
+ {
110
+ sort = '-published_on',
111
+ searchTerm = '',
112
+ page = 1,
113
+ limit = 10,
114
+ includedFields = [],
115
+ progressIds = [],
116
+ }: FetchGenreLessonsOptions = {}
117
+ ): Promise<LessonsByGenreResponse> {
118
+ const fieldsString = DEFAULT_FIELDS.join(',')
119
+ const start = (page - 1) * limit
120
+ const end = start + limit
121
+ const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
122
+ const sortOrder = getSortOrder(sort, brand)
123
+ const addType = contentType ? `_type == '${contentType}' && ` : ''
124
+ const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
125
+ // limits the results to supplied progressIds for started & completed filters
126
+ const progressFilter =
127
+ progressIds !== undefined ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
128
+ const now = getSanityDate(new Date())
129
+ const query = `{
130
+ "entity":
131
+ *[_type == 'genre' && slug.current == '${slug}']
132
+ {'type': _type, name, 'thumbnail':thumbnail_url.asset->url,
133
+ 'lessons_count': count(*[${addType} brand == '${brand}' && references(^._id)]),
134
+ 'lessons': *[${addType} brand == '${brand}' && references(^._id) && (status in ['published'] || (status == 'scheduled' && defined(published_on) && published_on >= '${now}')) ${searchFilter} ${includedFieldsFilter} ${progressFilter}]{${fieldsString}}
135
+ [${start}...${end}]}
136
+ |order(${sortOrder})
137
+ }`
138
+ return fetchSanity(query, true)
139
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @module Instructor
3
+ */
4
+ import { FilterBuilder } from '../../filterBuilder.js'
5
+ import { filtersToGroq, getFieldsForContentType } from '../../contentTypeConfig.js'
6
+ import { buildEntityAndTotalQuery, fetchSanity, getSortOrder } from '../sanity.js'
7
+ import { Lesson } from './content'
8
+
9
+ export interface Instructor {
10
+ lessonCount: number
11
+ slug: string
12
+ name: string
13
+ short_bio: string
14
+ thumbnail: string
15
+ }
16
+
17
+ /**
18
+ * Fetch all instructor with lessons available for a specific brand.
19
+ *
20
+ * @param {string} brand - The brand for which to fetch instructors.
21
+ * @returns {Promise<Instructor[]>} - A promise that resolves to an array of instructor objects.
22
+ *
23
+ * @example
24
+ * fetchInstructors('drumeo')
25
+ * .then(instructors => console.log(instructors))
26
+ * .catch(error => console.error(error));
27
+ */
28
+ export async function fetchInstructors(brand: string): Promise<Instructor[]> {
29
+ const filter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
30
+ bypassPermissions: true,
31
+ }).buildFilter()
32
+
33
+ const query = `
34
+ *[_type == "instructor"] {
35
+ name,
36
+ "slug": slug.current,
37
+ "lessonsCount": count(*[${filter}])
38
+ }[lessonsCount > 0] |order(lower(name)) `
39
+ return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
40
+ }
41
+
42
+ /**
43
+ * Fetch a single instructor by their name
44
+ *
45
+ * @param {string} slug - The slug of the instructor to fetch.
46
+ * @param {string} [brand] - The brand for which to fetch the instructor. Lesson count will be filtered by this brand if provided.
47
+ * @returns {Promise<Instructor[]>} - A promise that resolves to an instructor object or null if not found.
48
+ *
49
+ * @example
50
+ * fetchInstructorBySlug('66samus', 'drumeo')
51
+ * .then(instructor => console.log(instructor))
52
+ * .catch(error => console.error(error));
53
+ */
54
+ export async function fetchInstructorBySlug(
55
+ slug: string,
56
+ brand?: string
57
+ ): Promise<Instructor | null> {
58
+ const brandFilter = brand ? `brand == "${brand}" && ` : ''
59
+ const filter = await new FilterBuilder(`${brandFilter} references(^._id)`, {
60
+ bypassPermissions: true,
61
+ }).buildFilter()
62
+
63
+ const query = `
64
+ *[_type == "instructor" && slug.current == '${slug}'][0] {
65
+ name,
66
+ "slug": slug.current,
67
+ short_bio,
68
+ 'thumbnail': thumbnail_url.asset->url,
69
+ "lessonsCount": count(*[${filter}])
70
+ }`
71
+ return fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
72
+ }
73
+
74
+ export interface FetchInstructorLessonsOptions {
75
+ sortOrder?: string
76
+ searchTerm?: string
77
+ page?: number
78
+ limit?: number
79
+ includedFields?: Array<string>
80
+ }
81
+
82
+ export interface InstructorLessonsResponse {
83
+ entity: Lesson[]
84
+ total: number
85
+ }
86
+
87
+ /**
88
+ * Fetch the data needed for the instructor screen.
89
+ * @param {string} brand - The brand for which to fetch instructor lessons
90
+ * @param {string} slug - The slug of the instructor
91
+ *
92
+ * @param {FetchInstructorLessonsOptions} options - Parameters for pagination, filtering and sorting.
93
+ * @param {string} [options.sortOrder="-published_on"] - The field to sort the lessons by.
94
+ * @param {string} [options.searchTerm=""] - The search term to filter content by title.
95
+ * @param {number} [options.page=1] - The page number for pagination.
96
+ * @param {number} [options.limit=10] - The number of items per page.
97
+ * @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
+ *
99
+ * @returns {Promise<InstructorLessonsResponse>} - The lessons for the instructor or null if not found.
100
+ * @example
101
+ * fetchInstructorLessons('instructor123')
102
+ * .then(lessons => console.log(lessons))
103
+ * .catch(error => console.error(error));
104
+ */
105
+ export async function fetchInstructorLessons(
106
+ slug: string,
107
+ brand: string,
108
+ {
109
+ sortOrder = '-published_on',
110
+ searchTerm = '',
111
+ page = 1,
112
+ limit = 20,
113
+ includedFields = [],
114
+ }: FetchInstructorLessonsOptions = {}
115
+ ): Promise<InstructorLessonsResponse> {
116
+ const fieldsString = getFieldsForContentType() as string
117
+ const start = (page - 1) * limit
118
+ const end = start + limit
119
+ const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
120
+ 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
+
124
+ sortOrder = getSortOrder(sortOrder, brand)
125
+ const query = buildEntityAndTotalQuery(filterWithRestrictions, fieldsString, {
126
+ sortOrder: sortOrder,
127
+ start: start,
128
+ end: end,
129
+ })
130
+ return fetchSanity(query, true)
131
+ }