musora-content-services 1.0.127 → 1.0.129

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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.0.129](https://github.com/railroadmedia/musora-content-services/compare/v1.0.127...v1.0.129) (2024-10-09)
6
+
7
+ ### [1.0.128](https://github.com/railroadmedia/musora-content-services/compare/v1.0.127...v1.0.128) (2024-10-03)
8
+
5
9
  ### [1.0.127](https://github.com/railroadmedia/musora-content-services/compare/v1.0.123...v1.0.127) (2024-10-03)
6
10
 
7
11
  ### [1.0.126](https://github.com/railroadmedia/musora-content-services/compare/v1.0.124...v1.0.126) (2024-10-02)
package/README.md CHANGED
@@ -44,6 +44,15 @@ To install the package, use npm:
44
44
  npm install musora-content-services
45
45
  ```
46
46
 
47
+ ## Generating index.js and index.d.ts
48
+
49
+ The `index.js` and `index.d.ts` files provide all exported functions from this package, and are generated automatically.
50
+ Simply run `npm run build-index` to build these files. It works by running the `tools/generate-index.js` file, which simply
51
+ parses the files under the `src/services` directory and builds up the index files with any functions tagged with `export`.
52
+
53
+ If you want to exclude any of your exported functions from the generated index files, be sure to add the function name to
54
+ the `excludeFromGeneratedIndex` array inside the service file.
55
+
47
56
  ## Publishing Package Updates
48
57
 
49
58
  To publish a new version to NPM run,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "1.0.127",
3
+ "version": "1.0.129",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1066,6 +1066,7 @@ const contentMetadata = {
1066
1066
  }
1067
1067
  };
1068
1068
 
1069
+ const typeWithSortOrder = ['in-rhythm', 'diy-drum-experiments', 'rhythmic-adventures-of-captain-carson'];
1069
1070
  function processMetadata(brand, type, withFilters = false)
1070
1071
  {
1071
1072
  let brandMetaData = contentMetadata[brand]?.[type];
@@ -1099,4 +1100,5 @@ function processMetadata(brand, type, withFilters = false)
1099
1100
 
1100
1101
  module.exports = {
1101
1102
  processMetadata,
1103
+ typeWithSortOrder
1102
1104
  }
@@ -94,13 +94,26 @@ let contentTypeConfig = {
94
94
  'total_xp',
95
95
  'xp',
96
96
  '"instructors": instructor[]->name',
97
+ '"instructor_signature": instructor[0]->signature.asset->url',
97
98
  '"header_image_url": thumbnail.asset->url',
98
99
  '"logo_image_url": logo_image_url.asset->url',
99
100
  '"award": award.asset->url',
100
- `"award_template": award_template[]{
101
- streak_minimum,
102
- "template" : template.asset->url,
103
- }`,
101
+ 'award_custom_text',
102
+ '"gold_award": gold_award.asset->url',
103
+ '"silver_award": silver_award.asset->url',
104
+ '"bronze_award": bronze_award.asset->url',
105
+ `"lessons": child[]->{
106
+ "id": railcontent_id,
107
+ title,
108
+ "image": thumbnail.asset->url,
109
+ "instructors": instructor[]->name,
110
+ length_in_seconds,
111
+ difficulty_string,
112
+ difficulty,
113
+ "type": _type,
114
+ is_always_unlocked_for_challenge,
115
+ is_bonus_content_for_challenge,
116
+ }`,
104
117
  ]
105
118
  },
106
119
  'course': {
package/src/index.d.ts CHANGED
@@ -13,15 +13,20 @@ import {
13
13
 
14
14
  import {
15
15
  fetchAllCompletedStates,
16
+ fetchChallengeMetadata,
16
17
  fetchCompletedContent,
17
18
  fetchCompletedState,
18
19
  fetchContentInProgress,
19
20
  fetchContentPageUserData,
20
21
  fetchHandler,
21
22
  fetchSongsInProgress,
22
- fetchUserLikes,
23
+ fetchUserAward,
24
+ fetchUserChallengeProgress,
23
25
  fetchUserPermissions,
24
- fetchVimeoData
26
+ postChallengesEnroll,
27
+ postChallengesLeave,
28
+ postChallengesSetStartDate,
29
+ postChallengesUnlock
25
30
  } from './services/railcontent.js';
26
31
 
27
32
  import {
@@ -83,6 +88,7 @@ declare module 'musora-content-services' {
83
88
  fetchByRailContentIds,
84
89
  fetchByReference,
85
90
  fetchCatalogMetadata,
91
+ fetchChallengeMetadata,
86
92
  fetchChallengeOverview,
87
93
  fetchChildren,
88
94
  fetchCoachLessons,
@@ -120,15 +126,19 @@ declare module 'musora-content-services' {
120
126
  fetchSongFilterOptions,
121
127
  fetchSongsInProgress,
122
128
  fetchUpcomingEvents,
123
- fetchUserLikes,
129
+ fetchUserAward,
130
+ fetchUserChallengeProgress,
124
131
  fetchUserPermissions,
125
- fetchVimeoData,
126
132
  fetchWorkouts,
127
133
  getSortOrder,
128
134
  globalConfig,
129
135
  initializeService,
130
136
  isContentLiked,
131
137
  likeContent,
138
+ postChallengesEnroll,
139
+ postChallengesLeave,
140
+ postChallengesSetStartDate,
141
+ postChallengesUnlock,
132
142
  unlikeContent,
133
143
  }
134
144
  }
package/src/index.js CHANGED
@@ -13,15 +13,20 @@ import {
13
13
 
14
14
  import {
15
15
  fetchAllCompletedStates,
16
+ fetchChallengeMetadata,
16
17
  fetchCompletedContent,
17
18
  fetchCompletedState,
18
19
  fetchContentInProgress,
19
20
  fetchContentPageUserData,
20
21
  fetchHandler,
21
22
  fetchSongsInProgress,
22
- fetchUserLikes,
23
+ fetchUserAward,
24
+ fetchUserChallengeProgress,
23
25
  fetchUserPermissions,
24
- fetchVimeoData
26
+ postChallengesEnroll,
27
+ postChallengesLeave,
28
+ postChallengesSetStartDate,
29
+ postChallengesUnlock
25
30
  } from './services/railcontent.js';
26
31
 
27
32
  import {
@@ -82,6 +87,7 @@ export {
82
87
  fetchByRailContentIds,
83
88
  fetchByReference,
84
89
  fetchCatalogMetadata,
90
+ fetchChallengeMetadata,
85
91
  fetchChallengeOverview,
86
92
  fetchChildren,
87
93
  fetchCoachLessons,
@@ -119,14 +125,18 @@ export {
119
125
  fetchSongFilterOptions,
120
126
  fetchSongsInProgress,
121
127
  fetchUpcomingEvents,
122
- fetchUserLikes,
128
+ fetchUserAward,
129
+ fetchUserChallengeProgress,
123
130
  fetchUserPermissions,
124
- fetchVimeoData,
125
131
  fetchWorkouts,
126
132
  getSortOrder,
127
133
  globalConfig,
128
134
  initializeService,
129
135
  isContentLiked,
130
136
  likeContent,
137
+ postChallengesEnroll,
138
+ postChallengesLeave,
139
+ postChallengesSetStartDate,
140
+ postChallengesUnlock,
131
141
  unlikeContent,
132
142
  };
@@ -7,7 +7,14 @@ let globalConfig = {
7
7
  railcontentConfig: {},
8
8
  localStorage: null
9
9
  };
10
-
10
+
11
+ /**
12
+ * Exported functions that are excluded from index generation.
13
+ *
14
+ * @type {string[]}
15
+ */
16
+ const excludeFromGeneratedIndex = [];
17
+
11
18
  /**
12
19
  * Initializes the service with the given configuration.
13
20
  * This function must be called before using any other functions in this library.
@@ -1,6 +1,13 @@
1
1
  import {fetchUserLikes, postContentLiked, postContentUnliked} from "./railcontent";
2
2
  import {DataContext, ContentVersionKey} from "./dataContext";
3
3
 
4
+ /**
5
+ * Exported functions that are excluded from index generation.
6
+ *
7
+ * @type {string[]}
8
+ */
9
+ const excludeFromGeneratedIndex = [];
10
+
4
11
  export let dataContext = new DataContext(ContentVersionKey, fetchUserLikes);
5
12
 
6
13
  export async function isContentLiked(contentId) {
@@ -1,5 +1,12 @@
1
1
  import {globalConfig} from "./config";
2
2
 
3
+ /**
4
+ * Exported functions that are excluded from index generation.
5
+ *
6
+ * @type {string[]}
7
+ */
8
+ const excludeFromGeneratedIndex = [];
9
+
3
10
  //These constants need to match MWP UserDataVersionKeyEnum enum
4
11
  export const ContentVersionKey = 0;
5
12
 
@@ -4,6 +4,13 @@
4
4
 
5
5
  const {globalConfig} = require('./config');
6
6
 
7
+ /**
8
+ * Exported functions that are excluded from index generation.
9
+ *
10
+ * @type {string[]}
11
+ */
12
+ const excludeFromGeneratedIndex = ['fetchUserLikes', 'postContentLiked', 'postContentUnliked'];
13
+
7
14
 
8
15
  /**
9
16
  * Fetches the completion status of a specific lesson for the current user.
@@ -38,40 +45,6 @@ export async function fetchCompletedState(content_id) {
38
45
  }
39
46
  }
40
47
 
41
- /**
42
- * Fetches the vimeo meta-data
43
- *
44
- * @param {string} vimeo_id - The vimeo id, found in the <document>.video.external_id field for lessons
45
- * @returns {Promise<Object|null>} - Returns the
46
- * @example
47
- * fetchVimeoData('642900215')
48
- * .then(vimeoData => console.log(vimeoData))
49
- * .catch(error => console.error(error));
50
- */
51
- export async function fetchVimeoData(vimeo_id) {
52
- const url = `/content/vimeo-data/${vimeo_id}`;
53
-
54
- const headers = {
55
- 'Content-Type': 'application/json',
56
- 'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
57
- };
58
-
59
- try {
60
- const response = await fetchAbsolute(url, {headers});
61
- const result = await response.json();
62
-
63
- if (result) {
64
- return result; // Return the correct object
65
- } else {
66
- console.log('Invalid result structure', result);
67
- return null; // Handle unexpected structure
68
- }
69
- } catch (error) {
70
- console.error('Fetch error:', error);
71
- return null;
72
- }
73
- }
74
-
75
48
 
76
49
  /**
77
50
  * Fetches the completion status for multiple songs for the current user.
@@ -257,10 +230,6 @@ export async function fetchContentPageUserData(contentId) {
257
230
 
258
231
  export async function fetchUserPermissions() {
259
232
  let url = `/content/user_data_permissions`;
260
- const headers = {
261
- 'Content-Type': 'application/json',
262
- 'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
263
- };
264
233
  // in the case of an unauthorized user, we return empty permissions
265
234
  return fetchHandler(url, 'get') ?? [];
266
235
  }
@@ -304,6 +273,41 @@ export async function postContentUnliked(contentId) {
304
273
  return await fetchHandler(url, "post");
305
274
  }
306
275
 
276
+ export async function fetchChallengeMetadata(contentId) {
277
+ let url = `/challenges/${contentId}`;
278
+ return await fetchHandler(url, 'get');
279
+ }
280
+
281
+ export async function fetchUserChallengeProgress(contentId) {
282
+ let url = `/challenges/user_data/${contentId}`;
283
+ return await fetchHandler(url, 'get');
284
+ }
285
+
286
+ export async function fetchUserAward(contentId) {
287
+ let url = `/challenges/download_award/${contentId}`;
288
+ return await fetchHandler(url, 'get');
289
+ }
290
+
291
+ export async function postChallengesSetStartDate(contentId, startDate) {
292
+ let url = `/challenges/set_start_date/${contentId}?start_date=${startDate}`;
293
+ return await fetchHandler(url, 'post');
294
+ }
295
+
296
+ export async function postChallengesUnlock(contentId) {
297
+ let url = `/challenges/unlock/${contentId}`;
298
+ return await fetchHandler(url, 'post');
299
+ }
300
+
301
+ export async function postChallengesEnroll(contentId) {
302
+ let url = `/challenges/enroll/${contentId}`;
303
+ return await fetchHandler(url, 'post');
304
+ }
305
+
306
+ export async function postChallengesLeave(contentId) {
307
+ let url = `/challenges/leave/${contentId}`;
308
+ return await fetchHandler(url, 'post');
309
+ }
310
+
307
311
  function fetchAbsolute(url, params) {
308
312
  if (globalConfig.railcontentConfig.baseUrl) {
309
313
  if (url.startsWith('/')) {
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  import {
20
20
  processMetadata,
21
+ typeWithSortOrder
21
22
  } from "../contentMetaData";
22
23
 
23
24
  import {globalConfig} from "./config";
@@ -25,6 +26,12 @@ import {globalConfig} from "./config";
25
26
  import { fetchUserPermissions, fetchAllCompletedStates, fetchCurrentSongComplete } from './railcontent.js';
26
27
  import {arrayToStringRepresentation, FilterBuilder} from "../filterBuilder";
27
28
 
29
+ /**
30
+ * Exported functions that are excluded from index generation.
31
+ *
32
+ * @type {string[]}
33
+ */
34
+ const excludeFromGeneratedIndex = [];
28
35
  /**
29
36
  * Fetch a song by its document ID from Sanity.
30
37
  *
@@ -618,6 +625,7 @@ export async function fetchAllFilterOptions(
618
625
  progressIds = undefined,
619
626
  coachId = undefined, // New parameter for coach ID
620
627
  ) {
628
+ console.log('brand', brand)
621
629
  if (coachId && contentType !== 'coach-lessons') {
622
630
  throw new Error(`Invalid contentType: '${contentType}' for coachId. It must be 'coach-lessons'.`);
623
631
  }
@@ -959,19 +967,17 @@ export async function fetchLessonContent(railContentId) {
959
967
  * @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
960
968
  */
961
969
  export async function fetchRelatedLessons(railContentId, brand) {
962
- // let sort = 'published_on'
963
- // if (type == 'rhythmic-adventures-of-captain-carson' ||
964
- // type == 'diy-drum-experiments' ||
965
- // type == 'in-rhythm') {
966
- // sort = 'sort';
967
- // }
968
- //TODO: Implement $this->contentService->getFiltered
969
- const query = `*[railcontent_id == ${railContentId} && brand == "${brand}" && references(*[_type=='permission']._id)]{
970
+ const query = `*[railcontent_id == ${railContentId} && brand == "${brand}" && references(*[_type=='permission']._id)]{
971
+ _type, parent_type, railcontent_id,
970
972
  "related_lessons" : array::unique([
971
- ...(*[_type=="song" && brand == "${brand}" && references(^.artist->_id)]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,}[0...11]),
972
- ...(*[_type=="song" && brand == "${brand}" && references(^.genre[]->_id)]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,}[0...11])
973
- ])|order(published_on, railcontent_id)[0...11]}`;
974
- return fetchSanity(query, false);
973
+ ...(*[references(^._id)][0].child[]->{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type}),
974
+ ...(*[_type=="song" && _type==^._type && brand == "${brand}" && references(^.artist->_id) && railcontent_id !=${railContentId}]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type}|order(published_on desc, title asc)[0...11]),
975
+ ...(*[_type=="song" && _type==^._type && brand == "${brand}" && references(^.genre[]->_id) && railcontent_id !=${railContentId}]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type}|order(published_on desc, title asc)[0...11]),
976
+ ...(*[_type==^._type && _type in ${JSON.stringify(typeWithSortOrder)} && brand == "${brand}" && railcontent_id !=${railContentId}]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, sort}|order(sort asc, title asc)[0...11]),
977
+ ...(*[_type==^._type && !(_type in ${JSON.stringify(typeWithSortOrder)}) && brand == "${brand}" && railcontent_id !=${railContentId}]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type}|order(published_on desc, title asc)[0...11]),
978
+ ])[0...11]}`;
979
+
980
+ return fetchSanity(query, false);
975
981
  }
976
982
 
977
983
  /**
@@ -1105,18 +1111,6 @@ export async function fetchChallengeOverview(id) {
1105
1111
  // WIP
1106
1112
  const query = `*[railcontent_id == ${id}]{
1107
1113
  ${getFieldsForContentType("challenge")}
1108
- "lessons": child[]->{
1109
- "id": railcontent_id,
1110
- title,
1111
- "image": thumbnail.asset->url,
1112
- "instructors": instructor[]->name,
1113
- length_in_seconds,
1114
- difficulty_string,
1115
- difficulty,
1116
- "type": _type,
1117
- is_always_unlocked,
1118
- is_bonus_content,
1119
- }
1120
1114
  } [0...1]`;
1121
1115
  return fetchSanity(query, false);
1122
1116
  }
@@ -1382,9 +1376,8 @@ async function needsAccessDecorator(results)
1382
1376
 
1383
1377
  function doesUserNeedAccessToContent(result, userPermissions)
1384
1378
  {
1385
- const permissions = new Set(result.permission_id ?? []);
1386
-
1387
- if (permissions.size === 0) {
1379
+ const permissions = new Set(result?.permission_id ?? []);
1380
+ if (permissions.length === 0) {
1388
1381
  return false;
1389
1382
  }
1390
1383
  for (let permission of permissions) {
@@ -250,6 +250,44 @@ describe('Sanity Queries', function () {
250
250
  expect(isMatch).toBeTruthy();
251
251
  });
252
252
 
253
+ test('fetchRelatedLessons-quick-tips', async () => {
254
+ const id = 406213;
255
+ const response = await fetchRelatedLessons(id, 'singeo');
256
+ log(response);
257
+ const relatedLessons = response.related_lessons;
258
+ expect(Array.isArray(relatedLessons)).toBe(true);
259
+ relatedLessons.forEach(lesson => {
260
+ expect(lesson._type).toBe('quick-tips');
261
+ });
262
+ });
263
+
264
+ test('fetchRelatedLessons-in-rhythm', async () => {
265
+ const id = 236677;
266
+ const response = await fetchRelatedLessons(id, 'drumeo');
267
+ log(response);
268
+ const relatedLessons = response.related_lessons;
269
+ let episode = 0;
270
+ expect(Array.isArray(relatedLessons)).toBe(true);
271
+ relatedLessons.forEach(lesson => {
272
+ expect(lesson._type).toBe('in-rhythm');
273
+ expect(lesson.sort).toBeGreaterThan(episode);
274
+ episode = lesson.sort;
275
+ });
276
+ });
277
+
278
+ test('fetchRelatedLessons-child', async () => {
279
+ const id = 362278;
280
+ const course = await fetchByRailContentId(362277, 'course');
281
+ const lessonIds = course.lessons.map((doc) => doc.id);
282
+ const response = await fetchRelatedLessons(id, 'drumeo');
283
+ log(response.related_lessons);
284
+ const relatedLessons = response.related_lessons;
285
+ expect(Array.isArray(relatedLessons)).toBe(true);
286
+ expect(relatedLessons.some(
287
+ lesson => lessonIds.includes(lesson.id)
288
+ )).toBe(true);
289
+ },10000);
290
+
253
291
  test('fetchChildren', async () => {
254
292
  // complement test to fetchParentByRailContentId
255
293
  const id = 191338; ////https://web-staging-one.musora.com/admin/studio/publishing/structure/play-along;play-along_191338
@@ -463,9 +501,9 @@ describe('Sanity Queries', function () {
463
501
  const response = await fetchChallengeOverview(id);
464
502
  log(response);
465
503
  expect(response.award).toBeDefined();
466
- expect(response.award_template).toBeDefined();
467
- expect(response.lessons[0].is_always_unlocked).toBeDefined();
468
- expect(response.lessons[0].is_bonus_content).toBeDefined();
504
+ expect(response.award_custom_text).toBeDefined();
505
+ expect(response.lessons[0].is_always_unlocked_for_challenge).toBeDefined();
506
+ expect(response.lessons[0].is_bonus_content_for_challenge).toBeDefined();
469
507
  });
470
508
 
471
509
  test('fetchShowsData-OddTimes', async () => {
@@ -11,7 +11,7 @@ const fileExports = {};
11
11
  /**
12
12
  * Helper function to extract function names from ES module and CommonJS exports
13
13
  *
14
- * @param filePath
14
+ * @param {string} filePath
15
15
  * @returns {string[]}
16
16
  */
17
17
  function extractExportedFunctions(filePath) {
@@ -30,9 +30,29 @@ function extractExportedFunctions(filePath) {
30
30
  matches = matches.concat(exportsList);
31
31
  }
32
32
 
33
+ const excludedFunctions = getExclusionList(fileContent);
34
+ matches = matches.filter(fn => !excludedFunctions.includes(fn));
35
+
33
36
  return matches.sort();
34
37
  }
35
38
 
39
+ /**
40
+ * Helper function to find the list of exclusions from the file's exports
41
+ *
42
+ * @param {string} fileContent
43
+ * @returns {string[]}
44
+ */
45
+ function getExclusionList(fileContent) {
46
+ const excludeRegex = /const\s+excludeFromGeneratedIndex\s*=\s*\[(.*?)\];/;
47
+ const excludeMatch = fileContent.match(excludeRegex);
48
+ let excludedFunctions = [];
49
+ if (excludeMatch) {
50
+ excludedFunctions = excludeMatch[1]
51
+ .split(',')
52
+ .map(name => name.trim().replace(/['"`]/g, ''));
53
+ }
54
+ return excludedFunctions;
55
+ }
36
56
 
37
57
  // get all files in the services directory
38
58
  const servicesDir = path.join(__dirname, '../src/services');