musora-content-services 2.121.2 → 2.122.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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(rg:*)",
5
+ "Bash(npm run lint:*)"
6
+ ],
7
+ "deny": []
8
+ }
9
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
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.122.1](https://github.com/railroadmedia/musora-content-services/compare/v2.122.0...v2.122.1) (2026-01-23)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * handle global live streams ([#727](https://github.com/railroadmedia/musora-content-services/issues/727)) ([e0ed5f3](https://github.com/railroadmedia/musora-content-services/commit/e0ed5f398f61a2188beae56a05547494862a16b7))
11
+ * removes tentative progress call functionality ([#716](https://github.com/railroadmedia/musora-content-services/issues/716)) ([a167d3b](https://github.com/railroadmedia/musora-content-services/commit/a167d3b94c1d2ef6df9a7f76e4c4ea4c71f9bfb5))
12
+ * **T3PS-1562:** hide nonpinned method card ([#721](https://github.com/railroadmedia/musora-content-services/issues/721)) ([cc250ee](https://github.com/railroadmedia/musora-content-services/commit/cc250ee072cac976fc7389eb86e77eff1230b73a))
13
+ * **T3PS-1579:** fix reset bubbling ([#718](https://github.com/railroadmedia/musora-content-services/issues/718)) ([a9490a8](https://github.com/railroadmedia/musora-content-services/commit/a9490a85aeb4df947770a265543fa6782e30b436))
14
+
15
+ ## [2.122.0](https://github.com/railroadmedia/musora-content-services/compare/v2.118.1...v2.122.0) (2026-01-22)
16
+
17
+
18
+ ### Features
19
+
20
+ * add fetchLiveStreamData method ([aa9b704](https://github.com/railroadmedia/musora-content-services/commit/aa9b7046c7fc69e8b30c0f035577da62f8c5b5fd))
21
+ * add vimeo_live_event_id to GROQ queries ([d4fff6f](https://github.com/railroadmedia/musora-content-services/commit/d4fff6f09ca62682f6cead7e43f6979a56b86e0a))
22
+ * adds restore methods (for progress deletion undo) ([#705](https://github.com/railroadmedia/musora-content-services/issues/705)) ([b01a718](https://github.com/railroadmedia/musora-content-services/commit/b01a71892175b38789d62fffac0ce5f8e2e4513d))
23
+ * **TP-1060:** method data caching in mcs ([#701](https://github.com/railroadmedia/musora-content-services/issues/701)) ([25efc43](https://github.com/railroadmedia/musora-content-services/commit/25efc43b6c07172db2f20d51d5b79ac9fc204eef))
24
+ * update brand endpoint ([#719](https://github.com/railroadmedia/musora-content-services/issues/719)) ([4938b61](https://github.com/railroadmedia/musora-content-services/commit/4938b61c8526e31194b284c6daf172764b7900ed))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * adds live_global_event field where live_event_stream_id is returned ([#708](https://github.com/railroadmedia/musora-content-services/issues/708)) ([23c58f4](https://github.com/railroadmedia/musora-content-services/commit/23c58f42da866cd1035276c62c773b82c09d3aa5))
30
+ * better logs in Sentry for debugging ([#712](https://github.com/railroadmedia/musora-content-services/issues/712)) ([e1019b5](https://github.com/railroadmedia/musora-content-services/commit/e1019b50b0cf606b86e71028db0a2f9ac354cfca))
31
+ * remove unsupported collection types ([#713](https://github.com/railroadmedia/musora-content-services/issues/713)) ([0441941](https://github.com/railroadmedia/musora-content-services/commit/04419413ffea82303258b839c8e1e11b3a4c507a))
32
+ * **T3PS-1347:** Implement WatermelonDB-based streak calculator with year boundary fix ([916a8ef](https://github.com/railroadmedia/musora-content-services/commit/916a8ef532a858f82a8133d1725ad69ea3d550e9))
33
+
5
34
  ### [2.121.2](https://github.com/railroadmedia/musora-content-services/compare/v2.121.1...v2.121.2) (2026-01-20)
6
35
 
7
36
  ### [2.121.1](https://github.com/railroadmedia/musora-content-services/compare/v2.121.0...v2.121.1) (2026-01-20)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.121.2",
3
+ "version": "2.122.1",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -128,6 +128,7 @@ export class Tabs {
128
128
  static SingleLessons = { name: 'Single Lessons', short_name: 'Single Lessons', value: 'type,Single Lessons', recSysSection: 'lesson', }
129
129
  static SkillPacks = { name: 'Skill Packs', short_name: 'Skill Packs', value: 'type,Skill Packs', recSysSection: 'lesson', }
130
130
  static Entertainment = { name: 'Entertainment', short_name: 'Entertainment', value: 'type,Entertainment', recSysSection: 'lesson', }
131
+ static Routines = { name: 'Routines', short_name: 'Routines', value: 'type,routine', recSysSection: 'lesson', }
131
132
  }
132
133
 
133
134
  /**
@@ -325,6 +326,41 @@ const contentMetadata = {
325
326
  },
326
327
  singeo: {
327
328
  'songs-types': ['Tutorials', 'Sheet Music', 'Play-Alongs', 'Jam Tracks'],
329
+ lessons: {
330
+ name: 'Lessons',
331
+ filterOptions: {
332
+ difficulty: DIFFICULTY_STRINGS,
333
+ length: LengthFilterOptions.AllOptions,
334
+ style: [
335
+ 'Country/Folk',
336
+ 'Funk/Disco',
337
+ 'Hard Rock/Metal',
338
+ 'Hip-Hop/Rap/EDM',
339
+ 'Holiday/Soundtrack',
340
+ 'Jazz/Blues',
341
+ 'Latin/World',
342
+ 'Pop/Rock',
343
+ 'R&B/Soul',
344
+ 'Worship/Gospel',
345
+ ],
346
+ type: LESSON_TYPE_FILTER,
347
+ progress: PROGRESS_NAMES,
348
+ },
349
+ sortingOptions: {
350
+ title: 'Sort By',
351
+ type: 'radio',
352
+ items: SortingOptions.AllSortingOptions,
353
+ },
354
+ tabs: [
355
+ Tabs.ForYou,
356
+ Tabs.SingleLessons,
357
+ Tabs.Courses,
358
+ Tabs.SkillPacks,
359
+ Tabs.Routines,
360
+ Tabs.Entertainment,
361
+ Tabs.ExploreAll,
362
+ ],
363
+ },
328
364
  },
329
365
  }
330
366
 
@@ -50,7 +50,7 @@ export const DEFAULT_FIELDS = [
50
50
  "'image': thumbnail.asset->url",
51
51
  "'thumbnail': thumbnail.asset->url",
52
52
  'difficulty',
53
- 'difficulty_string',
53
+ difficultyStringField(),
54
54
  'published_on',
55
55
  "'type': _type",
56
56
  "'length_in_seconds' : coalesce(length_in_seconds, soundslice[0].soundslice_length_in_second)",
@@ -132,6 +132,24 @@ export const assignmentsField = `"assignments":assignment[]{
132
132
  "description": coalesce(assignment_description,'')
133
133
  },`
134
134
 
135
+ // todo: refactor live event queries to use this
136
+ export const liveFields = `
137
+ 'slug': slug.current,
138
+ 'id': railcontent_id,
139
+ title,
140
+ live_event_start_time,
141
+ live_event_end_time,
142
+ live_event_stream_id,
143
+ "live_event_is_global": live_global_event == true,
144
+ published_on,
145
+ "thumbnail": thumbnail.asset->url,
146
+ ${artistOrInstructorName()},
147
+ difficulty_string,
148
+ railcontent_id,
149
+ "instructors": ${instructorField},
150
+ 'videoId': coalesce(live_event_stream_id, video.external_id)
151
+ `
152
+
135
153
  const contentWithInstructorsField = {
136
154
  fields: ['"instructors": instructor[]->name'],
137
155
  }
@@ -224,7 +242,7 @@ export const collectionLessonTypes = [...coursesLessonTypes, ...showsLessonTypes
224
242
 
225
243
  export const lessonTypesMapping = {
226
244
  lessons: singleLessonTypes,
227
- 'practice alongs': practiceAlongsLessonTypes,
245
+ 'practice alongs': [ ...practiceAlongsLessonTypes, 'routine'],
228
246
  'live archives': liveArchivesLessonTypes,
229
247
  performances: performancesLessonTypes,
230
248
  'student archives': studentArchivesLessonTypes,
@@ -250,6 +268,7 @@ export const lessonTypesMapping = {
250
268
  ...studentArchivesLessonTypes,
251
269
  ...practiceAlongsLessonTypes,
252
270
  ],
271
+ routines: ['routine']
253
272
  }
254
273
 
255
274
  export const getNextLessonLessonParentTypes = [
@@ -270,7 +289,8 @@ export const progressTypesMapping = {
270
289
  ...studentArchivesLessonTypes,
271
290
  'documentary-lesson',
272
291
  'live',
273
- 'course-lesson'
292
+ 'course-lesson',
293
+ 'routine'
274
294
  ],
275
295
  course: ['course'],
276
296
  show: showsLessonTypes,
@@ -302,6 +322,7 @@ export const filterTypes = {
302
322
  ...coursesLessonTypes,
303
323
  ...skillLessonTypes,
304
324
  ...entertainmentLessonTypes,
325
+ 'routine'
305
326
  ],
306
327
  songs: [
307
328
  ...tutorialsLessonTypes,
@@ -314,9 +335,12 @@ export const filterTypes = {
314
335
  export const recentTypes = {
315
336
  lessons: [
316
337
  ...individualLessonsTypes,
338
+ ...skillLessonTypes,
339
+ ...entertainmentLessonTypes,
317
340
  'course-lesson',
318
341
  'guided-course-lesson',
319
342
  'quick-tips',
343
+ 'routine'
320
344
  ],
321
345
  songs: [...SONG_TYPES],
322
346
  home: [
@@ -331,6 +355,7 @@ export const recentTypes = {
331
355
  'live',
332
356
  'course',
333
357
  'course-collection',
358
+ 'routine'
334
359
  ],
335
360
  }
336
361
 
@@ -623,9 +648,9 @@ export function getIntroVideoFields(type) {
623
648
  `"id": railcontent_id`,
624
649
  'title',
625
650
  'brand',
626
- `"instructor": *[_type == "method-v2" && brand == ^.brand && references(^._id)][0].${instructorField}`,
627
- `"difficulty": *[_type == "method-v2" && brand == ^.brand && references(^._id)][0].difficulty`,
628
- `"difficulty_string": *[_type == "method-v2" && brand == ^.brand][0].difficulty_string`,
651
+ `"instructor": ${instructorField}`,
652
+ `difficulty`,
653
+ `difficulty_string`,
629
654
  `"type": _type`,
630
655
  'brand',
631
656
  `"description": ${descriptionField}`,
@@ -666,7 +691,7 @@ export function getNewReleasesTypes(brand) {
666
691
  'quick-tips',
667
692
  'workout',
668
693
  'podcasts',
669
- 'pack',
694
+ 'course-collection',
670
695
  'song',
671
696
  'play-along',
672
697
  'course',
@@ -703,7 +728,6 @@ export function getUpcomingEventsTypes(brand) {
703
728
  'boot-camp',
704
729
  'quick-tips',
705
730
  'recording',
706
- 'pack-bundle-lesson',
707
731
  ]
708
732
  switch (brand) {
709
733
  case 'drumeo':
@@ -732,6 +756,10 @@ export function artistOrInstructorNameAsArray(key = 'artists') {
732
756
  return `'${key}': select(artist->name != null => [artist->name], instructor[]->name)`
733
757
  }
734
758
 
759
+ export function difficultyStringField(key = 'difficulty_string') {
760
+ return `'${key}': select(difficulty_string == 'Novice' => 'Introductory', difficulty_string)`
761
+ }
762
+
735
763
  export async function getFieldsForContentTypeWithFilteredChildren(
736
764
  contentType,
737
765
  asQueryString = true
@@ -531,9 +531,8 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
531
531
  }
532
532
  }
533
533
 
534
- if (Object.keys(bubbledProgresses).length >= 0) {
535
- // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
536
- await db.contentProgress.recordProgressMany(bubbledProgresses, collection, {tentative: !isLP, skipPush: true, fromLearningPath})
534
+ if (Object.keys(bubbledProgresses).length > 0) {
535
+ await db.contentProgress.recordProgressMany(bubbledProgresses, collection, {skipPush: true, fromLearningPath})
537
536
  }
538
537
 
539
538
  if (isLP) {
@@ -567,8 +566,7 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {
567
566
  ...trickleProgress(hierarchy, contentId, collection, progress),
568
567
  ...await bubbleProgress(hierarchy, contentId, collection)
569
568
  }
570
- // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
571
- await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
569
+ await db.contentProgress.recordProgressMany(progresses, collection, {skipPush: true})
572
570
 
573
571
  if (isLP) {
574
572
  let exportProgresses = progresses
@@ -600,7 +598,7 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
600
598
  }
601
599
 
602
600
  const contents = Object.fromEntries(contentIds.map((id) => [id, progress]))
603
- const response = await db.contentProgress.recordProgressMany(contents, collection, {tentative: !isLP, skipPush: true})
601
+ const response = await db.contentProgress.recordProgressMany(contents, collection, {skipPush: true})
604
602
 
605
603
  // we assume this is used only for contents within the same hierarchy
606
604
  const hierarchy = await getHierarchy(collection.id, collection)
@@ -613,8 +611,7 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
613
611
  ...(await bubbleProgress(hierarchy, contentId, collection)),
614
612
  }
615
613
  }
616
- // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
617
- await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
614
+ await db.contentProgress.recordProgressMany(progresses, collection, {skipPush: true})
618
615
 
619
616
  if (isLP) {
620
617
  let exportProgresses = progresses
@@ -647,8 +644,21 @@ async function resetStatus(contentId, collection = null, {skipPush = false} = {}
647
644
  ...trickleProgress(hierarchy, contentId, collection, progress),
648
645
  ...await bubbleProgress(hierarchy, contentId, collection)
649
646
  }
650
- // BE bubbling/trickling currently does not work, so we utilize non-tentative pushing when learning path collection
651
- await db.contentProgress.recordProgressMany(progresses, collection, {tentative: !isLP, skipPush: true})
647
+ // have to use different endpoints for erase vs record
648
+ const eraseProgresses = Object.fromEntries(
649
+ Object.entries(progresses).filter(([_, pct]) => pct === 0)
650
+ )
651
+ progresses = Object.fromEntries(
652
+ Object.entries(progresses).filter(([_, pct]) => pct > 0)
653
+ )
654
+
655
+ if (Object.keys(progresses).length > 0) {
656
+ await db.contentProgress.recordProgressMany(progresses, collection, {skipPush: true, fromLearningPath: isLP})
657
+ }
658
+ if (Object.keys(eraseProgresses).length > 0) {
659
+ const eraseIds = Object.keys(eraseProgresses).map(Number)
660
+ await db.contentProgress.eraseProgressMany(eraseIds, collection, {skipPush: true})
661
+ }
652
662
 
653
663
  if (isLP) {
654
664
  progresses[contentId] = progress
@@ -178,12 +178,16 @@ function sortCards(pinnedCard, contentCardMap, playlistCards, methodCard, limit)
178
178
  combined.push(pinnedCard)
179
179
  }
180
180
 
181
- if (!(pinnedCard && pinnedCard.progressType === 'method')) {
181
+ const progressList = Array.from(contentCardMap.values())
182
+
183
+ combined = [...combined, ...progressList, ...playlistCards]
184
+
185
+ // welcome card state will only show if pinned
186
+ if (methodCard.type !== 'method') {
182
187
  combined.push(methodCard)
183
188
  }
184
189
 
185
- const progressList = Array.from(contentCardMap.values())
186
- return mergeAndSortItems([...combined, ...progressList, ...playlistCards], limit)
190
+ return mergeAndSortItems(combined, limit)
187
191
  }
188
192
 
189
193
  function mergeAndSortItems(items, limit) {
@@ -111,7 +111,7 @@ export async function getMethodCard(brand) {
111
111
  }
112
112
 
113
113
  return {
114
- id: learningPath?.id,
114
+ id: 1,
115
115
  type: COLLECTION_TYPE.LEARNING_PATH,
116
116
  progressType: 'method',
117
117
  header: 'Method',
@@ -9,6 +9,7 @@ import {
9
9
  contentTypeConfig,
10
10
  DEFAULT_FIELDS,
11
11
  descriptionField,
12
+ difficultyStringField,
12
13
  filtersToGroq,
13
14
  getChildFieldsForContentType,
14
15
  getFieldsForContentType,
@@ -31,6 +32,7 @@ import {
31
32
  showsTypes,
32
33
  SONG_TYPES,
33
34
  SONG_TYPES_WITH_CHILDREN,
35
+ liveFields,
34
36
  } from '../contentTypeConfig.js'
35
37
  import { fetchSimilarItems, recommendations } from './recommendations.js'
36
38
  import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS } from '../contentMetaData.js'
@@ -330,7 +332,7 @@ export async function fetchNewReleases(
330
332
  "instructor": ${instructorField},
331
333
  "artists": instructor[]->name,
332
334
  difficulty,
333
- difficulty_string,
335
+ ${difficultyStringField()},
334
336
  length_in_seconds,
335
337
  published_on,
336
338
  "type": _type,
@@ -368,7 +370,7 @@ export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {})
368
370
  "artists": instructor[]->name,
369
371
  "instructor": ${instructorField},
370
372
  difficulty,
371
- difficulty_string,
373
+ ${difficultyStringField()},
372
374
  length_in_seconds,
373
375
  published_on,
374
376
  "type": _type,
@@ -421,7 +423,7 @@ export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
421
423
  "instructor": ${instructorField},
422
424
  "artists": instructor[]->name,
423
425
  difficulty,
424
- difficulty_string,
426
+ ${difficultyStringField()},
425
427
  length_in_seconds,
426
428
  published_on,
427
429
  "type": _type,
@@ -961,9 +963,9 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
961
963
  ${parentQuery}
962
964
  ...select(
963
965
  defined(live_event_start_time) => {
964
- "live_event_start_time": live_event_start_time,
965
- "live_event_end_time": live_event_end_time,
966
- "live_event_stream_id": live_event_stream_id,
966
+ live_event_start_time,
967
+ live_event_end_time,
968
+ live_event_stream_id,
967
969
  "vimeo_live_event_id": vimeo_live_event_id,
968
970
  "videoId": coalesce(live_event_stream_id, video.external_id),
969
971
  "live_event_is_global": live_global_event == true
@@ -1096,7 +1098,7 @@ export async function fetchSiblingContent(railContentId, brand = null) {
1096
1098
  }).buildFilter()
1097
1099
 
1098
1100
  const brandString = brand ? ` && brand == "${brand}"` : ''
1099
- const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, artist->, "permission_id": permission_v2, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1101
+ const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, ${difficultyStringField()}, artist->, "permission_id": permission_v2, "genre": genre[]->name, "parent_id": parent_content_data[0].id`
1100
1102
 
1101
1103
  const query = `*[railcontent_id == ${railContentId}${brandString}]{
1102
1104
  _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
@@ -1143,7 +1145,7 @@ export async function fetchRelatedLessons(railContentId) {
1143
1145
  { showMembershipRestrictedContent: true }
1144
1146
  ).buildFilter()
1145
1147
 
1146
- const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission_v2,_type, "genre": genre[]->name`
1148
+ const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, ${difficultyStringField()}, railcontent_id, artist->,"permission_id": permission_v2,_type, "genre": genre[]->name`
1147
1149
 
1148
1150
  const query = `*[railcontent_id == ${railContentId} && (!defined(permission) || references(*[_type=='permission']._id))]{
1149
1151
  _type, parent_type, railcontent_id,
@@ -1184,50 +1186,21 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1184
1186
  )
1185
1187
  endDateTemp = new Date(endDateTemp.setMinutes(endDateTemp.getMinutes() - LIVE_EXTRA_MINUTES))
1186
1188
 
1187
- // See LiveStreamEventService.getCurrentOrNextLiveEvent for some nice complicated logic which I don't think is actually importart
1188
- // this has some +- on times
1189
- // But this query just finds the first scheduled event (sorted by start_time) that ends after now()
1190
- const query =
1189
+ const liveEventFields = liveFields + `, 'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}')`
1190
+
1191
+ const baseFilter =
1191
1192
  forcedContentId !== null
1192
- ? `*[railcontent_id == ${forcedContentId} ]{
1193
- 'slug': slug.current,
1194
- 'id': railcontent_id,
1195
- live_event_start_time,
1196
- live_event_end_time,
1197
- live_event_stream_id,
1198
- vimeo_live_event_id,
1199
- railcontent_id,
1200
- published_on,
1201
- 'event_coach_url' : instructor[0]->web_url_path,
1202
- 'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}'),
1203
- title,
1204
- "thumbnail": thumbnail.asset->url,
1205
- ${artistOrInstructorName()},
1206
- difficulty_string,
1207
- "instructors": ${instructorField},
1208
- 'videoId': coalesce(live_event_stream_id, video.external_id),
1209
- } | order(live_event_start_time)[0...1]`
1210
- : `*[status == 'scheduled' && brand == '${brand}' && defined(live_event_start_time) && live_event_start_time <= '${getSanityDate(startDateTemp, false)}' && live_event_end_time >= '${getSanityDate(endDateTemp, false)}']{
1211
- 'slug': slug.current,
1212
- 'id': railcontent_id,
1213
- live_event_start_time,
1214
- live_event_end_time,
1215
- live_event_stream_id,
1216
- vimeo_live_event_id,
1217
- railcontent_id,
1218
- published_on,
1219
- 'event_coach_url' : instructor[0]->web_url_path,
1220
- 'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}'),
1221
- title,
1222
- "thumbnail": thumbnail.asset->url,
1223
- ${artistOrInstructorName()},
1224
- difficulty_string,
1225
- "instructors": instructor[]->{
1226
- name,
1227
- web_url_path,
1228
- },
1229
- 'videoId': coalesce(live_event_stream_id, video.external_id),
1230
- } | order(live_event_start_time)[0...1]`
1193
+ ? `railcontent_id == ${forcedContentId}`
1194
+ : `status == 'scheduled'
1195
+ && (brand == '${brand}' || live_global_event == true)
1196
+ && defined(live_event_start_time)
1197
+ && live_event_start_time <= '${getSanityDate(startDateTemp, false)}'
1198
+ && live_event_end_time >= '${getSanityDate(endDateTemp, false)}'`
1199
+
1200
+ const filter = await new FilterBuilder(baseFilter, {bypassPermissions: true}).buildFilter()
1201
+
1202
+ // This query finds the first scheduled event (sorted by start_time) that ends after now()
1203
+ const query = `*[${filter}]{${liveEventFields}} | order(live_event_start_time)[0...1]`
1231
1204
 
1232
1205
  return await fetchSanity(query, false, { processNeedAccess: false })
1233
1206
  }
@@ -2001,7 +1974,7 @@ export async function fetchScheduledAndNewReleases(
2001
1974
  ${artistOrInstructorName()},
2002
1975
  "artists": instructor[]->name,
2003
1976
  difficulty,
2004
- difficulty_string,
1977
+ ${difficultyStringField()},
2005
1978
  length_in_seconds,
2006
1979
  published_on,
2007
1980
  "type": _type,
@@ -100,13 +100,6 @@ export default class SyncRepository<TModel extends BaseModel> {
100
100
  )
101
101
  }
102
102
 
103
- protected async upsertOneTentative(id: RecordId, builder: (record: TModel) => void) {
104
- return this.store.telemetry.trace(
105
- { name: `upsertOneTentative:${this.store.model.table}`, op: 'upsert', attributes: { ...this.context.session.toJSON() } },
106
- (span) => this._respondToWrite(() => this.store.upsertOneTentative(id, builder, span), span)
107
- )
108
- }
109
-
110
103
  protected async upsertSome(builders: Record<RecordId, (record: TModel) => void>, { skipPush = false } = {}) {
111
104
  return this.store.telemetry.trace(
112
105
  { name: `upsertSome:${this.store.model.table}`, op: 'upsert', attributes: { ...this.context.session.toJSON() } },
@@ -114,13 +107,6 @@ export default class SyncRepository<TModel extends BaseModel> {
114
107
  )
115
108
  }
116
109
 
117
- protected async upsertSomeTentative(builders: Record<RecordId, (record: TModel) => void>, { skipPush = false } = {}) {
118
- return this.store.telemetry.trace(
119
- { name: `upsertSomeTentative:${this.store.model.table}`, op: 'upsert', attributes: { ...this.context.session.toJSON() } },
120
- (span) => this._respondToWrite(() => this.store.upsertSomeTentative(builders, span, {skipPush}), span)
121
- )
122
- }
123
-
124
110
  protected async deleteOne(id: RecordId, { skipPush = false } = {}) {
125
111
  return this.store.telemetry.trace(
126
112
  { name: `delete:${this.store.model.table}`, op: 'delete', attributes: { ...this.context.session.toJSON() } },
@@ -128,10 +114,10 @@ export default class SyncRepository<TModel extends BaseModel> {
128
114
  )
129
115
  }
130
116
 
131
- protected async deleteSome(ids: RecordId[]) {
117
+ protected async deleteSome(ids: RecordId[], { skipPush = false } = {}) {
132
118
  return this.store.telemetry.trace(
133
119
  { name: `deleteSome:${this.store.model.table}`, op: 'delete', attributes: { ...this.context.session.toJSON() } },
134
- (span) => this._respondToWriteIds(() => this.store.deleteSome(ids, span), span)
120
+ (span) => this._respondToWriteIds(() => this.store.deleteSome(ids, span, {skipPush}), span)
135
121
  )
136
122
  }
137
123
 
@@ -187,7 +187,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
187
187
  recordProgressMany(
188
188
  contentProgresses: Record<string, number>, // Accept plain object
189
189
  collection: CollectionParameter | null,
190
- { tentative = true, skipPush = false, fromLearningPath = false }: { tentative?: boolean; skipPush?: boolean; fromLearningPath?: boolean } = {}
190
+ { skipPush = false, fromLearningPath = false }: { skipPush?: boolean; fromLearningPath?: boolean } = {}
191
191
  ) {
192
192
  if (collection?.type === COLLECTION_TYPE.LEARNING_PATH) {
193
193
  fromLearningPath = true
@@ -209,9 +209,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
209
209
  },
210
210
  ])
211
211
  )
212
- return tentative
213
- ? this.upsertSomeTentative(data, { skipPush })
214
- : this.upsertSome(data, { skipPush })
212
+ return this.upsertSome(data, { skipPush })
215
213
 
216
214
  //todo add event emitting for bulk updates?
217
215
  }
@@ -220,6 +218,11 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
220
218
  return this.deleteOne(ProgressRepository.generateId(contentId, collection), { skipPush })
221
219
  }
222
220
 
221
+ eraseProgressMany(contentIds: number[], collection: CollectionParameter | null, {skipPush = false} = {}) {
222
+ const ids = contentIds.map((id) => ProgressRepository.generateId(id, collection))
223
+ return this.deleteSome(ids, { skipPush })
224
+ }
225
+
223
226
  private static generateId(
224
227
  contentId: number,
225
228
  collection: CollectionParameter | null
@@ -354,7 +354,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
354
354
  })
355
355
  }
356
356
 
357
- async deleteSome(ids: RecordId[], span?: Span) {
357
+ async deleteSome(ids: RecordId[], span?: Span, { skipPush = false } = {}) {
358
358
  return this.runScope.abortable(async () => {
359
359
  await this.telemeterizedWrite(span, async writer => {
360
360
  const existing = await this.queryRecords(Q.where('id', Q.oneOf(ids)))
@@ -364,7 +364,9 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
364
364
 
365
365
  this.emit('deleted', ids)
366
366
 
367
- this.pushUnsyncedWithRetry(span)
367
+ if (!skipPush) {
368
+ this.pushUnsyncedWithRetry(span)
369
+ }
368
370
  await this.ensurePersistence()
369
371
 
370
372
  return ids
@@ -4,6 +4,7 @@
4
4
  import { HttpClient } from '../../infrastructure/http/HttpClient'
5
5
  import { HttpError } from '../../infrastructure/http/interfaces/HttpError'
6
6
  import { globalConfig } from '../config.js'
7
+ import { clearAllCachedData } from '../dataContext.js'
7
8
  import { Onboarding } from './onboarding'
8
9
  import { AuthResponse } from './types'
9
10
 
@@ -149,7 +150,10 @@ export async function confirmEmailChange(token: string): Promise<void> {
149
150
  export async function deleteAccount(userId: number): Promise<void> {
150
151
  const apiUrl = `/api/user-management-system/v1/users/${userId}`
151
152
  const httpClient = new HttpClient(globalConfig.baseUrl, globalConfig.sessionConfig.token)
152
- return httpClient.delete(apiUrl)
153
+ await httpClient.delete(apiUrl)
154
+
155
+ // Clear all locally cached data to prevent data leakage between users
156
+ await clearAllCachedData()
153
157
  }
154
158
 
155
159
  /**