musora-content-services 2.185.1 → 2.186.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/.claude/settings.local.json +6 -1
- package/CHANGELOG.md +17 -0
- package/jest.config.js +0 -1
- package/package.json +1 -1
- package/src/index.d.ts +15 -2
- package/src/index.js +15 -2
- package/src/lib/ads/async-either.ts +148 -0
- package/src/lib/ads/coproduct.ts +36 -0
- package/src/lib/sanity/decorators/navigate-to.ts +20 -14
- package/src/lib/sanity/examples.ts +27 -31
- package/src/lib/sanity/filter.ts +62 -0
- package/src/lib/sanity/groq.ts +60 -19
- package/src/lib/sanity/runner.ts +3 -2
- package/src/lib/sanity/types/marketing.d.ts +33 -0
- package/src/services/awards/internal/award-definitions.js +4 -0
- package/src/services/content.js +181 -191
- package/src/services/contentProgress.js +19 -2
- package/src/services/endScreen/endScreen.ts +1 -1
- package/src/services/marketing/promotions.ts +178 -0
- package/src/services/my-path/placement-quiz.ts +1 -1
- package/src/services/progress/mutations.ts +28 -2
- package/src/services/recommendations.ts +307 -0
- package/src/services/sanity.js +1 -1
- package/src/services/sync/repositories/content-progress.ts +26 -5
- package/src/version-info.js +1 -1
- package/src/services/recommendations.js +0 -142
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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
|
+
### [2.186.1](https://github.com/railroadmedia/musora-content-services/compare/v2.186.0...v2.186.1) (2026-10-19)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* guard against null awards list when Sanity fetch fails ([#1053](https://github.com/railroadmedia/musora-content-services/issues/1053)) ([77f0bf7](https://github.com/railroadmedia/musora-content-services/commit/77f0bf711c2fcc5a5e47cb1a8d0328f670f5107d))
|
|
11
|
+
* preserve resume time when duplicating playlist progress ([#1045](https://github.com/railroadmedia/musora-content-services/issues/1045)) ([d62d394](https://github.com/railroadmedia/musora-content-services/commit/d62d394542b170741c818182405ca53efde046d1)), closes [#1079](https://github.com/railroadmedia/musora-content-services/issues/1079)
|
|
12
|
+
* replace @/ alias imports with relative paths in src ([bc22248](https://github.com/railroadmedia/musora-content-services/commit/bc22248264a029904c7f8bae403c257268da47c6))
|
|
13
|
+
|
|
14
|
+
## [2.186.0](https://github.com/railroadmedia/musora-content-services/compare/v2.185.1...v2.186.0) (2026-10-16)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **MU2-1384:** Port recommendations to the new sanity builders ([#1058](https://github.com/railroadmedia/musora-content-services/issues/1058)) ([0e2e41f](https://github.com/railroadmedia/musora-content-services/commit/0e2e41f86e26e16af18b894ab2472d54e3008022))
|
|
20
|
+
* **MU2-2025:** Add promotional campaign queries and claim state ([#1081](https://github.com/railroadmedia/musora-content-services/issues/1081)) ([921ab1e](https://github.com/railroadmedia/musora-content-services/commit/921ab1effee488253413fd42146377e986571e45))
|
|
21
|
+
|
|
5
22
|
### [2.185.1](https://github.com/railroadmedia/musora-content-services/compare/v2.185.0...v2.185.1) (2026-10-06)
|
|
6
23
|
|
|
7
24
|
## [2.185.0](https://github.com/railroadmedia/musora-content-services/compare/v2.184.1...v2.185.0) (2026-10-01)
|
package/jest.config.js
CHANGED
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -81,7 +81,6 @@ import {
|
|
|
81
81
|
getNewAndUpcoming,
|
|
82
82
|
getOwnedContent,
|
|
83
83
|
getRecent,
|
|
84
|
-
getRecommendedForYou,
|
|
85
84
|
getScheduleContentRows,
|
|
86
85
|
getTabResults
|
|
87
86
|
} from './services/content.js';
|
|
@@ -217,6 +216,14 @@ import {
|
|
|
217
216
|
fetchMarketingTestimonials
|
|
218
217
|
} from './services/marketing/marketing.ts';
|
|
219
218
|
|
|
219
|
+
import {
|
|
220
|
+
claimPromotionalCampaign,
|
|
221
|
+
dismissPromotionalCampaign,
|
|
222
|
+
fetchAvailablePromotionalCampaigns,
|
|
223
|
+
fetchPromotionalCampaigns,
|
|
224
|
+
fetchUserCampaignStates
|
|
225
|
+
} from './services/marketing/promotions.ts';
|
|
226
|
+
|
|
220
227
|
import {
|
|
221
228
|
acceptInvite,
|
|
222
229
|
createAccount,
|
|
@@ -351,10 +358,11 @@ import {
|
|
|
351
358
|
|
|
352
359
|
import {
|
|
353
360
|
fetchSimilarItems,
|
|
361
|
+
getRecommendedForYou,
|
|
354
362
|
rankCategories,
|
|
355
363
|
rankItems,
|
|
356
364
|
recommendations
|
|
357
|
-
} from './services/recommendations.
|
|
365
|
+
} from './services/recommendations.ts';
|
|
358
366
|
|
|
359
367
|
import {
|
|
360
368
|
getReportIssueOptions,
|
|
@@ -585,6 +593,7 @@ declare module 'musora-content-services' {
|
|
|
585
593
|
buildEntityAndTotalQuery,
|
|
586
594
|
buildImageSRC,
|
|
587
595
|
calculateLongestStreaks,
|
|
596
|
+
claimPromotionalCampaign,
|
|
588
597
|
clearAllCachedData,
|
|
589
598
|
clearState,
|
|
590
599
|
closeComment,
|
|
@@ -625,6 +634,7 @@ declare module 'musora-content-services' {
|
|
|
625
634
|
deleteProfilePicture,
|
|
626
635
|
deleteThread,
|
|
627
636
|
deleteUserActivity,
|
|
637
|
+
dismissPromotionalCampaign,
|
|
628
638
|
doesUserHaveMembership,
|
|
629
639
|
duplicatePlaylist,
|
|
630
640
|
editComment,
|
|
@@ -638,6 +648,7 @@ declare module 'musora-content-services' {
|
|
|
638
648
|
fetchArtistBySlug,
|
|
639
649
|
fetchArtistLessons,
|
|
640
650
|
fetchArtists,
|
|
651
|
+
fetchAvailablePromotionalCampaigns,
|
|
641
652
|
fetchBrandsByContentIds,
|
|
642
653
|
fetchByRailContentId,
|
|
643
654
|
fetchByRailContentIds,
|
|
@@ -704,6 +715,7 @@ declare module 'musora-content-services' {
|
|
|
704
715
|
fetchPost,
|
|
705
716
|
fetchPosts,
|
|
706
717
|
fetchPracticeGoals,
|
|
718
|
+
fetchPromotionalCampaigns,
|
|
707
719
|
fetchPublicAnnouncement,
|
|
708
720
|
fetchQuizAnswers,
|
|
709
721
|
fetchRecent,
|
|
@@ -732,6 +744,7 @@ declare module 'musora-content-services' {
|
|
|
732
744
|
fetchUninterests,
|
|
733
745
|
fetchUnreadCount,
|
|
734
746
|
fetchUpcomingEvents,
|
|
747
|
+
fetchUserCampaignStates,
|
|
735
748
|
fetchUserPermissions,
|
|
736
749
|
fetchUserPlaylists,
|
|
737
750
|
fetchUserPracticeMeta,
|
package/src/index.js
CHANGED
|
@@ -85,7 +85,6 @@ import {
|
|
|
85
85
|
getNewAndUpcoming,
|
|
86
86
|
getOwnedContent,
|
|
87
87
|
getRecent,
|
|
88
|
-
getRecommendedForYou,
|
|
89
88
|
getScheduleContentRows,
|
|
90
89
|
getTabResults
|
|
91
90
|
} from './services/content.js';
|
|
@@ -221,6 +220,14 @@ import {
|
|
|
221
220
|
fetchMarketingTestimonials
|
|
222
221
|
} from './services/marketing/marketing.ts';
|
|
223
222
|
|
|
223
|
+
import {
|
|
224
|
+
claimPromotionalCampaign,
|
|
225
|
+
dismissPromotionalCampaign,
|
|
226
|
+
fetchAvailablePromotionalCampaigns,
|
|
227
|
+
fetchPromotionalCampaigns,
|
|
228
|
+
fetchUserCampaignStates
|
|
229
|
+
} from './services/marketing/promotions.ts';
|
|
230
|
+
|
|
224
231
|
import {
|
|
225
232
|
acceptInvite,
|
|
226
233
|
createAccount,
|
|
@@ -355,10 +362,11 @@ import {
|
|
|
355
362
|
|
|
356
363
|
import {
|
|
357
364
|
fetchSimilarItems,
|
|
365
|
+
getRecommendedForYou,
|
|
358
366
|
rankCategories,
|
|
359
367
|
rankItems,
|
|
360
368
|
recommendations
|
|
361
|
-
} from './services/recommendations.
|
|
369
|
+
} from './services/recommendations.ts';
|
|
362
370
|
|
|
363
371
|
import {
|
|
364
372
|
getReportIssueOptions,
|
|
@@ -584,6 +592,7 @@ export {
|
|
|
584
592
|
buildEntityAndTotalQuery,
|
|
585
593
|
buildImageSRC,
|
|
586
594
|
calculateLongestStreaks,
|
|
595
|
+
claimPromotionalCampaign,
|
|
587
596
|
clearAllCachedData,
|
|
588
597
|
clearState,
|
|
589
598
|
closeComment,
|
|
@@ -624,6 +633,7 @@ export {
|
|
|
624
633
|
deleteProfilePicture,
|
|
625
634
|
deleteThread,
|
|
626
635
|
deleteUserActivity,
|
|
636
|
+
dismissPromotionalCampaign,
|
|
627
637
|
doesUserHaveMembership,
|
|
628
638
|
duplicatePlaylist,
|
|
629
639
|
editComment,
|
|
@@ -637,6 +647,7 @@ export {
|
|
|
637
647
|
fetchArtistBySlug,
|
|
638
648
|
fetchArtistLessons,
|
|
639
649
|
fetchArtists,
|
|
650
|
+
fetchAvailablePromotionalCampaigns,
|
|
640
651
|
fetchBrandsByContentIds,
|
|
641
652
|
fetchByRailContentId,
|
|
642
653
|
fetchByRailContentIds,
|
|
@@ -703,6 +714,7 @@ export {
|
|
|
703
714
|
fetchPost,
|
|
704
715
|
fetchPosts,
|
|
705
716
|
fetchPracticeGoals,
|
|
717
|
+
fetchPromotionalCampaigns,
|
|
706
718
|
fetchPublicAnnouncement,
|
|
707
719
|
fetchQuizAnswers,
|
|
708
720
|
fetchRecent,
|
|
@@ -731,6 +743,7 @@ export {
|
|
|
731
743
|
fetchUninterests,
|
|
732
744
|
fetchUnreadCount,
|
|
733
745
|
fetchUpcomingEvents,
|
|
746
|
+
fetchUserCampaignStates,
|
|
734
747
|
fetchUserPermissions,
|
|
735
748
|
fetchUserPlaylists,
|
|
736
749
|
fetchUserPracticeMeta,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Either } from './either'
|
|
2
|
+
|
|
3
|
+
const toError = (error: unknown, message?: string): Error =>
|
|
4
|
+
error instanceof Error
|
|
5
|
+
? error
|
|
6
|
+
: new Error(message ?? String(error ?? 'AsyncEither.attempt failed'))
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An {@link Either} that has not resolved yet, so a chain can keep going across asynchronous
|
|
10
|
+
* steps instead of dropping back into `.then()` at every one. Not a `TaskEither`: the work is
|
|
11
|
+
* already running when the AsyncEither is built.
|
|
12
|
+
* @example
|
|
13
|
+
* const lessons = await AsyncEither.of(run<Lesson[]>(groq))
|
|
14
|
+
* .map((lessons) => lessons ?? [])
|
|
15
|
+
* .mapAsync((lessons) => decorateNavigateTo(lessons))
|
|
16
|
+
* .ltap((error) => console.error(error.message))
|
|
17
|
+
* .recover([])
|
|
18
|
+
*/
|
|
19
|
+
export class AsyncEither<L, R> implements PromiseLike<Either<L, R>> {
|
|
20
|
+
private constructor(private readonly promise: Promise<Either<L, R>>) {}
|
|
21
|
+
|
|
22
|
+
/** @param promise - the pending Either to lift */
|
|
23
|
+
static of<L, R>(promise: Promise<Either<L, R>>): AsyncEither<L, R> {
|
|
24
|
+
return new AsyncEither(promise)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static attempt<R>(fn: () => Promise<R>, message?: string): AsyncEither<Error, R>
|
|
28
|
+
static attempt<R, L>(
|
|
29
|
+
fn: () => Promise<R>,
|
|
30
|
+
onError: (error: unknown) => L
|
|
31
|
+
): AsyncEither<L, R>
|
|
32
|
+
/**
|
|
33
|
+
* @param fn - the work to run; a rejection becomes a Left instead of propagating
|
|
34
|
+
* @param onError - a message used when `fn` rejects with a non-Error, or a function receiving
|
|
35
|
+
* the rejection untouched when the left has to carry more than a message
|
|
36
|
+
* @example
|
|
37
|
+
* AsyncEither.attempt(() => fetchStates(slugs), 'failed to fetch campaign state')
|
|
38
|
+
* @example
|
|
39
|
+
* AsyncEither.attempt(
|
|
40
|
+
* () => client.query(groq),
|
|
41
|
+
* (error) => new SanityQueryError('Sanity query failed', groq, error)
|
|
42
|
+
* )
|
|
43
|
+
*/
|
|
44
|
+
static attempt<R, L>(
|
|
45
|
+
fn: () => Promise<R>,
|
|
46
|
+
onError?: string | ((error: unknown) => L)
|
|
47
|
+
): AsyncEither<L | Error, R> {
|
|
48
|
+
const message = typeof onError === 'string' ? onError : undefined
|
|
49
|
+
const toLeft =
|
|
50
|
+
typeof onError === 'function' ? onError : (error: unknown) => toError(error, message)
|
|
51
|
+
|
|
52
|
+
return AsyncEither.of(
|
|
53
|
+
fn().then(
|
|
54
|
+
(value) => Either.right<L | Error, R>(value),
|
|
55
|
+
(error) => Either.left<L | Error, R>(toLeft(error))
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @param fn - maps the right value once it resolves */
|
|
61
|
+
map<T>(fn: (r: R) => T): AsyncEither<L, T> {
|
|
62
|
+
return AsyncEither.of(this.promise.then((either) => either.map(fn)))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param fn - maps the right value with an asynchronous function; a rejection propagates */
|
|
66
|
+
mapAsync<T>(fn: (r: R) => Promise<T>): AsyncEither<L, T> {
|
|
67
|
+
return AsyncEither.of(this.promise.then((either) => either.mapAsync(fn)))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param fn - maps the right value to another AsyncEither, flattening the result; its left type
|
|
72
|
+
* may differ, in which case the result carries the union of both
|
|
73
|
+
*/
|
|
74
|
+
flatMap<T, E = L>(fn: (r: R) => AsyncEither<E, T>): AsyncEither<L | E, T> {
|
|
75
|
+
return AsyncEither.of(
|
|
76
|
+
this.promise.then((either) =>
|
|
77
|
+
either.fold(
|
|
78
|
+
(l) => Promise.resolve(Either.left<L | E, T>(l)),
|
|
79
|
+
(r) => fn(r).promise as Promise<Either<L | E, T>>
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param fn - maps the left value once it resolves */
|
|
86
|
+
lmap<T>(fn: (l: L) => T): AsyncEither<T, R> {
|
|
87
|
+
return AsyncEither.of(this.promise.then((either) => either.lmap(fn)))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param fn - maps the left value to another AsyncEither, flattening the result */
|
|
91
|
+
lflatMap<T>(fn: (l: L) => AsyncEither<T, R>): AsyncEither<T, R> {
|
|
92
|
+
return AsyncEither.of(
|
|
93
|
+
this.promise.then((either) =>
|
|
94
|
+
either.fold(
|
|
95
|
+
(l) => fn(l).promise,
|
|
96
|
+
(r) => Promise.resolve(Either.right<T, R>(r))
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param fn - visits the right value without modifying it */
|
|
103
|
+
tap(fn: (r: R) => void): AsyncEither<L, R> {
|
|
104
|
+
return AsyncEither.of(this.promise.then((either) => either.tap(fn)))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param fn - visits the left value without modifying it */
|
|
108
|
+
ltap(fn: (l: L) => void): AsyncEither<L, R> {
|
|
109
|
+
return AsyncEither.of(this.promise.then((either) => either.ltap(fn)))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param onLeft - handles the left value
|
|
114
|
+
* @param onRight - handles the right value
|
|
115
|
+
* @returns {Promise<T | U>}
|
|
116
|
+
*/
|
|
117
|
+
fold<T, U>(onLeft: (l: L) => T, onRight: (r: R) => U): Promise<T | U> {
|
|
118
|
+
return this.promise.then((either) => either.fold(onLeft, onRight))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param defaultValue - returned when the resolved Either is a Left
|
|
123
|
+
* @returns {Promise<R>}
|
|
124
|
+
*/
|
|
125
|
+
recover(defaultValue: R): Promise<R> {
|
|
126
|
+
return this.promise.then((either) => either.recover(defaultValue))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param error - thrown instead of the left, or used as the message of the thrown Error
|
|
131
|
+
* @returns {Promise<R>}
|
|
132
|
+
*/
|
|
133
|
+
orFail(error?: string | Error): Promise<R> {
|
|
134
|
+
return this.promise.then((either) => either.orFail(error))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @returns {Promise<Either<L, R>>} */
|
|
138
|
+
unlift(): Promise<Either<L, R>> {
|
|
139
|
+
return this.promise
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
then<T = Either<L, R>, U = never>(
|
|
143
|
+
onfulfilled?: ((either: Either<L, R>) => T | PromiseLike<T>) | null,
|
|
144
|
+
onrejected?: ((reason: unknown) => U | PromiseLike<U>) | null
|
|
145
|
+
): Promise<T | U> {
|
|
146
|
+
return this.promise.then(onfulfilled, onrejected)
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/lib/ads/coproduct.ts
CHANGED
|
@@ -84,6 +84,42 @@ export abstract class Coproduct<L, R>
|
|
|
84
84
|
*/
|
|
85
85
|
abstract fold<T, U>(onLeft: (l: L) => T, onRight: (r: R) => U): T | U
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Unwraps the right value, throwing the left instead of returning it. Use at the edge of a
|
|
89
|
+
* chain where a failure has to become an exception.
|
|
90
|
+
* @param error - thrown instead of the left, or used as the message of the thrown Error
|
|
91
|
+
* @throws the left value when it is an `Error`, otherwise an `Error` wrapping it
|
|
92
|
+
* @example
|
|
93
|
+
* Coproduct.right(5).orFail() // 5
|
|
94
|
+
* Coproduct.left(new Error('bad')).orFail() // throws Error('bad')
|
|
95
|
+
* Coproduct.left('bad').orFail('error during request') // throws Error('error during request')
|
|
96
|
+
* Coproduct.left('bad').orFail(new HttpError(502)) // throws HttpError(502)
|
|
97
|
+
*/
|
|
98
|
+
orFail(error?: string | Error): R {
|
|
99
|
+
return this.fold(
|
|
100
|
+
(l) => {
|
|
101
|
+
throw Coproduct.toError(l, error)
|
|
102
|
+
},
|
|
103
|
+
(r) => r
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static toError<L>(value: L, error?: string | Error): Error {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
return error
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof error === 'string') {
|
|
113
|
+
return new Error(error)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (value instanceof Error) {
|
|
117
|
+
return value
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new Error(typeof value === 'string' ? value : 'Coproduct.orFail called on a Left')
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
/** Convenience {@link fold} that applies the same `fn` regardless of side. */
|
|
88
124
|
foldMap<T>(initial: T, fn: (acc: T, value: L | R) => T): T {
|
|
89
125
|
return this.fold(
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
CollectionParameter,
|
|
5
5
|
STATE,
|
|
6
6
|
} from '../../../services/sync/models/ContentProgress'
|
|
7
|
-
import { Progress } from '
|
|
7
|
+
import { Progress } from '../../../services/progress'
|
|
8
8
|
|
|
9
9
|
export const NAVIGATE_TO_FIELD = 'navigateTo' as const
|
|
10
10
|
|
|
@@ -15,9 +15,10 @@ const NAVIGABLE_TYPES = [
|
|
|
15
15
|
'song-tutorial',
|
|
16
16
|
COLLECTION_TYPE.LEARNING_PATH,
|
|
17
17
|
'skill-pack',
|
|
18
|
+
'documentary',
|
|
18
19
|
] as const
|
|
19
20
|
|
|
20
|
-
const COURSE_FLOW_TYPES = ['course', 'skill-pack', 'song-tutorial']
|
|
21
|
+
const COURSE_FLOW_TYPES = ['course', 'skill-pack', 'song-tutorial', 'documentary']
|
|
21
22
|
const GUIDED_FLOW_TYPES = ['guided-course', COLLECTION_TYPE.LEARNING_PATH]
|
|
22
23
|
const TWO_DEPTH_TYPES = ['course-collection']
|
|
23
24
|
|
|
@@ -46,11 +47,18 @@ export type WithNavigateTo<T extends NavigateToDecoratable> = T & {
|
|
|
46
47
|
navigateTo: NavigateTo | null
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
const definedChildrenOf = (content: NavigateToDecoratable): NavigateToDecoratable[] =>
|
|
51
|
+
(content.children ?? []).filter(Boolean)
|
|
52
|
+
|
|
49
53
|
function buildNavigateTo(
|
|
50
|
-
content: NavigateToDecoratable,
|
|
54
|
+
content: NavigateToDecoratable | null | undefined,
|
|
51
55
|
child: NavigateTo | null = null,
|
|
52
56
|
collection: NavigateTo['collection'] = null
|
|
53
|
-
): NavigateTo {
|
|
57
|
+
): NavigateTo | null {
|
|
58
|
+
if (!content) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
return {
|
|
55
63
|
id: content.id,
|
|
56
64
|
type: content.type,
|
|
@@ -72,10 +80,10 @@ async function prefetchStates(items: NavigateToDecoratable[]): Promise<NavigateC
|
|
|
72
80
|
for (const item of items) {
|
|
73
81
|
if (!item || !NAVIGABLE_TYPES.includes(item.type as (typeof NAVIGABLE_TYPES)[number])) continue
|
|
74
82
|
ids.add(item.id)
|
|
75
|
-
for (const child of item
|
|
83
|
+
for (const child of definedChildrenOf(item)) {
|
|
76
84
|
ids.add(child.id)
|
|
77
85
|
if (TWO_DEPTH_TYPES.includes(item.type)) {
|
|
78
|
-
for (const grandchild of child
|
|
86
|
+
for (const grandchild of definedChildrenOf(child)) {
|
|
79
87
|
ids.add(grandchild.id)
|
|
80
88
|
}
|
|
81
89
|
}
|
|
@@ -92,8 +100,8 @@ async function computeNavigateTo(
|
|
|
92
100
|
): Promise<NavigateTo | null> {
|
|
93
101
|
if (!NAVIGABLE_TYPES.includes(content.type as (typeof NAVIGABLE_TYPES)[number])) return null
|
|
94
102
|
|
|
95
|
-
const children = content
|
|
96
|
-
if (
|
|
103
|
+
const children = definedChildrenOf(content)
|
|
104
|
+
if (children.length === 0) return null
|
|
97
105
|
|
|
98
106
|
const contentState = ctx?.states.get(content.id) ?? (await Progress.state(content.id))
|
|
99
107
|
if (contentState !== STATE.STARTED) {
|
|
@@ -117,14 +125,12 @@ async function computeNavigateTo(
|
|
|
117
125
|
lastInteractedStatus === STATE.STARTED
|
|
118
126
|
? lastInteractedId
|
|
119
127
|
: Progress.incompleteLesson(childrenStates, content.type, lastInteractedId)
|
|
120
|
-
|
|
121
|
-
return target ? buildNavigateTo(target) : null
|
|
128
|
+
return buildNavigateTo(childrenById.get(targetId))
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
if (GUIDED_FLOW_TYPES.includes(content.type)) {
|
|
125
132
|
const targetId = Progress.incompleteLesson(childrenStates, content.type, lastInteractedId)
|
|
126
|
-
|
|
127
|
-
return target ? buildNavigateTo(target) : null
|
|
133
|
+
return buildNavigateTo(childrenById.get(targetId))
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
if (TWO_DEPTH_TYPES.includes(content.type)) {
|
|
@@ -147,10 +153,10 @@ export function navigateToDecorator(
|
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
|
|
150
|
-
export function decorateNavigateTo<T extends NavigateToDecoratable>(
|
|
156
|
+
export async function decorateNavigateTo<T extends NavigateToDecoratable>(
|
|
151
157
|
items: T[]
|
|
152
158
|
): Promise<WithNavigateTo<T>[]>
|
|
153
|
-
export function decorateNavigateTo<T extends NavigateToDecoratable>(
|
|
159
|
+
export async function decorateNavigateTo<T extends NavigateToDecoratable>(
|
|
154
160
|
items: T
|
|
155
161
|
): Promise<WithNavigateTo<T>>
|
|
156
162
|
export async function decorateNavigateTo<T extends NavigateToDecoratable>(
|
|
@@ -25,19 +25,20 @@ const publishedSongs = (brand: string) =>
|
|
|
25
25
|
f.combine(f.type('song'), f.statusIn(['published']), f.defined('railcontent_id'), f.brand(brand))
|
|
26
26
|
|
|
27
27
|
export async function fetchSongTitles(brand: string) {
|
|
28
|
-
const result = await groq()
|
|
28
|
+
const result = await groq<Song[]>()
|
|
29
29
|
.and(publishedSongs(brand))
|
|
30
30
|
.order('published_on desc')
|
|
31
31
|
.slice(0, 10)
|
|
32
32
|
.select('_id', 'title', 'railcontent_id')
|
|
33
|
-
.
|
|
33
|
+
.default([])
|
|
34
|
+
.run()
|
|
34
35
|
|
|
35
36
|
return result.fold(
|
|
36
37
|
(error) => {
|
|
37
38
|
console.error(`${error.name}: ${error.message}`, error.groq)
|
|
38
39
|
return []
|
|
39
40
|
},
|
|
40
|
-
(songs) => songs
|
|
41
|
+
(songs) => songs
|
|
41
42
|
)
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -105,27 +106,28 @@ interface Lesson {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export async function fetchLessonsWithoutPermissions(brand: string) {
|
|
108
|
-
const result = await groq().and(f.brand(brand)).slice(0, 10).
|
|
109
|
+
const result = await groq<Lesson[]>().and(f.brand(brand)).slice(0, 10).default([]).run()
|
|
109
110
|
|
|
110
111
|
return result
|
|
111
112
|
.map((lessons) =>
|
|
112
|
-
decorateAll<Lesson>(lessons
|
|
113
|
+
decorateAll<Lesson>(lessons, [pageTypeDecorator as FieldDecorator<Lesson>])
|
|
113
114
|
)
|
|
114
115
|
.recover([])
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
export async function fetchDecoratedLessons(brand: string) {
|
|
118
119
|
const [result, permissions] = await Promise.all([
|
|
119
|
-
groq()
|
|
120
|
+
groq<Lesson[]>()
|
|
120
121
|
.and(f.combine(f.typeIn(['course']), f.brand(brand)))
|
|
121
122
|
.slice(0, 10)
|
|
122
|
-
.
|
|
123
|
+
.default([])
|
|
124
|
+
.run(),
|
|
123
125
|
fetchUserPermissions(),
|
|
124
126
|
])
|
|
125
127
|
|
|
126
128
|
return result
|
|
127
129
|
.map((lessons) =>
|
|
128
|
-
decorateAll<Lesson>(lessons
|
|
130
|
+
decorateAll<Lesson>(lessons, [
|
|
129
131
|
accessDecorator(permissions) as FieldDecorator<Lesson>,
|
|
130
132
|
lifetimeUpgradeDecorator(permissions) as FieldDecorator<Lesson>,
|
|
131
133
|
pageTypeDecorator as FieldDecorator<Lesson>,
|
|
@@ -135,18 +137,15 @@ export async function fetchDecoratedLessons(brand: string) {
|
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
export async function fetchLessonsWithNavigation(brand: string) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
fetchUserPermissions(),
|
|
141
|
-
])
|
|
140
|
+
const lessons = groq<Lesson[]>().and(f.brand(brand)).slice(0, 10).default([]).run()
|
|
141
|
+
const permissions = await fetchUserPermissions()
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
return lessons
|
|
144
144
|
.map((lessons) =>
|
|
145
|
-
decorateAll<Lesson>(lessons
|
|
145
|
+
decorateAll<Lesson>(lessons, [accessDecorator(permissions) as FieldDecorator<Lesson>])
|
|
146
146
|
)
|
|
147
147
|
.mapAsync((lessons) => decorateNavigateTo(lessons) as Promise<Lesson[]>)
|
|
148
|
-
|
|
149
|
-
return decorated.recover([])
|
|
148
|
+
.recover([])
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
export async function fetchWithACustomDecorator(brand: string) {
|
|
@@ -155,9 +154,9 @@ export async function fetchWithACustomDecorator(brand: string) {
|
|
|
155
154
|
compute: (lesson) => lesson.permission_id === undefined,
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
const result = await groq().and(f.brand(brand)).
|
|
157
|
+
const result = await groq<Lesson[]>().and(f.brand(brand)).default([]).run()
|
|
159
158
|
|
|
160
|
-
return result.map((lessons) => decorateAll<Lesson>(lessons
|
|
159
|
+
return result.map((lessons) => decorateAll<Lesson>(lessons, [isFree])).recover([])
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
export async function fetchWhenPermissionsMayBeUnavailable(brand: string) {
|
|
@@ -167,7 +166,7 @@ export async function fetchWhenPermissionsMayBeUnavailable(brand: string) {
|
|
|
167
166
|
)
|
|
168
167
|
|
|
169
168
|
const [result, permissions] = await Promise.all([
|
|
170
|
-
groq().and(f.brand(brand)).
|
|
169
|
+
groq<Lesson[]>().and(f.brand(brand)).default([]).run(),
|
|
171
170
|
fetchUserPermissions()
|
|
172
171
|
.then(Either.right<SanityQueryError, UserPermissions>)
|
|
173
172
|
.catch(queryError),
|
|
@@ -176,7 +175,7 @@ export async function fetchWhenPermissionsMayBeUnavailable(brand: string) {
|
|
|
176
175
|
return result
|
|
177
176
|
.flatMap((lessons) =>
|
|
178
177
|
permissions.map((userPermissions) =>
|
|
179
|
-
decorateAll<Lesson>(lessons
|
|
178
|
+
decorateAll<Lesson>(lessons, [
|
|
180
179
|
accessDecorator(userPermissions) as FieldDecorator<Lesson>,
|
|
181
180
|
])
|
|
182
181
|
)
|
|
@@ -188,17 +187,14 @@ export async function fetchWhenPermissionsMayBeUnavailable(brand: string) {
|
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
export async function decorateManuallyForPreciseTypes(brand: string) {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
fetchUserPermissions(),
|
|
194
|
-
])
|
|
190
|
+
const lessons = groq<Lesson[]>().and(f.brand(brand)).slice(0, 10).default([]).run()
|
|
191
|
+
const permissions = await fetchUserPermissions()
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
.map((lessons) => decorateAccess(lessons
|
|
193
|
+
return lessons
|
|
194
|
+
.map((lessons) => decorateAccess(lessons, permissions))
|
|
198
195
|
.mapAsync((lessons) => decorateNavigateTo(lessons))
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
)
|
|
196
|
+
.fold(
|
|
197
|
+
() => [],
|
|
198
|
+
(lessons) => lessons.map((lesson) => [lesson.need_access, lesson.navigateTo] as const)
|
|
199
|
+
)
|
|
204
200
|
}
|