musora-content-services 1.0.99 → 1.0.101
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 +4 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +46 -3
- package/src/index.d.ts +6 -2
- package/src/index.js +2 -0
- package/src/services/railcontent.js +50 -0
- package/src/services/sanity.js +89 -22
- package/src/services/userContext.js +105 -0
- package/test/localStorageMock.js +21 -0
- package/test/sanityQueryService.test.js +6 -0
- package/test/userContext.test.js +86 -0
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.101](https://github.com/railroadmedia/musora-content-services/compare/v1.0.100...v1.0.101) (2024-09-17)
|
|
6
|
+
|
|
7
|
+
### [1.0.100](https://github.com/railroadmedia/musora-content-services/compare/v1.0.99...v1.0.100) (2024-09-16)
|
|
8
|
+
|
|
5
9
|
### [1.0.99](https://github.com/railroadmedia/musora-content-services/compare/v1.0.98...v1.0.99) (2024-09-12)
|
|
6
10
|
|
|
7
11
|
### [1.0.98](https://github.com/railroadmedia/musora-content-services/compare/v1.0.97...v1.0.98) (2024-09-12)
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -46,6 +46,7 @@ const contentWithSortField = {
|
|
|
46
46
|
let contentTypeConfig = {
|
|
47
47
|
'song': {
|
|
48
48
|
'fields': [
|
|
49
|
+
'album',
|
|
49
50
|
'soundslice',
|
|
50
51
|
'instrumentless',
|
|
51
52
|
],
|
|
@@ -78,13 +79,15 @@ let contentTypeConfig = {
|
|
|
78
79
|
'"instructors": instructor[]->name',
|
|
79
80
|
`"description": ${descriptionField}`,
|
|
80
81
|
'resource',
|
|
82
|
+
'xp',
|
|
83
|
+
'total_xp',
|
|
81
84
|
`"lessons": child[]->{
|
|
82
85
|
"id": railcontent_id,
|
|
83
86
|
title,
|
|
84
87
|
"image": thumbnail.asset->url,
|
|
85
88
|
"instructors": instructor[]->name,
|
|
86
89
|
length_in_seconds,
|
|
87
|
-
}`,
|
|
90
|
+
}`,
|
|
88
91
|
]
|
|
89
92
|
},
|
|
90
93
|
'method': {
|
|
@@ -182,9 +185,19 @@ let contentTypeConfig = {
|
|
|
182
185
|
},
|
|
183
186
|
'instructor': {
|
|
184
187
|
'fields': [
|
|
185
|
-
'
|
|
188
|
+
'"coach_top_banner_image": coach_top_banner_image.asset->url',
|
|
189
|
+
'"coach_bottom_banner_image": coach_bottom_banner_image.asset->url',
|
|
186
190
|
'"coach_card_image": coach_card_image.asset->url',
|
|
187
|
-
'
|
|
191
|
+
'"coach_featured_image": coach_featured_image.asset->url',
|
|
192
|
+
'"coach_top_banner_image": coach_top_banner_image.asset->url',
|
|
193
|
+
'focus',
|
|
194
|
+
'focus_text',
|
|
195
|
+
'forum_thread_id',
|
|
196
|
+
'long_bio',
|
|
197
|
+
'name',
|
|
198
|
+
'short_bio',
|
|
199
|
+
'bands',
|
|
200
|
+
'endorsements',
|
|
188
201
|
]
|
|
189
202
|
},
|
|
190
203
|
// content with just the added 'instructors' Field
|
|
@@ -215,6 +228,34 @@ let contentTypeConfig = {
|
|
|
215
228
|
'exploring-beats': contentWithSortField,
|
|
216
229
|
}
|
|
217
230
|
|
|
231
|
+
function getNewReleasesTypes(brand) {
|
|
232
|
+
const baseNewTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "boot-camps", "quick-tips", "workout", "challenge", "challenge-part", "podcasts", "pack", "song", "learning-path-level", "play-along", "course", "unit"];
|
|
233
|
+
switch(brand) {
|
|
234
|
+
case 'drumeo':
|
|
235
|
+
return [...baseNewTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "student-collaborations", "live-streams", "solos", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums"];
|
|
236
|
+
case 'guitareo':
|
|
237
|
+
return [...baseNewTypes, "archives", "recording", "chords-and-scales"];
|
|
238
|
+
case 'pianote':
|
|
239
|
+
case 'singeo':
|
|
240
|
+
default:
|
|
241
|
+
return baseNewTypes
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getUpcomingEventsTypes(brand) {
|
|
246
|
+
const baseLiveTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "boot-camps", "quick-tips", "recording", "pack-bundle-lesson"];
|
|
247
|
+
switch(brand) {
|
|
248
|
+
case 'drumeo':
|
|
249
|
+
return [...baseLiveTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "student-collaborations", "live-streams", "podcasts", "solos", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums"];
|
|
250
|
+
case 'guitareo':
|
|
251
|
+
return [...baseLiveTypes, "archives"];
|
|
252
|
+
case 'pianote':
|
|
253
|
+
case 'singeo':
|
|
254
|
+
default:
|
|
255
|
+
return baseLiveTypes;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
218
259
|
function artistOrInstructorName(key='artist_name') {
|
|
219
260
|
return `'${key}': coalesce(artist->name, instructor[0]->name)`;
|
|
220
261
|
}
|
|
@@ -264,4 +305,6 @@ module.exports = {
|
|
|
264
305
|
DEFAULT_FIELDS,
|
|
265
306
|
assignmentsField,
|
|
266
307
|
filtersToGroq,
|
|
308
|
+
getNewReleasesTypes,
|
|
309
|
+
getUpcomingEventsTypes,
|
|
267
310
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -28,17 +28,19 @@ import {
|
|
|
28
28
|
fetchLessonContent,
|
|
29
29
|
fetchCourseOverview,
|
|
30
30
|
fetchChallengeOverview,
|
|
31
|
+
fetchScheduledReleases,
|
|
31
32
|
fetchArtistLessons,
|
|
32
33
|
fetchGenreLessons,
|
|
33
34
|
} from './services/sanity.js';
|
|
34
35
|
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
36
37
|
fetchCompletedState,
|
|
37
38
|
fetchAllCompletedStates,
|
|
38
39
|
fetchContentInProgress,
|
|
40
|
+
fetchCompletedContent,
|
|
39
41
|
fetchVimeoData,
|
|
40
42
|
fetchContentPageUserData,
|
|
41
|
-
} from
|
|
43
|
+
} from './services/railcontent.js';
|
|
42
44
|
|
|
43
45
|
import { initializeService } from './services/config.js';
|
|
44
46
|
|
|
@@ -79,7 +81,9 @@ declare module 'musora-content-services' {
|
|
|
79
81
|
fetchCompletedState,
|
|
80
82
|
fetchAllCompletedStates,
|
|
81
83
|
fetchContentInProgress,
|
|
84
|
+
fetchCompletedContent,
|
|
82
85
|
fetchContentPageUserData,
|
|
86
|
+
fetchScheduledReleases,
|
|
83
87
|
fetchArtistLessons,
|
|
84
88
|
fetchGenreLessons,
|
|
85
89
|
}
|
package/src/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
fetchChallengeOverview,
|
|
36
36
|
fetchCoachLessons,
|
|
37
37
|
fetchByReference,
|
|
38
|
+
fetchScheduledReleases,
|
|
38
39
|
fetchArtistLessons,
|
|
39
40
|
fetchGenreLessons,
|
|
40
41
|
} from './services/sanity.js';
|
|
@@ -91,6 +92,7 @@ export {
|
|
|
91
92
|
fetchContentPageUserData,
|
|
92
93
|
fetchCoachLessons,
|
|
93
94
|
fetchByReference,
|
|
95
|
+
fetchScheduledReleases,
|
|
94
96
|
fetchArtistLessons,
|
|
95
97
|
fetchGenreLessons,
|
|
96
98
|
}
|
|
@@ -261,4 +261,54 @@ function fetchAbsolute(url, params) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
return fetch(url, params);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function fetchUserContext() {
|
|
267
|
+
let url = `/content/user_data_all`;
|
|
268
|
+
const headers = {
|
|
269
|
+
'Content-Type': 'application/json',
|
|
270
|
+
'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
const response = await fetch(url, {headers});
|
|
274
|
+
const result = await response.json();
|
|
275
|
+
if (result) {
|
|
276
|
+
console.log('fetchUserContext', result);
|
|
277
|
+
return result;
|
|
278
|
+
} else {
|
|
279
|
+
console.log('result not json');
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error('Fetch error:', error);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function fetchHandler(url, method = "get") {
|
|
288
|
+
const headers = {
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
|
|
291
|
+
};
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetch(url, {method, headers});
|
|
294
|
+
const result = await response.json();
|
|
295
|
+
if (result) {
|
|
296
|
+
return result;
|
|
297
|
+
} else {
|
|
298
|
+
console.log('result not json');
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('Fetch error:', error);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function fetchLikeContent(contentId) {
|
|
307
|
+
let url = `/content/${contentId}/like`;
|
|
308
|
+
return await fetchHandler(url, "post");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function fetchUnlikeContent(contentId) {
|
|
312
|
+
let url = `/content/${contentId}/unlike`;
|
|
313
|
+
return await fetchHandler(url, "post");
|
|
264
314
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
contentTypeConfig,
|
|
10
10
|
DEFAULT_FIELDS,
|
|
11
11
|
getFieldsForContentType,
|
|
12
|
-
filtersToGroq
|
|
12
|
+
filtersToGroq,
|
|
13
|
+
getUpcomingEventsTypes,
|
|
14
|
+
getNewReleasesTypes,
|
|
13
15
|
} from "../contentTypeConfig";
|
|
14
16
|
import {globalConfig} from "./config";
|
|
15
17
|
|
|
@@ -238,7 +240,7 @@ export async function fetchSongCount(brand) {
|
|
|
238
240
|
* @example
|
|
239
241
|
* fetchWorkouts('drumeo')
|
|
240
242
|
* .then(workouts => console.log(workouts))
|
|
241
|
-
* .catch(error => console.error(error));
|
|
243
|
+
* .catch(error => console.error(error));
|
|
242
244
|
*/
|
|
243
245
|
export async function fetchWorkouts(brand) {
|
|
244
246
|
const fields = getFieldsForContentType('workout');
|
|
@@ -254,14 +256,8 @@ export async function fetchWorkouts(brand) {
|
|
|
254
256
|
* @returns {Promise<Object|null>} - The fetched new releases data or null if not found.
|
|
255
257
|
*/
|
|
256
258
|
export async function fetchNewReleases(brand, { page = 1, limit = 10, sort="-published_on" } = {}) {
|
|
257
|
-
const newTypes =
|
|
258
|
-
|
|
259
|
-
'pianote': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
|
|
260
|
-
'guitareo': ["student-review", "student-reviews", "question-and-answer", "archives", "recording", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
|
|
261
|
-
'singeo': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
|
|
262
|
-
'default': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"]
|
|
263
|
-
};
|
|
264
|
-
const typesString = arrayJoinWithQuotes(newTypes[brand] ?? newTypes['default']);
|
|
259
|
+
const newTypes = getNewReleasesTypes(brand);
|
|
260
|
+
const typesString = arrayJoinWithQuotes(newTypes);
|
|
265
261
|
const start = (page - 1) * limit;
|
|
266
262
|
const end = start + limit;
|
|
267
263
|
const sortOrder = getSortOrder(sort);
|
|
@@ -297,15 +293,8 @@ export async function fetchNewReleases(brand, { page = 1, limit = 10, sort="-pub
|
|
|
297
293
|
* .catch(error => console.error(error));
|
|
298
294
|
*/
|
|
299
295
|
export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {}) {
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
'drumeo': [...baseLiveTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "student-collaborations", "live-streams", "podcasts", "solos", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums"],
|
|
303
|
-
'pianote': baseLiveTypes,
|
|
304
|
-
'guitareo': [...baseLiveTypes, "archives"],
|
|
305
|
-
'singeo': baseLiveTypes,
|
|
306
|
-
'default': baseLiveTypes
|
|
307
|
-
};
|
|
308
|
-
const typesString = arrayJoinWithQuotes(liveTypes[brand] ?? liveTypes['default']);
|
|
296
|
+
const liveTypes = getUpcomingEventsTypes(brand);
|
|
297
|
+
const typesString = arrayJoinWithQuotes(liveTypes);
|
|
309
298
|
const now = getSanityDate(new Date());
|
|
310
299
|
const start = (page - 1) * limit;
|
|
311
300
|
const end = start + limit;
|
|
@@ -325,6 +314,45 @@ export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {})
|
|
|
325
314
|
return fetchSanity(query, true);
|
|
326
315
|
}
|
|
327
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Fetch scheduled releases for a specific brand.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} brand - The brand for which to fetch scheduled releasess.
|
|
321
|
+
* @returns {Promise<Object|null>} - A promise that resolves to an array of scheduled release objects or null if not found.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* fetchScheduledReleases('drumeo', {
|
|
325
|
+
* page: 2,
|
|
326
|
+
* limit: 20,
|
|
327
|
+
* })
|
|
328
|
+
* .then(content => console.log(content))
|
|
329
|
+
* .catch(error => console.error(error));
|
|
330
|
+
*/
|
|
331
|
+
export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
|
|
332
|
+
const upcomingTypes = getUpcomingEventsTypes(brand);
|
|
333
|
+
const newTypes = getNewReleasesTypes(brand);
|
|
334
|
+
|
|
335
|
+
const scheduledTypes = merge(upcomingTypes, newTypes)
|
|
336
|
+
const typesString = arrayJoinWithQuotes(scheduledTypes);
|
|
337
|
+
const now = getSanityDate(new Date());
|
|
338
|
+
const start = (page - 1) * limit;
|
|
339
|
+
const end = start + limit;
|
|
340
|
+
const query = `*[_type in [${typesString}] && brand == '${brand}' && status in ['published','scheduled'] && published_on > '${now}']{
|
|
341
|
+
"id": railcontent_id,
|
|
342
|
+
title,
|
|
343
|
+
"image": thumbnail.asset->url,
|
|
344
|
+
"artist_name": instructor[0]->name,
|
|
345
|
+
"artists": instructor[]->name,
|
|
346
|
+
difficulty,
|
|
347
|
+
difficulty_string,
|
|
348
|
+
length_in_seconds,
|
|
349
|
+
published_on,
|
|
350
|
+
"type": _type,
|
|
351
|
+
web_url_path,
|
|
352
|
+
} | order(published_on asc)[${start}...${end}]`;
|
|
353
|
+
return fetchSanity(query, true);
|
|
354
|
+
}
|
|
355
|
+
|
|
328
356
|
/**
|
|
329
357
|
* Fetch content by a specific Railcontent ID.
|
|
330
358
|
*
|
|
@@ -415,7 +443,7 @@ export async function fetchAll(brand, type, {
|
|
|
415
443
|
const searchFilter = searchTerm
|
|
416
444
|
? groupBy !== "" ?
|
|
417
445
|
`&& (^.name match "${searchTerm}*" || title match "${searchTerm}*")`
|
|
418
|
-
: `&& (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*")`
|
|
446
|
+
: `&& (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*" || name match "${searchTerm}*")`
|
|
419
447
|
: "";
|
|
420
448
|
|
|
421
449
|
// Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
|
|
@@ -499,8 +527,9 @@ export function getSortOrder(sort= '-published_on', groupBy)
|
|
|
499
527
|
case "slug":
|
|
500
528
|
sortOrder = groupBy ? 'name' : "title";
|
|
501
529
|
break;
|
|
530
|
+
case "name":
|
|
502
531
|
case "popularity":
|
|
503
|
-
sortOrder =
|
|
532
|
+
sortOrder = sort;
|
|
504
533
|
break;
|
|
505
534
|
case "published_on":
|
|
506
535
|
default:
|
|
@@ -664,6 +693,7 @@ export async function fetchMethod(brand, slug) {
|
|
|
664
693
|
"type": _type,
|
|
665
694
|
"description": ${descriptionField},
|
|
666
695
|
"url": web_url_path,
|
|
696
|
+
web_url_path,
|
|
667
697
|
xp,
|
|
668
698
|
}
|
|
669
699
|
} | order(published_on asc)`
|
|
@@ -821,7 +851,13 @@ export async function fetchLessonContent(railContentId) {
|
|
|
821
851
|
"coach_profile_image":thumbnail_url.asset->url
|
|
822
852
|
},
|
|
823
853
|
"instructors":instructor[]->name,
|
|
824
|
-
instructor[]
|
|
854
|
+
"instructor": instructor[]->{
|
|
855
|
+
"id":_id,
|
|
856
|
+
name,
|
|
857
|
+
short_bio,
|
|
858
|
+
web_url_path,
|
|
859
|
+
"coach_card_image": coach_card_image.asset->url,
|
|
860
|
+
},
|
|
825
861
|
${assignmentsField}
|
|
826
862
|
video,
|
|
827
863
|
length_in_seconds
|
|
@@ -851,6 +887,30 @@ export async function fetchRelatedLessons(railContentId, brand) {
|
|
|
851
887
|
return fetchSanity(query, false);
|
|
852
888
|
}
|
|
853
889
|
|
|
890
|
+
/**
|
|
891
|
+
* Fetch related method lessons for a specific lesson by RailContent ID and type.
|
|
892
|
+
* @param {string} railContentId - The RailContent ID of the current lesson.
|
|
893
|
+
* @param {string} brand - The current brand.
|
|
894
|
+
* @returns {Promise<Object>|null>} - The fetched related lessons
|
|
895
|
+
*/
|
|
896
|
+
export async function fetchRelatedMethodLessons(railContentId, brand) {
|
|
897
|
+
const query = `*[railcontent_id == ${railContentId} && brand == "${brand}"]{
|
|
898
|
+
"id":_id,
|
|
899
|
+
"related_lessons": *[references(^._id)][0].child[]->{
|
|
900
|
+
"id": railcontent_id,
|
|
901
|
+
"type": _type,
|
|
902
|
+
title,
|
|
903
|
+
"description": description[0].children[0].text, // Extraer texto plano
|
|
904
|
+
"thumbnail_url": thumbnail.asset->url,
|
|
905
|
+
"url": web_url_path,
|
|
906
|
+
difficulty,
|
|
907
|
+
difficulty_string,
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}`
|
|
911
|
+
return fetchSanity(query, false);
|
|
912
|
+
}
|
|
913
|
+
|
|
854
914
|
/**
|
|
855
915
|
* Fetch all packs.
|
|
856
916
|
* @param {string} brand - The brand for which to fetch packs.
|
|
@@ -1224,6 +1284,13 @@ function getSanityDate(date) {
|
|
|
1224
1284
|
return date.toISOString();
|
|
1225
1285
|
}
|
|
1226
1286
|
|
|
1287
|
+
const merge = (a, b, predicate = (a, b) => a === b) => {
|
|
1288
|
+
const c = [...a]; // copy to avoid side effects
|
|
1289
|
+
// add all items from B to copy C if they're not already present
|
|
1290
|
+
b.forEach((bItem) => (c.some((cItem) => predicate(bItem, cItem)) ? null : c.push(bItem)))
|
|
1291
|
+
return c;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1227
1294
|
function checkSanityConfig(config) {
|
|
1228
1295
|
if (!config.sanityConfig.token) {
|
|
1229
1296
|
console.warn('fetchSanity: The "token" property is missing in the config object.');
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {fetchUserContext, fetchLikeContent, fetchUnlikeContent} from "./railcontent";
|
|
2
|
+
|
|
3
|
+
const StorageKey = "userContext";
|
|
4
|
+
let userContext = null;
|
|
5
|
+
let cache = null;
|
|
6
|
+
|
|
7
|
+
export function init(localCache) {
|
|
8
|
+
cache = localCache;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function version() {
|
|
12
|
+
ensureLocalContextLoaded();
|
|
13
|
+
return userContext.version;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function getUserContext() {
|
|
17
|
+
ensureLocalContextLoaded();
|
|
18
|
+
if (userContext) {
|
|
19
|
+
verifyContextIsValid();
|
|
20
|
+
}
|
|
21
|
+
if (!userContext) {
|
|
22
|
+
await fetchFromServer();
|
|
23
|
+
}
|
|
24
|
+
return userContext;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function verifyContextIsValid() {
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureLocalContextLoaded() {
|
|
32
|
+
if (userContext) return;
|
|
33
|
+
let localData = cache.getItem(StorageKey);
|
|
34
|
+
if (localData) {
|
|
35
|
+
userContext = JSON.parse(localData);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function updateLocalContext(contentId, updateFunction) {
|
|
40
|
+
ensureLocalContextLoaded();
|
|
41
|
+
if (userContext) {
|
|
42
|
+
let contentData = userContext.data[contentId] ?? [];
|
|
43
|
+
updateFunction(contentData);
|
|
44
|
+
userContext.data[contentId] = contentData;
|
|
45
|
+
userContext.version++;
|
|
46
|
+
let data = JSON.stringify(userContext);
|
|
47
|
+
cache.setItem(StorageKey, data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchFromServer() {
|
|
52
|
+
let data = await fetchUserContext();
|
|
53
|
+
userContext = JSON.parse(data);
|
|
54
|
+
cache.setItem(StorageKey, data);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
function transformData(data, contentId) {
|
|
59
|
+
let transformed = [];
|
|
60
|
+
transformed["contentId"] = contentId;
|
|
61
|
+
transformed["liked"] = (data && data.l) ?? 0;
|
|
62
|
+
return transformed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function fetchContentData(contentId) {
|
|
66
|
+
let userContext = await getUserContext();
|
|
67
|
+
let data = userContext.data[contentId];
|
|
68
|
+
data = transformData(data, contentId);
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function clearCache() {
|
|
73
|
+
userContext = null;
|
|
74
|
+
cache.setItem(StorageKey, null);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function testClearLocal() {
|
|
78
|
+
userContext = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function likeContent(contentId) {
|
|
82
|
+
updateLocalContext(contentId,
|
|
83
|
+
function (contentData) {
|
|
84
|
+
contentData.l = 1;
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
let result = await fetchLikeContent(contentId);
|
|
89
|
+
if (result.version !== userContext.version) {
|
|
90
|
+
clearCache();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function unlikeContent(contentId) {
|
|
95
|
+
updateLocalContext(contentId,
|
|
96
|
+
function (contentData) {
|
|
97
|
+
contentData.l = 0;
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
let result = await fetchUnlikeContent(contentId);
|
|
102
|
+
if (result.version !== userContext.version) {
|
|
103
|
+
clearCache();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class LocalStorageMock {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.store = {};
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
clear() {
|
|
7
|
+
this.store = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getItem(key) {
|
|
11
|
+
return this.store[key] || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setItem(key, value) {
|
|
15
|
+
this.store[key] = String(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
removeItem(key) {
|
|
19
|
+
delete this.store[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
fetchPacksAll,
|
|
33
33
|
fetchCoachLessons,
|
|
34
34
|
fetchByReference,
|
|
35
|
+
fetchScheduledReleases
|
|
35
36
|
} = require('../src/services/sanity.js');
|
|
36
37
|
|
|
37
38
|
describe('Sanity Queries', function () {
|
|
@@ -286,4 +287,9 @@ describe('Sanity Queries', function () {
|
|
|
286
287
|
const response = await fetchByReference('drumeo', { includedFields: ['is_featured'] });
|
|
287
288
|
expect(response.entity.length).toBeGreaterThan(0);
|
|
288
289
|
});
|
|
290
|
+
|
|
291
|
+
test('fetchScheduledReleases', async () => {
|
|
292
|
+
const response = await fetchScheduledReleases('drumeo', {});
|
|
293
|
+
expect(response.length).toBeGreaterThan(0);
|
|
294
|
+
});
|
|
289
295
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const railContentModule = require('../src/services/railcontent.js')
|
|
2
|
+
const userContextModule = require('../src/services/userContext.js');
|
|
3
|
+
import {LocalStorageMock} from "./localStorageMock";
|
|
4
|
+
|
|
5
|
+
describe('userContext', function () {
|
|
6
|
+
let mock = null;
|
|
7
|
+
const testVersion = 1;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
userContextModule.init(new LocalStorageMock());
|
|
10
|
+
mock = jest.spyOn(railContentModule, 'fetchUserContext');
|
|
11
|
+
var json = `{"version":${testVersion},"data":{"308516":{"l":1},"308515":{"p":100},"308514":{"p":13},"308518":{"p":100}}}`;
|
|
12
|
+
mock.mockImplementation(() => json);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('contentLiked', async () => {
|
|
16
|
+
let contentData = await userContextModule.fetchContentData(308516);
|
|
17
|
+
expect(contentData.liked).toBe(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('contentDoesNotExist', async () => {
|
|
21
|
+
//fetch content that does not exist
|
|
22
|
+
let contentData = await userContextModule.fetchContentData(121111);
|
|
23
|
+
expect(contentData.liked).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('ensureOnlyOneServerFetchRequest', async () => {
|
|
27
|
+
userContextModule.clearCache();
|
|
28
|
+
await userContextModule.fetchContentData(308516);
|
|
29
|
+
await userContextModule.fetchContentData(308514);
|
|
30
|
+
expect(railContentModule.fetchUserContext).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('ensureDataPulledFromLocalCache', async () => {
|
|
34
|
+
userContextModule.clearCache();
|
|
35
|
+
await userContextModule.fetchContentData(308516);
|
|
36
|
+
userContextModule.testClearLocal();
|
|
37
|
+
await userContextModule.fetchContentData(308514);
|
|
38
|
+
expect(railContentModule.fetchUserContext).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// test('hashExpiration', async () => {
|
|
42
|
+
// userContextModule.clearCache();
|
|
43
|
+
// await userContextModule.fetchContentData(testHash308516);
|
|
44
|
+
// let newHash = "8g9qg5wn3e5s5oi69q6g22et9w6g34t5";
|
|
45
|
+
// await userContextModule.fetchContentData(newHash, 308516);
|
|
46
|
+
// expect(railContentModule.fetchUserContext).toHaveBeenCalledTimes(2);
|
|
47
|
+
// });
|
|
48
|
+
|
|
49
|
+
test('likeContent', async () => {
|
|
50
|
+
mock = jest.spyOn(railContentModule, 'fetchLikeContent');
|
|
51
|
+
var json = JSON.parse(`{"version":${testVersion + 1}}`);
|
|
52
|
+
mock.mockImplementation(() => json);
|
|
53
|
+
|
|
54
|
+
userContextModule.clearCache();
|
|
55
|
+
await userContextModule.fetchContentData(308515);
|
|
56
|
+
await userContextModule.likeContent(308515);
|
|
57
|
+
let contentData = await userContextModule.fetchContentData(308515);
|
|
58
|
+
expect(contentData.liked).toBe(1);
|
|
59
|
+
|
|
60
|
+
userContextModule.testClearLocal();
|
|
61
|
+
contentData = await userContextModule.fetchContentData(308515);
|
|
62
|
+
expect(contentData.liked).toBe(1);
|
|
63
|
+
|
|
64
|
+
expect(userContextModule.version()).toBe(testVersion + 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
test('unlikeContent', async () => {
|
|
69
|
+
mock = jest.spyOn(railContentModule, 'fetchUnlikeContent');
|
|
70
|
+
var json = JSON.parse(`{"version":${testVersion + 1}}`);
|
|
71
|
+
mock.mockImplementation(() => json);
|
|
72
|
+
|
|
73
|
+
userContextModule.clearCache();
|
|
74
|
+
await userContextModule.fetchContentData(308516);
|
|
75
|
+
await userContextModule.unlikeContent(308516);
|
|
76
|
+
let contentData = await userContextModule.fetchContentData(308516);
|
|
77
|
+
expect(contentData.liked).toBe(0);
|
|
78
|
+
|
|
79
|
+
userContextModule.testClearLocal();
|
|
80
|
+
contentData = await userContextModule.fetchContentData(308516);
|
|
81
|
+
expect(contentData.liked).toBe(0);
|
|
82
|
+
|
|
83
|
+
expect(userContextModule.version()).toBe(testVersion + 1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
});
|