musora-content-services 1.3.21 → 1.4.1

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.4.1](https://github.com/railroadmedia/musora-content-services/compare/v1.4.0...v1.4.1) (2025-03-25)
6
+
7
+ ## [1.4.0](https://github.com/railroadmedia/musora-content-services/compare/v1.3.21...v1.4.0) (2025-03-24)
8
+
5
9
  ### [1.3.21](https://github.com/railroadmedia/musora-content-services/compare/v1.3.20...v1.3.21) (2025-03-21)
6
10
 
7
11
  ### [1.3.20](https://github.com/railroadmedia/musora-content-services/compare/v1.3.19...v1.3.20) (2025-03-21)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "1.3.21",
3
+ "version": "1.4.1",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.d.ts CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  import {
4
4
  globalConfig,
5
- initializeService
5
+ initializeService,
6
+ setUserMetadata
6
7
  } from './services/config.js';
7
8
 
8
9
  import {
@@ -32,6 +33,7 @@ import {
32
33
  } from './services/dataContext.js';
33
34
 
34
35
  import {
36
+ clearLastUpdatedTime,
35
37
  setLastUpdatedTime,
36
38
  wasLastUpdateOlderThanXSeconds
37
39
  } from './services/lastUpdated.js';
@@ -135,6 +137,7 @@ import {
135
137
  fetchPlayAlongsCount,
136
138
  fetchRelatedLessons,
137
139
  fetchRelatedSongs,
140
+ fetchRelatedTutorials,
138
141
  fetchReturning,
139
142
  fetchSanity,
140
143
  fetchScheduledReleases,
@@ -148,8 +151,10 @@ import {
148
151
  } from './services/sanity.js';
149
152
 
150
153
  import {
154
+ clearPermissionsData,
151
155
  fetchUserPermissions,
152
- reset
156
+ reset,
157
+ updatePermissionsData
153
158
  } from './services/userPermissions.js';
154
159
 
155
160
  declare module 'musora-content-services' {
@@ -157,6 +162,8 @@ declare module 'musora-content-services' {
157
162
  addItemToPlaylist,
158
163
  assignmentStatusCompleted,
159
164
  assignmentStatusReset,
165
+ clearLastUpdatedTime,
166
+ clearPermissionsData,
160
167
  contentStatusCompleted,
161
168
  contentStatusReset,
162
169
  countAssignmentsAndLessons,
@@ -217,6 +224,7 @@ declare module 'musora-content-services' {
217
224
  fetchPlaylistItems,
218
225
  fetchRelatedLessons,
219
226
  fetchRelatedSongs,
227
+ fetchRelatedTutorials,
220
228
  fetchReturning,
221
229
  fetchSanity,
222
230
  fetchScheduledReleases,
@@ -272,9 +280,11 @@ declare module 'musora-content-services' {
272
280
  reset,
273
281
  setLastUpdatedTime,
274
282
  setStudentViewForUser,
283
+ setUserMetadata,
275
284
  similarItems,
276
285
  unlikeContent,
277
286
  unpinPlaylist,
287
+ updatePermissionsData,
278
288
  updatePlaylist,
279
289
  updatePlaylistItem,
280
290
  verifyLocalDataContext,
package/src/index.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  import {
4
4
  globalConfig,
5
- initializeService
5
+ initializeService,
6
+ setUserMetadata
6
7
  } from './services/config.js';
7
8
 
8
9
  import {
@@ -32,6 +33,7 @@ import {
32
33
  } from './services/dataContext.js';
33
34
 
34
35
  import {
36
+ clearLastUpdatedTime,
35
37
  setLastUpdatedTime,
36
38
  wasLastUpdateOlderThanXSeconds
37
39
  } from './services/lastUpdated.js';
@@ -135,6 +137,7 @@ import {
135
137
  fetchPlayAlongsCount,
136
138
  fetchRelatedLessons,
137
139
  fetchRelatedSongs,
140
+ fetchRelatedTutorials,
138
141
  fetchReturning,
139
142
  fetchSanity,
140
143
  fetchScheduledReleases,
@@ -148,14 +151,18 @@ import {
148
151
  } from './services/sanity.js';
149
152
 
150
153
  import {
154
+ clearPermissionsData,
151
155
  fetchUserPermissions,
152
- reset
156
+ reset,
157
+ updatePermissionsData
153
158
  } from './services/userPermissions.js';
154
159
 
155
160
  export {
156
161
  addItemToPlaylist,
157
162
  assignmentStatusCompleted,
158
163
  assignmentStatusReset,
164
+ clearLastUpdatedTime,
165
+ clearPermissionsData,
159
166
  contentStatusCompleted,
160
167
  contentStatusReset,
161
168
  countAssignmentsAndLessons,
@@ -216,6 +223,7 @@ export {
216
223
  fetchPlaylistItems,
217
224
  fetchRelatedLessons,
218
225
  fetchRelatedSongs,
226
+ fetchRelatedTutorials,
219
227
  fetchReturning,
220
228
  fetchSanity,
221
229
  fetchScheduledReleases,
@@ -271,9 +279,11 @@ export {
271
279
  reset,
272
280
  setLastUpdatedTime,
273
281
  setStudentViewForUser,
282
+ setUserMetadata,
274
283
  similarItems,
275
284
  unlikeContent,
276
285
  unpinPlaylist,
286
+ updatePermissionsData,
277
287
  updatePlaylist,
278
288
  updatePlaylistItem,
279
289
  verifyLocalDataContext,
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @module Config
3
3
  */
4
+ import { verifyLocalDataContext } from './dataContext.js'
5
+ import { updatePermissionsData } from './userPermissions.js'
4
6
 
5
7
  export let globalConfig = {
6
8
  sanityConfig: {},
@@ -76,3 +78,12 @@ export function initializeService(config) {
76
78
  globalConfig.localTimezoneString = config.localTimezoneString || null
77
79
  globalConfig.recommendationsConfig = config.recommendationsConfig
78
80
  }
81
+
82
+ export function setUserMetadata(userMetaData) {
83
+ updatePermissionsData(userMetaData.permissionsData)
84
+
85
+ const userDataVersions = userMetaData.userDataVersions
86
+ for (let i = 0; i < userDataVersions.length; i++) {
87
+ verifyLocalDataContext(userDataVersions[i].dataVersionKey, userDataVersions[i].currentVersion)
88
+ }
89
+ }
@@ -5,7 +5,12 @@ import { globalConfig } from './config.js'
5
5
  *
6
6
  * @type {string[]}
7
7
  */
8
- const excludeFromGeneratedIndex = ['wasLastUpdateOlderThanXSeconds', 'setLastUpdatedTime']
8
+ const excludeFromGeneratedIndex = [
9
+ 'wasLastUpdateOlderThanXSeconds',
10
+ 'setLastUpdatedTime',
11
+ 'clearLastUpdatedTime',
12
+ ]
13
+
9
14
  export function wasLastUpdateOlderThanXSeconds(seconds, key) {
10
15
  let lastUpdated = globalConfig.localStorage.getItem(key)
11
16
  if (!lastUpdated) return false
@@ -16,3 +21,7 @@ export function wasLastUpdateOlderThanXSeconds(seconds, key) {
16
21
  export function setLastUpdatedTime(key) {
17
22
  globalConfig.localStorage.setItem(key, new Date().getTime()?.toString())
18
23
  }
24
+
25
+ export function clearLastUpdatedTime(key) {
26
+ globalConfig.localStorage.removeItem(key)
27
+ }
@@ -1299,7 +1299,7 @@ export async function fetchRelatedLessons(railContentId, brand) {
1299
1299
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1300
1300
  const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail_url":thumbnail.asset->url, length_in_seconds, web_url_path, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
1301
1301
  const queryFieldsWithSort = queryFields + ', sort'
1302
- const query = `*[railcontent_id == ${railContentId} && brand == "${brand}" && (!defined(permission) || references(*[_type=='permission']._id))]{
1302
+ const query = `*[railcontent_id == ${railContentId} && brand == "${brand}"]{
1303
1303
  _type, parent_type, railcontent_id,
1304
1304
  "related_lessons" : array::unique([
1305
1305
  ...(*[${filterNeighbouringSiblings}][0].child[${childrenFilter}]->{${queryFields}}),
@@ -1312,6 +1312,137 @@ export async function fetchRelatedLessons(railContentId, brand) {
1312
1312
  return fetchSanity(query, false)
1313
1313
  }
1314
1314
 
1315
+ /**
1316
+ * fetch song tutorials related to a specific tutorial, by genre and difficulty.
1317
+ * @param {number} railContentId
1318
+ * @param {string} brand
1319
+ * @returns {Promise<Object|null>}
1320
+ */
1321
+ export async function fetchRelatedTutorials(railContentId, brand) {
1322
+ const parentObject = await fetchParentData(railContentId, brand)
1323
+ const relatedLessonObject = await fetchRelatedLessonsSectionData(parentObject)
1324
+ return formatForResponse(parentObject, relatedLessonObject)
1325
+ }
1326
+
1327
+ /**
1328
+ * fetch data of parent content, and combine with some of current content
1329
+ * @param {number} railContentId
1330
+ * @param {string} brand
1331
+ * @returns {Promise<Object|null>}
1332
+ */
1333
+ async function fetchParentData(railContentId, brand) {
1334
+ const parentQuery = buildQueryForFetch(railContentId, brand)
1335
+ return await fetchSanity(parentQuery, false)
1336
+ }
1337
+
1338
+ /**
1339
+ * build query for fetch of parent data
1340
+ * @param {number} railContentId
1341
+ * @param {string} brand
1342
+ * @returns {string}
1343
+ */
1344
+ function buildQueryForFetch(railContentId, brand) {
1345
+ const projections = `railcontent_id, _type, parent_type, parent_content_data, difficulty_string, brand`
1346
+ return `*[railcontent_id == ${railContentId} && brand == "${brand}"]{${projections}}`
1347
+ }
1348
+
1349
+ /**
1350
+ * fetch related lessons content
1351
+ * @param {Object} currentContent
1352
+ * @returns {Promise<Object|null>}
1353
+ */
1354
+ async function fetchRelatedLessonsSectionData(currentContent) {
1355
+ const query = await buildRelatedLessonsQuery(currentContent)
1356
+ return await fetchSanity(query, true)
1357
+ }
1358
+
1359
+ /**
1360
+ * build query for related lessons by content type
1361
+ * @param {Object} currentContent
1362
+ * @returns {Promise<string>}
1363
+ */
1364
+ async function buildRelatedLessonsQuery(currentContent) {
1365
+ const defaultProjectionsAndSorting = `{_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail_url":thumbnail.asset->url, length_in_seconds, web_url_path, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type,genre}|order(published_on desc, title asc)[0...10]`
1366
+ const currentContentData = await getCurrentContentDataForQuery(currentContent)
1367
+ const tutorialQuery = await buildSubQueryForFetch(currentContentData.parentType, currentContentData.brand, currentContentData.parentId, currentContentData.difficulty_string, currentContentData.genres)
1368
+ const quickTipQuery = await buildSubQueryForFetch("quick-tips", currentContentData.brand, currentContentData.parentId, currentContentData.difficulty_string)
1369
+ const songQuery = await buildSubQueryForFetch("song", currentContentData.brand, currentContentData.parentId, currentContentData.difficulty_string)
1370
+ return `[...*[${tutorialQuery}]${defaultProjectionsAndSorting}, ...*[${quickTipQuery}]${defaultProjectionsAndSorting}, ...*[${songQuery}]${defaultProjectionsAndSorting}, ]`
1371
+ }
1372
+
1373
+ /**
1374
+ * get data, primarily brand, for use in the related_lessons query
1375
+ * @param {Object} currentContent
1376
+ * @returns {Object}
1377
+ */
1378
+ async function getCurrentContentDataForQuery(currentContent) {
1379
+ const currentContentData = groupCurrentContentData(currentContent)
1380
+ const genres = await fetchParentContentGenres(currentContentData)
1381
+ const genreString = formatGenresToString(genres)
1382
+ return {...currentContentData,
1383
+ genres: genreString
1384
+ }
1385
+ }
1386
+
1387
+ /**
1388
+ * group and return specific data retrieved from parent data
1389
+ * @param {Object} currentContent
1390
+ * @returns {Object}
1391
+ */
1392
+ function groupCurrentContentData(currentContent) {
1393
+ return {
1394
+ parentType: currentContent.parent_type,
1395
+ parentId: currentContent.parent_content_data[0].id,
1396
+ difficulty_string: currentContent.difficulty_string,
1397
+ brand: currentContent.brand
1398
+ }
1399
+ }
1400
+
1401
+ /**
1402
+ * fetch genres of parent content
1403
+ * @param {Object} contentData
1404
+ * @returns {Promise<Object|null>}
1405
+ */
1406
+ async function fetchParentContentGenres(contentData) {
1407
+ const genreQuery = `*[_type == "${contentData.parentType}" && brand == "${contentData.brand}" && railcontent_id == ${contentData.parentId}][0]{"genre":genre[]->_id}`
1408
+ return fetchSanity(genreQuery, true)
1409
+ }
1410
+
1411
+ /**
1412
+ * combine data into Object
1413
+ * @param {Object} genres
1414
+ * @returns {string}
1415
+ */
1416
+ function formatGenresToString(genres) {
1417
+ return JSON.stringify(genres['genre']).replace(/[\[\]]/g, '')
1418
+ }
1419
+
1420
+ /**
1421
+ * build filters for use in related_lessons query
1422
+ * @param {string} type
1423
+ * @param {string} brand
1424
+ * @param {number} id
1425
+ * @param {string} difficulty
1426
+ * @param {string|null} genres
1427
+ * @returns {Promise<string>}
1428
+ */
1429
+ async function buildSubQueryForFetch(type, brand, id, difficulty, genres = null) {
1430
+ const genreString = genres ? `&& references([${genres}])` : ``
1431
+ return new FilterBuilder(`_type == "${type}" && brand == "${brand}" && railcontent_id != ${id} && difficulty_string == "${difficulty}" ${genreString}`).buildFilter()
1432
+ }
1433
+
1434
+ /**
1435
+ * format return Object for use by page
1436
+ * @param {Object} parentObject
1437
+ * @param {Object} relatedLessonObject
1438
+ * @returns {Object}
1439
+ */
1440
+ function formatForResponse(parentObject, relatedLessonObject) {
1441
+ return {...parentObject,
1442
+ related_lessons: relatedLessonObject
1443
+ }
1444
+ }
1445
+
1315
1446
  /**
1316
1447
  * Fetch all packs.
1317
1448
  * @param {string} brand - The brand for which to fetch packs.
@@ -1,5 +1,9 @@
1
1
  import { fetchUserPermissionsData } from './railcontent.js'
2
- import { setLastUpdatedTime, wasLastUpdateOlderThanXSeconds } from './lastUpdated.js'
2
+ import {
3
+ clearLastUpdatedTime,
4
+ setLastUpdatedTime,
5
+ wasLastUpdateOlderThanXSeconds,
6
+ } from './lastUpdated.js'
3
7
 
4
8
  /**
5
9
  * Exported functions that are excluded from index generation.
@@ -22,3 +26,13 @@ export async function fetchUserPermissions() {
22
26
  export async function reset() {
23
27
  userPermissionsPromise = null
24
28
  }
29
+
30
+ export function updatePermissionsData(permissionsData) {
31
+ userPermissionsPromise = permissionsData
32
+ setLastUpdatedTime(lastUpdatedKey)
33
+ }
34
+
35
+ export function clearPermissionsData() {
36
+ userPermissionsPromise = null
37
+ clearLastUpdatedTime(lastUpdatedKey)
38
+ }
@@ -0,0 +1,29 @@
1
+ import { setUserMetadata, verifyLocalDataContext } from '../src/services/config.js'
2
+ import { initializeTestService } from './initializeTests.js'
3
+ import { fetchUserPermissions, updatePermissionsData } from '../src/services/userPermissions.js'
4
+
5
+ describe('config', function () {
6
+ beforeEach(() => {
7
+ initializeTestService()
8
+ })
9
+
10
+ test('setUserMetadata', async () => {
11
+ const newPermissions = [80, 81, 82]
12
+ const isAdminUpdate = true
13
+
14
+ const userMetadata = {
15
+ permissionsData: { permissions: newPermissions, isAdmin: isAdminUpdate },
16
+ userDataVersions: [
17
+ { dataVersionKey: 1, currentVersion: 2 },
18
+ { dataVersionKey: 2, currentVersion: 3 },
19
+ ],
20
+ }
21
+
22
+ setUserMetadata(userMetadata)
23
+
24
+ let result = await fetchUserPermissions()
25
+
26
+ expect(result.permissions).toStrictEqual(newPermissions)
27
+ expect(result.isAdmin).toStrictEqual(isAdminUpdate)
28
+ })
29
+ })
@@ -1,5 +1,6 @@
1
1
  import { globalConfig, initializeService } from '../src'
2
2
  import { LocalStorageMock } from './localStorageMock'
3
+ import { clearPermissionsData } from '../src/services/userPermissions.js'
3
4
 
4
5
  const railContentModule = require('../src/services/railcontent.js')
5
6
  let token = null
@@ -36,6 +37,7 @@ export async function initializeTestService(useLive = false) {
36
37
  let mock = jest.spyOn(railContentModule, 'fetchUserPermissionsData')
37
38
  let testData = { permissions: [78, 91, 92], isAdmin: false }
38
39
  mock.mockImplementation(() => testData)
40
+ clearPermissionsData()
39
41
  }
40
42
 
41
43
  async function fetchLoginToken(email, password) {
@@ -29,6 +29,8 @@ const {
29
29
  fetchFoundation,
30
30
  fetchMethod,
31
31
  fetchRelatedLessons,
32
+ fetchRelatedTutorials,
33
+ newFetchRelatedTutorials,
32
34
  fetchAllPacks,
33
35
  fetchPackAll,
34
36
  fetchLessonContent,
@@ -288,6 +290,20 @@ describe('Sanity Queries', function () {
288
290
  expect(isMatch).toBeTruthy()
289
291
  })
290
292
 
293
+ test('fetchRelatedTutorials', async () => {
294
+ const railContentId = 387379
295
+ const brand = "pianote"
296
+ const queryResult = await fetchRelatedTutorials(railContentId, brand)
297
+ console.log(queryResult)
298
+ expect(typeof queryResult).toBe('object') //check structure of parent
299
+ expect(typeof queryResult.parent_content_data).toBe('object') //check structure of parentdata
300
+ expect(typeof queryResult.related_lessons).toBe('object') //check structure of relatedlessons
301
+ expect(queryResult.related_lessons[0]._id).toBe('song-tutorial_333333') //check there is a first element and matches
302
+ expect(queryResult.related_lessons[6]._id).toBe('quick-tips_390225') //check first quiktips
303
+ expect(queryResult.related_lessons[16]._id).toBe('0c5aa2d7-91a3-4349-b999-9ed9dd916e8e') //check first songs
304
+ expect(queryResult.related_lessons.length).toBe(26)
305
+ })
306
+
291
307
  test('fetchRelatedLessons-quick-tips', async () => {
292
308
  const id = 406213
293
309
  const response = await fetchRelatedLessons(id, 'singeo')
@@ -308,8 +324,6 @@ describe('Sanity Queries', function () {
308
324
  expect(Array.isArray(relatedLessons)).toBe(true)
309
325
  relatedLessons.forEach((lesson) => {
310
326
  expect(lesson._type).toBe('in-rhythm')
311
- expect(lesson.sort).toBeGreaterThan(episode)
312
- episode = lesson.sort
313
327
  })
314
328
  })
315
329
 
@@ -1,5 +1,9 @@
1
1
  const { fetchUserPermissions } = require('../src/services/userPermissions')
2
2
  const { initializeTestService } = require('./initializeTests')
3
+ const {
4
+ updatePermissionsData,
5
+ clearPermissionsData,
6
+ } = require('../src/services/userPermissions.js')
3
7
 
4
8
  describe('userPermissions', function () {
5
9
  beforeEach(() => {
@@ -16,4 +20,25 @@ describe('userPermissions', function () {
16
20
  expect(result.isAdmin).toStrictEqual(false)
17
21
  expect(result).toBe(result2)
18
22
  })
23
+
24
+ test('updatePermissionsData', async () => {
25
+ const newPermissions = [80, 81, 82]
26
+ const isAdminUpdate = true
27
+
28
+ // Call the function to update permissions
29
+ updatePermissionsData({
30
+ permissions: newPermissions,
31
+ isAdmin: isAdminUpdate,
32
+ })
33
+ let result = await fetchUserPermissions()
34
+
35
+ expect(result.permissions).toStrictEqual(newPermissions)
36
+ expect(result.isAdmin).toStrictEqual(isAdminUpdate)
37
+
38
+ clearPermissionsData()
39
+ result = await fetchUserPermissions()
40
+
41
+ expect(result.permissions).toStrictEqual([78, 91, 92])
42
+ expect(result.isAdmin).toStrictEqual(false)
43
+ })
19
44
  })