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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "1.0.177",
3
+ "version": "1.0.179",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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
  ],
@@ -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',
@@ -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 (this.availableContentStatuses.length === 0) return this;
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
  *
@@ -23,7 +23,12 @@ import {
23
23
 
24
24
  import {globalConfig} from "./config";
25
25
 
26
- import {fetchAllCompletedStates, fetchCurrentSongComplete} from './railcontent.js';
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
- filter,
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('published_on');
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('published_on');
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('published_on');
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].field).toBe('published_on');
722
- expect(clauses[1].operator).toBe('<=');
723
- const restrictionDate = new Date(clauses[1].condition)
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[1].field).toBe('published_on');
736
- expect(clauses[1].operator).toBe('>=');
739
+ expect(clauses[2].field).toBe('published_on');
740
+ expect(clauses[2].operator).toBe('>=');
737
741
  });
738
742
 
739
743
  function spliceFilterForAnds(filter) {