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.
@@ -20,6 +20,11 @@
20
20
  "asana",
21
21
  "nightwatch",
22
22
  "sentry",
23
- "customerio"
23
+ "customerio",
24
+ "vercel",
25
+ "cloudflare"
26
+ ],
27
+ "disabledMcpjsonServers": [
28
+ "mysql-archive"
24
29
  ]
25
30
  }
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
@@ -28,7 +28,6 @@ export default {
28
28
  'src/**/*.{js,ts}',
29
29
  '!src/services/sanity.js',
30
30
  '!src/services/railcontent.js',
31
- '!src/services/recommendations.js',
32
31
  '!src/index.js',
33
32
  '!src/index.d.ts',
34
33
  '!src/services/user/account.ts',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.185.1",
3
+ "version": "2.186.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
@@ -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.js';
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.js';
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
+ }
@@ -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 '@/services/progress'
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.children ?? []) {
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.children ?? []) {
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.children
96
- if (!children || children.length === 0) return null
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
- const target = childrenById.get(targetId)
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
- const target = childrenById.get(targetId)
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
- .run<Song[]>()
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).run<Lesson[]>()
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 ?? [], [pageTypeDecorator as FieldDecorator<Lesson>])
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
- .run<Lesson[]>(),
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 [result, permissions] = await Promise.all([
139
- groq().and(f.brand(brand)).slice(0, 10).run<Lesson[]>(),
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
- const decorated = await result
143
+ return lessons
144
144
  .map((lessons) =>
145
- decorateAll<Lesson>(lessons ?? [], [accessDecorator(permissions) as FieldDecorator<Lesson>])
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)).run<Lesson[]>()
157
+ const result = await groq<Lesson[]>().and(f.brand(brand)).default([]).run()
159
158
 
160
- return result.map((lessons) => decorateAll<Lesson>(lessons ?? [], [isFree])).recover([])
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)).run<Lesson[]>(),
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 [result, permissions] = await Promise.all([
192
- groq().and(f.brand(brand)).slice(0, 10).run<Lesson[]>(),
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
- const decorated = await result
197
- .map((lessons) => decorateAccess(lessons ?? [], permissions))
193
+ return lessons
194
+ .map((lessons) => decorateAccess(lessons, permissions))
198
195
  .mapAsync((lessons) => decorateNavigateTo(lessons))
199
-
200
- return decorated.fold(
201
- () => [],
202
- (lessons) => lessons.map((lesson) => [lesson.need_access, lesson.navigateTo] as const)
203
- )
196
+ .fold(
197
+ () => [],
198
+ (lessons) => lessons.map((lesson) => [lesson.need_access, lesson.navigateTo] as const)
199
+ )
204
200
  }