musora-content-services 1.0.177 → 1.0.179
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/contentMetaData.js +0 -2
- package/src/contentTypeConfig.js +1 -1
- package/src/filterBuilder.js +22 -2
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -0
- package/src/services/railcontent.js +30 -0
- package/src/services/sanity.js +66 -4
- package/test/sanityQueryService.test.js +16 -12
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.179](https://github.com/railroadmedia/musora-content-services/compare/v1.0.178...v1.0.179) (2024-11-19)
|
|
6
|
+
|
|
7
|
+
### [1.0.178](https://github.com/railroadmedia/musora-content-services/compare/v1.0.177...v1.0.178) (2024-11-19)
|
|
8
|
+
|
|
5
9
|
### [1.0.177](https://github.com/railroadmedia/musora-content-services/compare/v1.0.176...v1.0.177) (2024-11-18)
|
|
6
10
|
|
|
7
11
|
### [1.0.176](https://github.com/railroadmedia/musora-content-services/compare/v1.0.161...v1.0.176) (2024-11-15)
|
package/package.json
CHANGED
package/src/contentMetaData.js
CHANGED
|
@@ -37,13 +37,11 @@ const commonMetadata ={
|
|
|
37
37
|
{
|
|
38
38
|
name: 'Completed',
|
|
39
39
|
short_name: 'COMPLETED',
|
|
40
|
-
is_group_by: true,
|
|
41
40
|
value: 'completed'
|
|
42
41
|
},
|
|
43
42
|
{
|
|
44
43
|
name: 'Owned Challenges',
|
|
45
44
|
short_name: 'OWNED CHALLENGES',
|
|
46
|
-
is_group_by: true,
|
|
47
45
|
value: 'owned',
|
|
48
46
|
},
|
|
49
47
|
],
|
package/src/contentTypeConfig.js
CHANGED
|
@@ -86,7 +86,7 @@ let contentTypeConfig = {
|
|
|
86
86
|
'fields': [
|
|
87
87
|
'enrollment_start_time',
|
|
88
88
|
'enrollment_end_time',
|
|
89
|
-
'registration_url',
|
|
89
|
+
"'registration_url': '/' + brand + '/enrollment/' + slug.current",
|
|
90
90
|
'"lesson_count": child_count',
|
|
91
91
|
'"primary_cta_text": select(dateTime(published_on) > dateTime(now()) && dateTime(enrollment_start_time) > dateTime(now()) => "Notify Me", "Start Challenge")',
|
|
92
92
|
'challenge_state',
|
package/src/filterBuilder.js
CHANGED
|
@@ -4,6 +4,10 @@ import {fetchUserPermissions} from "./services/userPermissions";
|
|
|
4
4
|
export class FilterBuilder {
|
|
5
5
|
|
|
6
6
|
STATUS_SCHEDULED = 'scheduled';
|
|
7
|
+
STATUS_PUBLISHED = 'published';
|
|
8
|
+
STATUS_DRAFT = 'draft';
|
|
9
|
+
STATUS_ARCHIVED = 'archived';
|
|
10
|
+
STATUS_UNLISTED = 'unlisted';
|
|
7
11
|
|
|
8
12
|
constructor(
|
|
9
13
|
filter = '',
|
|
@@ -13,13 +17,18 @@ export class FilterBuilder {
|
|
|
13
17
|
pullFutureContent = false,
|
|
14
18
|
getFutureContentOnly = false,
|
|
15
19
|
getFutureScheduledContentsOnly = false,
|
|
16
|
-
|
|
20
|
+
bypassStatuses = false,
|
|
21
|
+
bypassPublishedDateRestriction = false,
|
|
22
|
+
isSingle = false
|
|
17
23
|
} = {}) {
|
|
18
24
|
this.availableContentStatuses = availableContentStatuses;
|
|
19
25
|
this.bypassPermissions = bypassPermissions;
|
|
26
|
+
this.bypassStatuses = bypassStatuses;
|
|
27
|
+
this.bypassPublishedDateRestriction = bypassPublishedDateRestriction;
|
|
20
28
|
this.pullFutureContent = pullFutureContent;
|
|
21
29
|
this.getFutureContentOnly = getFutureContentOnly;
|
|
22
30
|
this.getFutureScheduledContentsOnly = getFutureScheduledContentsOnly;
|
|
31
|
+
this.isSingle = isSingle;
|
|
23
32
|
this.filter = filter;
|
|
24
33
|
// this.debug = process.env.DEBUG === 'true' || false;
|
|
25
34
|
this.debug = false;
|
|
@@ -48,7 +57,17 @@ export class FilterBuilder {
|
|
|
48
57
|
|
|
49
58
|
_applyContentStatuses() {
|
|
50
59
|
// This must be run before _applyPublishDateRestrictions()
|
|
51
|
-
if
|
|
60
|
+
if(this.bypassStatuses) return this;
|
|
61
|
+
if (this.availableContentStatuses.length === 0) {
|
|
62
|
+
if (this.userData.isAdmin) {
|
|
63
|
+
this.availableContentStatuses = [this.STATUS_DRAFT, this.STATUS_SCHEDULED, this.STATUS_PUBLISHED, this.STATUS_ARCHIVED, this.STATUS_UNLISTED];
|
|
64
|
+
} else if(this.isSingle){
|
|
65
|
+
this.availableContentStatuses = [this.STATUS_SCHEDULED, this.STATUS_PUBLISHED, this.STATUS_UNLISTED, this.STATUS_ARCHIVED];
|
|
66
|
+
} else{
|
|
67
|
+
this.availableContentStatuses = [this.STATUS_SCHEDULED, this.STATUS_PUBLISHED];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
// I'm not sure if I'm 100% on this logic, but this is my intepretation of the ContentRepository logic
|
|
53
72
|
if (this.getFutureScheduledContentsOnly && this.availableContentStatuses.includes(this.STATUS_SCHEDULED)) {
|
|
54
73
|
// we must pull in future content here, otherwise we'll restrict on content this is published in the past and remove any scheduled content
|
|
@@ -77,6 +96,7 @@ export class FilterBuilder {
|
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
_applyPublishingDateRestrictions() {
|
|
99
|
+
if(this.bypassPublishedDateRestriction) return this;
|
|
80
100
|
const now = new Date().toISOString();
|
|
81
101
|
if (this.getFutureContentOnly) {
|
|
82
102
|
this._andWhere(`published_on >= '${now}'`);
|
package/src/index.d.ts
CHANGED
|
@@ -40,12 +40,14 @@ import {
|
|
|
40
40
|
fetchChallengeLessonData,
|
|
41
41
|
fetchChallengeMetadata,
|
|
42
42
|
fetchChallengeUserActiveChallenges,
|
|
43
|
+
fetchCompletedChallenges,
|
|
43
44
|
fetchCompletedContent,
|
|
44
45
|
fetchCompletedState,
|
|
45
46
|
fetchContentInProgress,
|
|
46
47
|
fetchContentPageUserData,
|
|
47
48
|
fetchContentProgress,
|
|
48
49
|
fetchHandler,
|
|
50
|
+
fetchOwnedChallenges,
|
|
49
51
|
fetchPinnedPlaylists,
|
|
50
52
|
fetchPlaylist,
|
|
51
53
|
fetchPlaylistItem,
|
|
@@ -166,6 +168,7 @@ declare module 'musora-content-services' {
|
|
|
166
168
|
fetchChildren,
|
|
167
169
|
fetchCoachLessons,
|
|
168
170
|
fetchCommentModContentData,
|
|
171
|
+
fetchCompletedChallenges,
|
|
169
172
|
fetchCompletedContent,
|
|
170
173
|
fetchCompletedState,
|
|
171
174
|
fetchContentInProgress,
|
|
@@ -187,6 +190,7 @@ declare module 'musora-content-services' {
|
|
|
187
190
|
fetchMethods,
|
|
188
191
|
fetchNewReleases,
|
|
189
192
|
fetchNextPreviousLesson,
|
|
193
|
+
fetchOwnedChallenges,
|
|
190
194
|
fetchPackAll,
|
|
191
195
|
fetchPackChildren,
|
|
192
196
|
fetchPackData,
|
package/src/index.js
CHANGED
|
@@ -40,12 +40,14 @@ import {
|
|
|
40
40
|
fetchChallengeLessonData,
|
|
41
41
|
fetchChallengeMetadata,
|
|
42
42
|
fetchChallengeUserActiveChallenges,
|
|
43
|
+
fetchCompletedChallenges,
|
|
43
44
|
fetchCompletedContent,
|
|
44
45
|
fetchCompletedState,
|
|
45
46
|
fetchContentInProgress,
|
|
46
47
|
fetchContentPageUserData,
|
|
47
48
|
fetchContentProgress,
|
|
48
49
|
fetchHandler,
|
|
50
|
+
fetchOwnedChallenges,
|
|
49
51
|
fetchPinnedPlaylists,
|
|
50
52
|
fetchPlaylist,
|
|
51
53
|
fetchPlaylistItem,
|
|
@@ -165,6 +167,7 @@ export {
|
|
|
165
167
|
fetchChildren,
|
|
166
168
|
fetchCoachLessons,
|
|
167
169
|
fetchCommentModContentData,
|
|
170
|
+
fetchCompletedChallenges,
|
|
168
171
|
fetchCompletedContent,
|
|
169
172
|
fetchCompletedState,
|
|
170
173
|
fetchContentInProgress,
|
|
@@ -186,6 +189,7 @@ export {
|
|
|
186
189
|
fetchMethods,
|
|
187
190
|
fetchNewReleases,
|
|
188
191
|
fetchNextPreviousLesson,
|
|
192
|
+
fetchOwnedChallenges,
|
|
189
193
|
fetchPackAll,
|
|
190
194
|
fetchPackChildren,
|
|
191
195
|
fetchPackData,
|
|
@@ -336,6 +336,36 @@ export async function fetchChallengeLessonData(contentId) {
|
|
|
336
336
|
return await fetchHandler(url, 'get');
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Fetch all owned brand challenges for user
|
|
342
|
+
* @param {string|null} brand - brand
|
|
343
|
+
* @param {int} page - page of data to pull
|
|
344
|
+
* @param {int} limit - number of elements to pull
|
|
345
|
+
* @returns {Promise<any|null>}
|
|
346
|
+
*/
|
|
347
|
+
export async function fetchOwnedChallenges(brand = null, page, limit) {
|
|
348
|
+
let brandParam = brand ? `&brand=${brand}` : '';
|
|
349
|
+
let pageAndLimit = `?page=${page}&limit=${limit}`;
|
|
350
|
+
let url = `/challenges/tab_owned/get${pageAndLimit}${brandParam}`;
|
|
351
|
+
return await fetchHandler(url, 'get');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Fetch all completed brand challenges for user
|
|
356
|
+
* @param {string|null} brand - brand
|
|
357
|
+
* @param {int} page - page of data to pull
|
|
358
|
+
* @param {int} limit - number of elements to pull
|
|
359
|
+
* @returns {Promise<any|null>}
|
|
360
|
+
*/
|
|
361
|
+
export async function fetchCompletedChallenges(brand = null, page, limit) {
|
|
362
|
+
let brandParam = brand ? `&brand=${brand}` : '';
|
|
363
|
+
let pageAndLimit = `?page=${page}&limit=${limit}`;
|
|
364
|
+
let url = `/challenges/tab_completed/get${pageAndLimit}${brandParam}`;
|
|
365
|
+
return await fetchHandler(url, 'get');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
339
369
|
/**
|
|
340
370
|
* Fetch challenge, lesson, and user metadata for a given challenge
|
|
341
371
|
*
|
package/src/services/sanity.js
CHANGED
|
@@ -23,7 +23,12 @@ import {
|
|
|
23
23
|
|
|
24
24
|
import {globalConfig} from "./config";
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
fetchAllCompletedStates,
|
|
28
|
+
fetchCompletedChallenges,
|
|
29
|
+
fetchCurrentSongComplete,
|
|
30
|
+
fetchOwnedChallenges
|
|
31
|
+
} from './railcontent.js';
|
|
27
32
|
import {arrayToStringRepresentation, FilterBuilder} from "../filterBuilder";
|
|
28
33
|
import {fetchUserPermissions} from "./userPermissions";
|
|
29
34
|
import {getAllCompleted, getAllStarted, getAllStartedOrCompleted} from "./contentProgress";
|
|
@@ -33,7 +38,7 @@ import {getAllCompleted, getAllStarted, getAllStartedOrCompleted} from "./conten
|
|
|
33
38
|
*
|
|
34
39
|
* @type {string[]}
|
|
35
40
|
*/
|
|
36
|
-
const excludeFromGeneratedIndex = [];
|
|
41
|
+
const excludeFromGeneratedIndex = ['handleCustomFetchAll'];
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
44
|
* Fetch a song by its document ID from Sanity.
|
|
@@ -484,12 +489,28 @@ export async function fetchAll(brand, type, {
|
|
|
484
489
|
customFields = [],
|
|
485
490
|
progress = "all"
|
|
486
491
|
} = {}) {
|
|
492
|
+
let customResults = await handleCustomFetchAll(brand, type, {
|
|
493
|
+
page,
|
|
494
|
+
limit,
|
|
495
|
+
searchTerm,
|
|
496
|
+
sort,
|
|
497
|
+
includedFields,
|
|
498
|
+
groupBy,
|
|
499
|
+
progressIds,
|
|
500
|
+
useDefaultFields,
|
|
501
|
+
customFields,
|
|
502
|
+
progress});
|
|
503
|
+
if (customResults) {
|
|
504
|
+
return customResults;
|
|
505
|
+
}
|
|
506
|
+
|
|
487
507
|
let config = contentTypeConfig[type] ?? {};
|
|
488
508
|
let additionalFields = config?.fields ?? [];
|
|
489
509
|
let isGroupByOneToOne = (groupBy ? config?.relationships?.[groupBy]?.isOneToOne : false) ?? false;
|
|
490
510
|
let webUrlPathType = config?.slug ?? type;
|
|
491
511
|
const start = (page - 1) * limit;
|
|
492
512
|
const end = start + limit;
|
|
513
|
+
let bypassStatusAndPublishedValidation = (type == 'instructor');
|
|
493
514
|
|
|
494
515
|
// Construct the type filter
|
|
495
516
|
const typeFilter = type ? `&& _type == '${type}'` : "";
|
|
@@ -554,8 +575,10 @@ export async function fetchAll(brand, type, {
|
|
|
554
575
|
filter = `brand == "${brand}" ${typeFilter} ${searchFilter} ${includedFieldsFilter} ${progressFilter}`
|
|
555
576
|
entityFieldsString = fieldsString;
|
|
556
577
|
}
|
|
578
|
+
|
|
579
|
+
const filterWithRestrictions = await new FilterBuilder(filter,{bypassStatuses:bypassStatusAndPublishedValidation, bypassPermissions: bypassStatusAndPublishedValidation, bypassPublishedDateRestriction: bypassStatusAndPublishedValidation} ).buildFilter();
|
|
557
580
|
query = buildEntityAndTotalQuery(
|
|
558
|
-
|
|
581
|
+
filterWithRestrictions,
|
|
559
582
|
entityFieldsString,
|
|
560
583
|
{
|
|
561
584
|
sortOrder: sortOrder,
|
|
@@ -566,6 +589,45 @@ export async function fetchAll(brand, type, {
|
|
|
566
589
|
return fetchSanity(query, true);
|
|
567
590
|
}
|
|
568
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Fetch all content that requires custom handling or a distinct external call
|
|
594
|
+
* @param {string} brand - The brand for which to fetch content.
|
|
595
|
+
* @param {string} type - The content type to fetch (e.g., 'song', 'artist').
|
|
596
|
+
* @param {Object} params - Parameters for pagination, filtering, sorting, and grouping.
|
|
597
|
+
* @param {number} [params.page=1] - The page number for pagination.
|
|
598
|
+
* @param {number} [params.limit=10] - The number of items per page.
|
|
599
|
+
* @param {string} [params.searchTerm=""] - The search term to filter content by title or artist.
|
|
600
|
+
* @param {string} [params.sort="-published_on"] - The field to sort the content by.
|
|
601
|
+
* @param {Array<string>} [params.includedFields=[]] - The fields to include in the query.
|
|
602
|
+
* @param {string} [params.groupBy=""] - The field to group the results by (e.g., 'artist', 'genre').
|
|
603
|
+
* @param {Array<string>} [params.progressIds=undefined] - An array of railcontent IDs to filter the results by. Used for filtering by progress.
|
|
604
|
+
* @param {boolean} [params.useDefaultFields=true] - use the default sanity fields for content Type
|
|
605
|
+
* @param {Array<string>} [params.customFields=[]] - An array of sanity fields to include in the request
|
|
606
|
+
* @param {string} [params.progress="all"] - An string representing which progress filter to use ("all", "in progress", "complete", "not started").
|
|
607
|
+
* @returns {Promise<Object|null>} - The fetched content data or null if not found.
|
|
608
|
+
*/
|
|
609
|
+
async function handleCustomFetchAll(brand, type, {
|
|
610
|
+
page = 1,
|
|
611
|
+
limit = 10,
|
|
612
|
+
searchTerm = "",
|
|
613
|
+
sort = "-published_on",
|
|
614
|
+
includedFields = [],
|
|
615
|
+
groupBy = "",
|
|
616
|
+
progressIds = undefined,
|
|
617
|
+
useDefaultFields = true,
|
|
618
|
+
customFields = [],
|
|
619
|
+
progress = "all"
|
|
620
|
+
} = {}) {
|
|
621
|
+
if (type === 'challenge') {
|
|
622
|
+
if (groupBy === 'completed') {
|
|
623
|
+
return fetchCompletedChallenges(brand, page, limit);
|
|
624
|
+
} else if(groupBy === 'owned') {
|
|
625
|
+
return fetchOwnedChallenges(brand, page, limit);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
|
|
569
631
|
async function getProgressFilter(progress, progressIds) {
|
|
570
632
|
switch (progress) {
|
|
571
633
|
case "all":
|
|
@@ -931,7 +993,7 @@ export async function fetchNextPreviousLesson(railcontentId) {
|
|
|
931
993
|
* .catch(error => console.error(error));
|
|
932
994
|
*/
|
|
933
995
|
export async function fetchLessonContent(railContentId) {
|
|
934
|
-
const filterParams = {};
|
|
996
|
+
const filterParams = {isSingle:true};
|
|
935
997
|
// Format changes made to the `fields` object may also need to be reflected in Musora-web-platform SanityGateway.php $fields object
|
|
936
998
|
// Currently only for challenges and challenge lessons
|
|
937
999
|
// If you're unsure, message Adrian, or just add them.
|
|
@@ -630,13 +630,16 @@ describe('Filter Builder', function () {
|
|
|
630
630
|
let finalFilter = await builder.buildFilter(filter);
|
|
631
631
|
let clauses = spliceFilterForAnds(finalFilter);
|
|
632
632
|
expect(clauses[0].phrase).toBe(filter);
|
|
633
|
-
expect(clauses[1].field).toBe('
|
|
633
|
+
expect(clauses[1].field).toBe('status');
|
|
634
|
+
expect(clauses[2].field).toBe('published_on');
|
|
634
635
|
|
|
635
636
|
builder = new FilterBuilder('', {bypassPermissions: true});
|
|
636
637
|
finalFilter = await builder.buildFilter(filter);
|
|
637
638
|
clauses = spliceFilterForAnds(finalFilter);
|
|
638
|
-
expect(clauses[0].field).toBe('
|
|
639
|
-
expect(clauses[0].operator).toBe('
|
|
639
|
+
expect(clauses[0].field).toBe('status');
|
|
640
|
+
expect(clauses[0].operator).toBe('in');
|
|
641
|
+
expect(clauses[1].field).toBe('published_on');
|
|
642
|
+
expect(clauses[1].operator).toBe('<=');
|
|
640
643
|
});
|
|
641
644
|
|
|
642
645
|
test('withOnlyFilterAvailableStatuses', async () => {
|
|
@@ -665,8 +668,6 @@ describe('Filter Builder', function () {
|
|
|
665
668
|
expect(clauses[1].operator).toBe('in');
|
|
666
669
|
// getFutureScheduledContentsOnly doesn't make a filter that's splicable, so we match on the more static string
|
|
667
670
|
const expected = "['published','unlisted'] || (status == 'scheduled' && published_on >=";
|
|
668
|
-
console.log(clauses[1].condition);
|
|
669
|
-
console.log(expected)
|
|
670
671
|
const isMatch = finalFilter.includes(expected);
|
|
671
672
|
expect(isMatch).toBeTruthy();
|
|
672
673
|
});
|
|
@@ -701,7 +702,9 @@ describe('Filter Builder', function () {
|
|
|
701
702
|
expect(isMatch).toBeFalsy();
|
|
702
703
|
const clauses = spliceFilterForAnds(finalFilter);
|
|
703
704
|
expect(clauses[0].field).toBe('railcontent_id');
|
|
704
|
-
expect(clauses[1].field).toBe('
|
|
705
|
+
expect(clauses[1].field).toBe('status');
|
|
706
|
+
expect(clauses[2].field).toBe('published_on');
|
|
707
|
+
|
|
705
708
|
});
|
|
706
709
|
|
|
707
710
|
|
|
@@ -717,10 +720,11 @@ describe('Filter Builder', function () {
|
|
|
717
720
|
let finalFilter = await builder.buildFilter();
|
|
718
721
|
let clauses = spliceFilterForAnds(finalFilter);
|
|
719
722
|
expect(clauses[0].phrase).toBe(filter);
|
|
720
|
-
|
|
721
|
-
expect(clauses[1].
|
|
722
|
-
expect(clauses[
|
|
723
|
-
|
|
723
|
+
expect(clauses[1].field).toBe('status');
|
|
724
|
+
expect(clauses[1].operator).toBe('in');
|
|
725
|
+
expect(clauses[2].field).toBe('published_on');
|
|
726
|
+
expect(clauses[2].operator).toBe('<=');
|
|
727
|
+
const restrictionDate = new Date(clauses[2].condition)
|
|
724
728
|
const now = new Date();
|
|
725
729
|
expect(now.getTime()).toBeLessThan(restrictionDate.getTime());
|
|
726
730
|
|
|
@@ -732,8 +736,8 @@ describe('Filter Builder', function () {
|
|
|
732
736
|
finalFilter = await builder.buildFilter();
|
|
733
737
|
clauses = spliceFilterForAnds(finalFilter);
|
|
734
738
|
expect(clauses[0].phrase).toBe(filter);
|
|
735
|
-
expect(clauses[
|
|
736
|
-
expect(clauses[
|
|
739
|
+
expect(clauses[2].field).toBe('published_on');
|
|
740
|
+
expect(clauses[2].operator).toBe('>=');
|
|
737
741
|
});
|
|
738
742
|
|
|
739
743
|
function spliceFilterForAnds(filter) {
|