musora-content-services 2.139.7 → 2.140.0

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,18 @@
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.140.0](https://github.com/railroadmedia/musora-content-services/compare/v2.139.7...v2.140.0) (2026-03-17)
6
+
7
+
8
+ ### Features
9
+
10
+ * default field updates ([#869](https://github.com/railroadmedia/musora-content-services/issues/869)) ([d751e5a](https://github.com/railroadmedia/musora-content-services/commit/d751e5a586735664ec40b0dfed82b00460e34e92))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **TP-1105:** playlist progress for homepage row ([#846](https://github.com/railroadmedia/musora-content-services/issues/846)) ([717e123](https://github.com/railroadmedia/musora-content-services/commit/717e1233e07cc3f81c98b80022a4c9e8b9df952c))
16
+
5
17
  ### [2.139.7](https://github.com/railroadmedia/musora-content-services/compare/v2.139.6...v2.139.7) (2026-03-17)
6
18
 
7
19
  ### [2.139.6](https://github.com/railroadmedia/musora-content-services/compare/v2.139.5...v2.139.6) (2026-03-17)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.139.7",
3
+ "version": "2.140.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -70,6 +70,8 @@ export const DEFAULT_FIELDS = [
70
70
  'child_count',
71
71
  '"parent_id": parent_content_data[0].id',
72
72
  '"grandparent_id": parent_content_data[1].id',
73
+ 'live_event_start_time',
74
+ 'live_event_end_time',
73
75
  ]
74
76
 
75
77
  // these are identical... why
@@ -658,9 +660,8 @@ export let contentTypeConfig = {
658
660
  ],
659
661
  'new-and-scheduled': {
660
662
  fields: [
661
- 'show_in_new_feed',
662
- isLiveField(),
663
- 'live_event_start_time',
663
+ 'show_in_new_feed',
664
+ isLiveField()
664
665
  ],
665
666
  },
666
667
  }
package/src/index.d.ts CHANGED
@@ -114,6 +114,7 @@ import {
114
114
  contentStatusReset,
115
115
  contentStatusStarted,
116
116
  flushWatchSession,
117
+ generateRecordId,
117
118
  getAllCompleted,
118
119
  getAllCompletedByIds,
119
120
  getAllStarted,
@@ -123,11 +124,12 @@ import {
123
124
  getNavigateTo,
124
125
  getNavigateToForMethod,
125
126
  getProgressDataByIds,
126
- getProgressDataByIdsAndCollections,
127
+ getProgressDataByRecordIds,
127
128
  getProgressState,
128
129
  getProgressStateByIds,
130
+ getProgressStateByRecordIds,
129
131
  getResumeTimeSecondsByIds,
130
- getResumeTimeSecondsByIdsAndCollections,
132
+ getResumeTimeSecondsByRecordIds,
131
133
  getStartedOrCompletedProgressOnly,
132
134
  recordWatchSession
133
135
  } from './services/contentProgress.js';
@@ -277,8 +279,6 @@ import {
277
279
  fetchContentRows,
278
280
  fetchContentTypeCounts,
279
281
  fetchCourseCollectionData,
280
- fetchHierarchy,
281
- fetchLearningPathHierarchy,
282
282
  fetchLeaving,
283
283
  fetchLessonContent,
284
284
  fetchLessonsFeaturingThisContent,
@@ -309,6 +309,7 @@ import {
309
309
  fetchTabData,
310
310
  fetchTopLevelParentId,
311
311
  fetchUpcomingEvents,
312
+ getHierarchy,
312
313
  getSanityDate,
313
314
  getSongTypesFor,
314
315
  getSortOrder,
@@ -447,7 +448,7 @@ import {
447
448
  } from './services/userActivity.js';
448
449
 
449
450
  import {
450
- default as EventsAPI
451
+ default as EventsAPI
451
452
  } from './services/eventsAPI';
452
453
 
453
454
  declare module 'musora-content-services' {
@@ -529,14 +530,12 @@ declare module 'musora-content-services' {
529
530
  fetchGenreLessons,
530
531
  fetchGenres,
531
532
  fetchHasActivePlatformSubscription,
532
- fetchHierarchy,
533
533
  fetchInstructorBySlug,
534
534
  fetchInstructorLessons,
535
535
  fetchInstructors,
536
536
  fetchInterests,
537
537
  fetchLastSubscriptionPlatform,
538
538
  fetchLatestThreads,
539
- fetchLearningPathHierarchy,
540
539
  fetchLearningPathLessons,
541
540
  fetchLearningPathProgressCheckLessons,
542
541
  fetchLeaving,
@@ -604,6 +603,7 @@ declare module 'musora-content-services' {
604
603
  generateContentUrlWithDomain,
605
604
  generateForumPostUrl,
606
605
  generatePlaylistUrl,
606
+ generateRecordId,
607
607
  getActiveDiscussions,
608
608
  getActivePath,
609
609
  getAllCompleted,
@@ -619,6 +619,7 @@ declare module 'musora-content-services' {
619
619
  getDailySession,
620
620
  getEnrichedLearningPath,
621
621
  getEnrichedLearningPaths,
622
+ getHierarchy,
622
623
  getIdsWhereLastAccessedFromMethod,
623
624
  getInProgressAwards,
624
625
  getLastInteractedOf,
@@ -635,16 +636,17 @@ declare module 'musora-content-services' {
635
636
  getPracticeNotes,
636
637
  getPracticeSessions,
637
638
  getProgressDataByIds,
638
- getProgressDataByIdsAndCollections,
639
+ getProgressDataByRecordIds,
639
640
  getProgressRows,
640
641
  getProgressState,
641
642
  getProgressStateByIds,
643
+ getProgressStateByRecordIds,
642
644
  getRecent,
643
645
  getRecentActivity,
644
646
  getRecommendedForYou,
645
647
  getReportIssueOptions,
646
648
  getResumeTimeSecondsByIds,
647
- getResumeTimeSecondsByIdsAndCollections,
649
+ getResumeTimeSecondsByRecordIds,
648
650
  getSanityDate,
649
651
  getScheduleContentRows,
650
652
  getSongTypesFor,
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*** This file was generated automatically. To recreate, please run `npm run build-index`. ***/
2
2
 
3
3
  import {
4
- default as EventsAPI
4
+ default as EventsAPI
5
5
  } from './services/eventsAPI';
6
6
 
7
7
  import {
@@ -118,6 +118,7 @@ import {
118
118
  contentStatusReset,
119
119
  contentStatusStarted,
120
120
  flushWatchSession,
121
+ generateRecordId,
121
122
  getAllCompleted,
122
123
  getAllCompletedByIds,
123
124
  getAllStarted,
@@ -127,11 +128,12 @@ import {
127
128
  getNavigateTo,
128
129
  getNavigateToForMethod,
129
130
  getProgressDataByIds,
130
- getProgressDataByIdsAndCollections,
131
+ getProgressDataByRecordIds,
131
132
  getProgressState,
132
133
  getProgressStateByIds,
134
+ getProgressStateByRecordIds,
133
135
  getResumeTimeSecondsByIds,
134
- getResumeTimeSecondsByIdsAndCollections,
136
+ getResumeTimeSecondsByRecordIds,
135
137
  getStartedOrCompletedProgressOnly,
136
138
  recordWatchSession
137
139
  } from './services/contentProgress.js';
@@ -281,8 +283,6 @@ import {
281
283
  fetchContentRows,
282
284
  fetchContentTypeCounts,
283
285
  fetchCourseCollectionData,
284
- fetchHierarchy,
285
- fetchLearningPathHierarchy,
286
286
  fetchLeaving,
287
287
  fetchLessonContent,
288
288
  fetchLessonsFeaturingThisContent,
@@ -313,6 +313,7 @@ import {
313
313
  fetchTabData,
314
314
  fetchTopLevelParentId,
315
315
  fetchUpcomingEvents,
316
+ getHierarchy,
316
317
  getSanityDate,
317
318
  getSongTypesFor,
318
319
  getSortOrder,
@@ -528,14 +529,12 @@ export {
528
529
  fetchGenreLessons,
529
530
  fetchGenres,
530
531
  fetchHasActivePlatformSubscription,
531
- fetchHierarchy,
532
532
  fetchInstructorBySlug,
533
533
  fetchInstructorLessons,
534
534
  fetchInstructors,
535
535
  fetchInterests,
536
536
  fetchLastSubscriptionPlatform,
537
537
  fetchLatestThreads,
538
- fetchLearningPathHierarchy,
539
538
  fetchLearningPathLessons,
540
539
  fetchLearningPathProgressCheckLessons,
541
540
  fetchLeaving,
@@ -603,6 +602,7 @@ export {
603
602
  generateContentUrlWithDomain,
604
603
  generateForumPostUrl,
605
604
  generatePlaylistUrl,
605
+ generateRecordId,
606
606
  getActiveDiscussions,
607
607
  getActivePath,
608
608
  getAllCompleted,
@@ -618,6 +618,7 @@ export {
618
618
  getDailySession,
619
619
  getEnrichedLearningPath,
620
620
  getEnrichedLearningPaths,
621
+ getHierarchy,
621
622
  getIdsWhereLastAccessedFromMethod,
622
623
  getInProgressAwards,
623
624
  getLastInteractedOf,
@@ -634,16 +635,17 @@ export {
634
635
  getPracticeNotes,
635
636
  getPracticeSessions,
636
637
  getProgressDataByIds,
637
- getProgressDataByIdsAndCollections,
638
+ getProgressDataByRecordIds,
638
639
  getProgressRows,
639
640
  getProgressState,
640
641
  getProgressStateByIds,
642
+ getProgressStateByRecordIds,
641
643
  getRecent,
642
644
  getRecentActivity,
643
645
  getRecommendedForYou,
644
646
  getReportIssueOptions,
645
647
  getResumeTimeSecondsByIds,
646
- getResumeTimeSecondsByIdsAndCollections,
648
+ getResumeTimeSecondsByRecordIds,
647
649
  getSanityDate,
648
650
  getScheduleContentRows,
649
651
  getSongTypesFor,
@@ -1,12 +1,13 @@
1
1
  import {
2
+ generateRecordId,
2
3
  getNavigateTo,
3
4
  getNavigateToForMethod,
4
5
  getProgressDataByIds,
5
- getProgressDataByIdsAndCollections,
6
- getProgressStateByIds,
6
+ getProgressDataByRecordIds,
7
+ getProgressStateByRecordIds,
7
8
  getResumeTimeSecondsByIds,
8
- getResumeTimeSecondsByIdsAndCollections,
9
- } from './contentProgress'
9
+ getResumeTimeSecondsByRecordIds,
10
+ } from './contentProgress.js'
10
11
  import { isContentLikedByIds } from './contentLikes'
11
12
  import { fetchLikeCount } from './railcontent'
12
13
  import {COLLECTION_TYPE} from "./sync/models/ContentProgress";
@@ -181,14 +182,9 @@ export async function addContextToLearningPaths(dataPromise, ...dataArgs) {
181
182
  let items = extractItemsWithCollectionFromMethodData(data, dataField, isDataAnArray, dataField_includeParent, dataField_includeIntroVideo) ?? []
182
183
  if (items.length === 0) return data
183
184
 
184
- let ids = items.map((item) => (
185
- {
186
- contentId: item.content?.id,
187
- collection: item.collection
188
- })
189
- ).filter(obj => obj.contentId)
185
+ const recordIds = items.map((item) => generateRecordId(item.content?.id, item.collection))
190
186
 
191
- const justIds = ids.map(obj => obj.contentId)
187
+ const ids = items.map(item => item.content?.id)
192
188
 
193
189
  const [
194
190
  progressData,
@@ -198,11 +194,11 @@ export async function addContextToLearningPaths(dataPromise, ...dataArgs) {
198
194
  awards,
199
195
  ] = await Promise.all([
200
196
  addProgressPercentage || addProgressStatus || addProgressTimestamp
201
- ? getProgressDataByIdsAndCollections(ids) : Promise.resolve(null),
202
- addIsLiked ? isContentLikedByIds(justIds) : Promise.resolve(null),
203
- addResumeTimeSeconds ? getResumeTimeSecondsByIdsAndCollections(ids) : Promise.resolve(null),
197
+ ? getProgressDataByRecordIds(recordIds) : Promise.resolve(null),
198
+ addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
199
+ addResumeTimeSeconds ? getResumeTimeSecondsByRecordIds(recordIds) : Promise.resolve(null),
204
200
  addNavigateTo ? getNavigateToForMethod(items) : Promise.resolve(null),
205
- addAwards ? getContentAwardsByIds(justIds) : Promise.resolve(null),
201
+ addAwards ? getContentAwardsByIds(ids) : Promise.resolve(null),
206
202
  ])
207
203
 
208
204
  const addContext = async (item) => {
@@ -239,23 +235,21 @@ export async function addContextToLearningPaths(dataPromise, ...dataArgs) {
239
235
 
240
236
  export async function getNavigateToForPlaylists(data, { dataField = null } = {}) {
241
237
  let playlists = extractItemsFromData(data, dataField, false, false)
242
- let allIds = []
243
- playlists.forEach(
244
- (playlist) => (allIds = [...allIds, ...playlist.items.map((a) => a.content_id)])
245
- )
246
- const progressOnItems = await getProgressStateByIds(allIds)
238
+
239
+ const allIds = [...new Set(playlists.flatMap(playlist => playlist.items.map(item => item.content_id)))]
240
+ const progressOnItems = await getProgressDataByIds(allIds) // currently playlist progress IS a-la-carte progress.
241
+
247
242
  const addContext = async (playlist) => {
248
243
  // Filter out locked items (where need_access === true) and scheduled content
249
244
  const accessibleItems = playlist.items.filter((item) => !item.need_access && item.status !== 'scheduled')
250
245
 
251
- const allItemsCompleted = accessibleItems.every((i) => {
252
- const itemId = i.content_id
253
- const progress = progressOnItems.get(itemId)
254
- return progress && progress === 'completed'
246
+ const allItemsCompleted = accessibleItems.every((item) => {
247
+ const progress = progressOnItems[item.content_id]
248
+ return progress?.status === 'completed'
255
249
  })
256
250
  let nextItem = accessibleItems[0] ?? playlist.items[0] ?? null
257
251
  if (!allItemsCompleted) {
258
- const lastItemProgress = progressOnItems.get(playlist.last_engaged_on)
252
+ const lastItemProgress = progressOnItems[playlist.last_engaged_on]
259
253
  const index = accessibleItems.findIndex((i) => i.content_id === playlist.last_engaged_on)
260
254
  if (lastItemProgress === 'completed') {
261
255
  nextItem = accessibleItems[index + 1] ?? nextItem
@@ -19,6 +19,10 @@ export async function getProgressStateByIds(contentIds, collection = null) {
19
19
  return getByIds(normalizeContentIds(contentIds), normalizeCollection(collection), 'state', '')
20
20
  }
21
21
 
22
+ export async function getProgressStateByRecordIds(ids) {
23
+ return getByRecordIds(ids, 'state', '')
24
+ }
25
+
22
26
  export async function getResumeTimeSecondsByIds(contentIds, collection = null) {
23
27
  return getByIds(
24
28
  normalizeContentIds(contentIds),
@@ -28,8 +32,8 @@ export async function getResumeTimeSecondsByIds(contentIds, collection = null) {
28
32
  )
29
33
  }
30
34
 
31
- export async function getResumeTimeSecondsByIdsAndCollections(tuples) {
32
- return getByIdsAndCollections(tuples, 'resume_time_seconds', 0)
35
+ export async function getResumeTimeSecondsByRecordIds(ids) {
36
+ return getByRecordIds(ids, 'resume_time_seconds', 0)
33
37
  }
34
38
 
35
39
  export async function getNavigateToForMethod(data) {
@@ -246,7 +250,7 @@ export async function getProgressDataByIds(contentIds, collection) {
246
250
  * Get progress data for multiple content IDs, each with their own collection context.
247
251
  * Useful when fetching progress for tuples that belong to different collections.
248
252
  *
249
- * @param {Array<{contentId: number, collection: {type: string, id: number}|null}>} tuples - Array of objects with contentId and collection
253
+ * @param ids {Array<string>} - Array of record ids
250
254
  * @returns {Promise<Object>} - Object mapping content IDs to progress data
251
255
  *
252
256
  * @example
@@ -255,32 +259,26 @@ export async function getProgressDataByIds(contentIds, collection) {
255
259
  * { contentId: 789, collection: { id: 101, type: 'learning-path-v2' } },
256
260
  * { contentId: 111, collection: null }
257
261
  * ]
258
- * const progress = await getProgressDataByIdsAndCollections(tuples)
262
+ * const progress = await getProgressDataByRecordIds(tuples)
259
263
  * // Returns: { 123: { progress: 50, status: 'started', last_update: 123456 }, ... }
260
264
  */
261
265
 
262
- // warning: unsafe due to object key conflicts.
263
- // todo: remove this, and simplify addContextToLearningPaths
264
- export async function getProgressDataByIdsAndCollections(tuples) {
265
- tuples = tuples.map(t => ({contentId: normalizeContentId(t.contentId), collection: normalizeCollection(t.collection)}))
266
- const progress = Object.fromEntries(tuples.map(item => [item.contentId, {
266
+ export async function getProgressDataByRecordIds(ids) {
267
+ const progress = Object.fromEntries(ids.map(id => [id, {
267
268
  last_update: 0,
268
269
  progress: 0,
269
270
  status: '',
270
- collection: {},
271
271
  }]))
272
272
 
273
- await db.contentProgress.getSomeProgressByContentIdsAndCollections(tuples).then(r => {
273
+ await db.contentProgress.getSomeProgressByRecordIds(ids).then(r => {
274
274
  r.data.forEach(p => {
275
- progress[p.content_id] = {
275
+ progress[p.id] = {
276
276
  last_update: p.updated_at,
277
277
  progress: p.progress_percent,
278
278
  status: p.state,
279
- collection: (p.collection_type && p.collection_id) ? {type: p.collection_type, id: p.collection_id} : null
280
279
  }
281
280
  })
282
281
  })
283
-
284
282
  return progress
285
283
  }
286
284
 
@@ -303,24 +301,31 @@ async function getByIds(contentIds, collection, dataKey, defaultValue) {
303
301
  return progress
304
302
  }
305
303
 
306
- async function getByIdsAndCollections(tuples, dataKey, defaultValue) {
307
- tuples = tuples.map(t => ({contentId: normalizeContentId(t.contentId), collection: normalizeCollection(t.collection)}))
308
- const progress = Object.fromEntries(tuples.map(tuple => [tuple.contentId, defaultValue]))
304
+ async function getByRecordIds(ids, dataKey, defaultValue) {
305
+ const progress = Object.fromEntries(ids.map(id => [id, defaultValue]))
309
306
 
310
- await db.contentProgress.getSomeProgressByContentIdsAndCollections(tuples).then(r => {
307
+ await db.contentProgress.getSomeProgressByRecordIds(ids).then(r => {
311
308
  r.data.forEach(p => {
312
- progress[p.content_id] = p[dataKey] ?? defaultValue
309
+ progress[p.id] = p[dataKey] ?? defaultValue
313
310
  })
314
311
  })
315
312
  return progress
316
313
  }
317
314
 
318
- export async function getAllStarted(limit = null) {
319
- return db.contentProgress.startedIds(limit)
315
+ export async function getAllStarted(limit = null, {
316
+ onlyIds = true,
317
+ include = { aLaCarte: true, learningPaths: false },
318
+ } = {}
319
+ ) {
320
+ return db.contentProgress.started(limit, {onlyIds, include})
320
321
  }
321
322
 
322
- export async function getAllCompleted(limit = null) {
323
- return db.contentProgress.completedIds(limit)
323
+ export async function getAllCompleted(limit = null, {
324
+ onlyIds = true,
325
+ include = { aLaCarte: true, learningPaths: false },
326
+ } = {}
327
+ ) {
328
+ return db.contentProgress.completed(limit, {onlyIds, include})
324
329
  }
325
330
 
326
331
  export async function getAllCompletedByIds(contentIds) {
@@ -333,8 +338,17 @@ export async function getAllCompletedByIds(contentIds) {
333
338
  export async function getAllStartedOrCompleted({
334
339
  brand = null,
335
340
  limit = null,
341
+ include = { aLaCarte: true, learningPaths: false },
342
+ onlyIds = true // need to be careful if allowing non-alacarte progress, because some content_ids can overlap
336
343
  } = {}) {
337
- return await _getAllStartedOrCompleted({ brand, limit }).then(recs => recs.map(rec => rec.content_id))
344
+ const data = await _getAllStartedOrCompleted({
345
+ brand,
346
+ limit,
347
+ include
348
+ })
349
+ return onlyIds
350
+ ? data.map(rec => rec.content_id)
351
+ : data
338
352
  }
339
353
 
340
354
  /**
@@ -363,10 +377,12 @@ export async function getStartedOrCompletedProgressOnly({ brand = undefined } =
363
377
  async function _getAllStartedOrCompleted({
364
378
  brand = null,
365
379
  limit = null,
380
+ include = { aLaCarte: true, learningPaths: false },
366
381
  } = {}) {
367
382
  const agoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
368
383
  const baseFilters = {
369
384
  updatedAfter: agoInSeconds,
385
+ include: include,
370
386
  }
371
387
 
372
388
  if (!brand) {
@@ -487,9 +503,10 @@ export async function contentStatusReset(contentId, collection = null, {skipPush
487
503
  return resetStatus(contentId, collection, {skipPush})
488
504
  }
489
505
 
490
- async function saveContentProgress(contentId, collection, progress, currentSeconds, {skipPush = false, fromLearningPath = false} = {}) {
506
+ async function saveContentProgress(contentId, collection, progress, currentSeconds, {skipPush = false, accessedDirectly = true} = {}) {
491
507
  collection = collection ?? {id: COLLECTION_ID_SELF, type: COLLECTION_TYPE.SELF}
492
508
  const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
509
+ const isPlaylist = collection?.type === COLLECTION_TYPE.PLAYLIST
493
510
 
494
511
  // filter out contentIds that are setting progress lower than existing
495
512
  const contentIdProgress = await getProgressDataByIds([contentId], collection)
@@ -498,12 +515,18 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
498
515
  progress = currentProgress;
499
516
  }
500
517
 
518
+ if (isPlaylist) {
519
+ const exportIds = { [contentId]: progress }
520
+ await duplicateProgressToALaCarte(exportIds, collection, {skipPush: true})
521
+ return
522
+ }
523
+
501
524
  const response = await db.contentProgress.recordProgress(
502
525
  normalizeContentId(contentId),
503
526
  normalizeCollection(collection),
504
527
  progress,
505
528
  currentSeconds,
506
- {skipPush: true, fromLearningPath}
529
+ {skipPush: true, accessedDirectly}
507
530
  )
508
531
  // note - previous implementation explicitly did not trickle progress to children here
509
532
  // (only to siblings/parents via le bubbles)
@@ -527,13 +550,13 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
527
550
  }
528
551
 
529
552
  if (Object.keys(bubbledProgresses).length > 0) {
530
- await db.contentProgress.recordProgressMany(bubbledProgresses, normalizeCollection(collection), {skipPush: true, fromLearningPath})
553
+ await db.contentProgress.recordProgressMany(bubbledProgresses, normalizeCollection(collection), {skipPush: true, accessedDirectly})
531
554
  }
532
555
 
533
556
  if (isLP) {
534
557
  let exportIds = bubbledProgresses
535
558
  exportIds[contentId] = progress
536
- await duplicateLearningPathProgressToExternalContents(exportIds, collection, hierarchy, {skipPush: true})
559
+ await duplicateProgressToALaCarte(exportIds, collection, {skipPush: true})
537
560
  }
538
561
 
539
562
  if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
@@ -568,7 +591,7 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {
568
591
  if (isLP) {
569
592
  let exportProgresses = progresses
570
593
  exportProgresses[contentId] = progress
571
- await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy, {skipPush: true})
594
+ await duplicateProgressToALaCarte(exportProgresses, collection, {skipPush: true})
572
595
  }
573
596
 
574
597
  if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
@@ -617,7 +640,7 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
617
640
  for (const contentId of contentIds){
618
641
  exportProgresses[contentId] = progress
619
642
  }
620
- await duplicateLearningPathProgressToExternalContents(exportProgresses, collection, hierarchy, {skipPush: true})
643
+ await duplicateProgressToALaCarte(exportProgresses, collection, {skipPush: true})
621
644
  }
622
645
 
623
646
  for (const [id, progress] of Object.entries(progresses)) {
@@ -649,7 +672,7 @@ async function resetStatus(contentId, collection = null, {skipPush = false} = {}
649
672
 
650
673
  if (isLP) {
651
674
  progresses[contentId] = progress
652
- await duplicateLearningPathProgressToExternalContents(progresses, collection, hierarchy, {skipPush: true})
675
+ await duplicateProgressToALaCarte(progresses, collection, {skipPush: true})
653
676
  }
654
677
 
655
678
  if (!skipPush) db.contentProgress.requestPushUnsynced('reset-status')
@@ -657,44 +680,52 @@ async function resetStatus(contentId, collection = null, {skipPush = false} = {}
657
680
  return response
658
681
  }
659
682
 
660
- // we cannot simply pass LP id with self collection, because we do not have a-la-carte LP's set up yet,
661
- // and we need each lesson to bubble to its parent outside of LP
662
- async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy, {skipPush = false} = {}) {
663
- // filter out LPs. we dont want to duplicate to LP's while we dont have a-la-cart LP's set up.
664
- let filteredIds = Object.fromEntries(
665
- Object.entries(ids).filter(([id]) => {
666
- return hierarchy.parents[parseInt(id)] !== null
683
+ async function duplicateProgressToALaCarte(progresses, collection, {skipPush = false} = {}) {
684
+
685
+ // a-la-cart LPs not set up.
686
+ let filteredProgresses = filterOutLearningPathsForDuplication(progresses, collection)
687
+
688
+ const externalProgresses = await getProgressDataByIds(Object.keys(filteredProgresses), null)
689
+
690
+ filteredProgresses = filterGreaterThanProgress(filteredProgresses, externalProgresses)
691
+
692
+ duplicateProgressForIds(filteredProgresses, skipPush)
693
+ }
694
+
695
+ function filterOutLearningPathsForDuplication(progresses, collection) {
696
+ return Object.fromEntries(
697
+ Object.entries(progresses).filter(([id]) => {
698
+ if (collection.type === COLLECTION_TYPE.LEARNING_PATH) {
699
+ // dont want progress on a-la-carte LPs (not supported)
700
+ return id !== collection.id
701
+ } else {
702
+ return true
703
+ }
667
704
  })
668
705
  )
706
+ }
669
707
 
670
- const extProgresses = await getProgressDataByIds(Object.keys(filteredIds), null)
671
-
708
+ function filterGreaterThanProgress(progresses, external) {
672
709
  // overwrite if LP progress greater, unless LP progress was reset to 0
673
- filteredIds = Object.entries(filteredIds).filter(([id, pct]) => {
674
- const extPct = extProgresses[id]?.progress
710
+ return Object.entries(progresses).filter(([id, pct]) => {
711
+ const extPct = external[id]?.progress
675
712
  return (pct !== 0)
676
713
  ? pct > extPct
677
714
  : false
678
715
  })
716
+ }
679
717
 
680
- // each handles its own bubbling.
681
- // skipPush on all but last to avoid multiple push requests
682
- filteredIds.forEach(([id, pct], index) => {
718
+ async function duplicateProgressForIds(ids, skipPush) {
719
+ ids.forEach(([id, pct], index) => {
683
720
  let skip = true
684
- if (index === filteredIds.length - 1) {
721
+ if (index === ids.length - 1) {
722
+ // only allow push on last call, to group into one push
685
723
  skip = skipPush
686
724
  }
687
- saveContentProgress(parseInt(id), null, pct, null, {skipPush: skip, fromLearningPath: true})
725
+ saveContentProgress(parseInt(id), null, pct, null, {skipPush: skip, accessedDirectly: false})
688
726
  })
689
727
  }
690
728
 
691
- async function getHierarchy(contentId, collection) {
692
- if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
693
- return await fetchLearningPathHierarchy(contentId, collection)
694
- } else {
695
- return await fetchHierarchy(contentId)
696
- }
697
- }
698
729
 
699
730
  // agnostic to collection - makes returned data structure simpler,
700
731
  // as long as callers remember to pass collection where needed
@@ -811,3 +842,11 @@ export async function getIdsWhereLastAccessedFromMethod(contentIds) {
811
842
 
812
843
  return records.data.map(record => record.content_id)
813
844
  }
845
+
846
+ export function generateRecordId(contentId, collection) {
847
+ if (!contentId) return null
848
+
849
+ contentId = normalizeContentId(contentId)
850
+
851
+ return `${contentId}:${collection?.type || COLLECTION_TYPE.SELF}:${collection?.id || COLLECTION_ID_SELF}`
852
+ }
@@ -4,8 +4,6 @@
4
4
  import { getMethodCard } from './rows/method-card.js'
5
5
  import {
6
6
  getPlaylistCards,
7
- getPlaylistEngagedOnContent,
8
- getRecentPlaylists,
9
7
  processPlaylistItem,
10
8
  } from './rows/playlist-card.js'
11
9
  import { globalConfig } from '../config.js'
@@ -168,19 +166,17 @@ export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}, opti
168
166
  db.contentProgress.pull()
169
167
  }
170
168
 
171
- const [userPinnedItem, recentPlaylists] = await Promise.all([
172
- getUserPinnedItem(brand),
173
- getRecentPlaylists(brand, limit),
174
- ])
175
- const playlistEngagedOnContent = await getPlaylistEngagedOnContent(recentPlaylists)
169
+ const userPinnedItem = await getUserPinnedItem(brand)
176
170
 
177
- const [contentCardMap, playlistCards, methodCard] = await getCards(brand, limit, playlistEngagedOnContent, userPinnedItem, recentPlaylists)
171
+ const [contentCardMap, playlistCards, methodCard] = await getCards(brand, limit, userPinnedItem)
178
172
 
179
173
  const pinnedCard = await popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard)
174
+
180
175
  let allResultsLength = playlistCards.length + contentCardMap.size
181
176
  if (methodCard) {
182
177
  allResultsLength += 1
183
178
  }
179
+
184
180
  const results = sortCards(pinnedCard, contentCardMap, playlistCards, methodCard, limit)
185
181
  return {
186
182
  type: TabResponseType.PROGRESS_ROWS,
@@ -189,13 +185,13 @@ export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}, opti
189
185
  }
190
186
  }
191
187
 
192
- async function getCards(brand, limit, playlistEngagedOnContent, userPinnedItem, recentPlaylists) {
188
+ async function getCards(brand, limit, userPinnedItem) {
193
189
  return Promise.all([
194
- getContentCardMap(brand, limit, playlistEngagedOnContent, userPinnedItem).catch(e => {
190
+ getContentCardMap(brand, limit, userPinnedItem).catch(e => {
195
191
  console.error('getContentCardMap failed:', e)
196
192
  return new Map()
197
193
  }),
198
- getPlaylistCards(recentPlaylists).catch(e => {
194
+ getPlaylistCards(brand, limit).catch(e => {
199
195
  console.error('getPlaylistCards failed:', e)
200
196
  return []
201
197
  }),
@@ -241,7 +237,7 @@ async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, meth
241
237
  item = pinnedPlaylist
242
238
  } else {
243
239
  const playlist = await fetchPlaylist(pinnedId)
244
- item = await processPlaylistItem({
240
+ item = processPlaylistItem({
245
241
  id: pinnedId,
246
242
  playlist: playlist,
247
243
  type: 'playlist',
@@ -16,24 +16,17 @@ import { getTimeRemainingUntilLocal } from '../../dateUtils.js'
16
16
 
17
17
  /**
18
18
  * Fetch any content IDs with some progress, include the userPinnedItem,
19
- * remove any content IDs that already exist in playlistEngagedOnContent,
20
19
  * and generate a map of the cards keyed by the content IDs
21
20
  */
22
- export async function getContentCardMap(brand, limit, playlistEngagedOnContent, userPinnedItem ){
23
- let recentContentIds = await getAllStartedOrCompleted({ brand: brand, limit: (limit ? (limit * 5) : limit) })
21
+ export async function getContentCardMap(brand, limit, userPinnedItem ){
22
+ let recentContentIds = await getAllStartedOrCompleted({
23
+ brand: brand,
24
+ limit: (limit ? (limit * 5) : limit),
25
+ })
24
26
  if (userPinnedItem?.progressType === 'content') {
25
27
  recentContentIds.push(userPinnedItem.id)
26
28
  }
27
- if (playlistEngagedOnContent) {
28
- for (const item of playlistEngagedOnContent) {
29
- const parentIds = item.parent_content_data || []
30
- recentContentIds = recentContentIds.filter(id => {
31
- if (id === item.id) return false
32
- if (parentIds.includes(id) && item.progressTimestamp > 0) return false
33
- return true
34
- })
35
- }
36
- }
29
+
37
30
  let contents = recentContentIds.length > 0
38
31
  ? await addContextToContent(
39
32
  fetchByRailContentIds,
@@ -4,17 +4,13 @@
4
4
  import { fetchUserPlaylists } from '../../content-org/playlists.js'
5
5
  import { addContextToContent } from '../../contentAggregator.js'
6
6
  import { fetchByRailContentIds } from '../../sanity.js'
7
- import { postProcessBadge } from "../../../contentTypeConfig.js";
8
7
 
9
- export async function getPlaylistCards(recentPlaylists){
10
- return await Promise.all(
11
- recentPlaylists.map((playlist) => {
12
- return processPlaylistItem(playlist)
13
- })
14
- )
8
+ export async function getPlaylistCards(brand, limit) {
9
+ let recentPlaylists = await getRecentPlaylists(brand, limit)
10
+ return recentPlaylists.map((p) => processPlaylistItem(p))
15
11
  }
16
12
 
17
- export async function processPlaylistItem(item) {
13
+ export function processPlaylistItem(item) {
18
14
  const playlist = item.playlist
19
15
 
20
16
  return {
@@ -45,28 +41,28 @@ export async function processPlaylistItem(item) {
45
41
  }
46
42
 
47
43
  export async function getRecentPlaylists(brand, limit) {
44
+ // todo: add a get_playlist param ot get a specific playlist, so we get ideally do only 1 fetch.
48
45
  const response = await fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit })
49
46
  const playlists = response?.data || []
47
+
50
48
  const recentPlaylists = playlists.filter((p) => p.last_progress && p.last_engaged_on)
51
- return await Promise.all(
52
- recentPlaylists.map(async (p) => {
53
- const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z')
54
- const timestamp = utcDate.getTime()
55
- return {
56
- type: 'playlist',
57
- progressTimestamp: timestamp,
58
- playlist: p,
59
- id: p.id,
60
- }
61
- })
62
- )
49
+ return recentPlaylists.map((p) => {
50
+ const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z')
51
+ const timestamp = utcDate.getTime()
52
+ return {
53
+ type: 'playlist',
54
+ progressTimestamp: timestamp,
55
+ playlist: p,
56
+ id: p.id,
57
+ }
58
+ })
63
59
  }
64
60
 
65
61
  export async function getPlaylistEngagedOnContent(recentPlaylists){
66
62
  const playlistEngagedOnContents = recentPlaylists.map(
67
63
  (item) => item.playlist.last_engaged_on
68
64
  )
69
- let contents = playlistEngagedOnContents.length > 0
65
+ return playlistEngagedOnContents.length > 0
70
66
  ? await addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
71
67
  addNavigateTo: true,
72
68
  addProgressStatus: true,
@@ -74,6 +70,4 @@ export async function getPlaylistEngagedOnContent(recentPlaylists){
74
70
  addProgressTimestamp: true,
75
71
  })
76
72
  : []
77
- contents = postProcessBadge(contents)
78
- return contents
79
73
  }
@@ -33,7 +33,6 @@ import {
33
33
  SONG_TYPES_WITH_CHILDREN,
34
34
  liveFields,
35
35
  postProcessBadge,
36
- contentAwardField,
37
36
  parentField,
38
37
  grandParentField,
39
38
  } from '../contentTypeConfig.js'
@@ -47,6 +46,7 @@ import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
47
46
  import { getPermissionsAdapter } from './permissions/index.ts'
48
47
  import { getAllCompleted, getAllStarted, getAllStartedOrCompleted } from './contentProgress.js'
49
48
  import { fetchRecentActivitiesActiveTabs } from './userActivity.js'
49
+ import { COLLECTION_TYPE } from './sync/models/ContentProgress.js'
50
50
 
51
51
  /**
52
52
  * Exported functions that are excluded from index generation.
@@ -1330,7 +1330,15 @@ export async function fetchTopLevelParentId(railcontentId) {
1330
1330
  return response['top_parent'] ?? response['railcontent_id']
1331
1331
  }
1332
1332
 
1333
- export async function fetchLearningPathHierarchy(railcontentId, collection) {
1333
+ export async function getHierarchy(contentId, collection) {
1334
+ if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
1335
+ return await fetchLearningPathHierarchy(contentId, collection)
1336
+ } else {
1337
+ return await fetchHierarchy(contentId)
1338
+ }
1339
+ }
1340
+
1341
+ async function fetchLearningPathHierarchy(railcontentId, collection) {
1334
1342
  if (!collection) {
1335
1343
  return null
1336
1344
  }
@@ -1349,7 +1357,7 @@ export async function fetchLearningPathHierarchy(railcontentId, collection) {
1349
1357
  return data
1350
1358
  }
1351
1359
 
1352
- export async function fetchHierarchy(railcontentId) {
1360
+ async function fetchHierarchy(railcontentId) {
1353
1361
  let topLevelId = await fetchTopLevelParentId(railcontentId)
1354
1362
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1355
1363
  const query = `*[railcontent_id == ${topLevelId}]{
@@ -1897,7 +1905,7 @@ export async function fetchTabData(
1897
1905
 
1898
1906
  switch (progress) {
1899
1907
  case 'recent':
1900
- progressIds = await getAllStartedOrCompleted({ brand, onlyIds: true })
1908
+ progressIds = await getAllStartedOrCompleted({ brand })
1901
1909
  sortOrder = null
1902
1910
  break
1903
1911
  case 'incomplete':
@@ -12,6 +12,7 @@ import {
12
12
  export enum COLLECTION_TYPE {
13
13
  SELF = 'self',
14
14
  LEARNING_PATH = 'learning-path-v2',
15
+ PLAYLIST = 'playlist',
15
16
  }
16
17
  export const COLLECTION_ID_SELF = 0
17
18
 
@@ -1,38 +1,51 @@
1
1
  import SyncRepository, {Q} from './base'
2
2
  import ContentProgress, {COLLECTION_ID_SELF, COLLECTION_TYPE, STATE, CollectionParameter} from '../models/ContentProgress'
3
- import {EpochMs} from "../index";
4
-
5
- interface ContentIdCollectionTuple {
6
- contentId: number,
7
- collection: CollectionParameter | null,
8
- }
9
3
 
10
4
  export default class ProgressRepository extends SyncRepository<ContentProgress> {
11
- // null collection only
12
- async startedIds(limit?: number) {
13
- return this.queryAll(...[
14
- ProgressRepository.filterOutStandardContentsAccessedByLP,
5
+
6
+ async started(
7
+ limit?: number,
8
+ opts: {
9
+ onlyIds?: boolean
10
+ include?: { aLaCarte?: boolean, learningPaths?: boolean }
11
+ } = {}
12
+ ) {
13
+ const results = await this.queryAll(...[
14
+ ProgressRepository.collectionTypeFilter(opts.include),
15
15
 
16
16
  Q.where('state', STATE.STARTED),
17
17
  Q.sortBy('updated_at', 'desc'),
18
18
 
19
19
  ...(limit ? [Q.take(limit)] : []),
20
- ]).then((r) => r.data.map((r) => r.content_id))
20
+ ])
21
+
22
+ return opts.onlyIds
23
+ ? results.data.map((r) => r.content_id)
24
+ : results.data
21
25
  }
22
26
 
23
- // null collection only
24
- async completedIds(limit?: number) {
25
- return this.queryAll(...[
26
- ProgressRepository.filterOutStandardContentsAccessedByLP,
27
+ async completed(
28
+ limit?: number,
29
+ opts: {
30
+ onlyIds?: boolean
31
+ include?: { aLaCarte?: boolean, learningPaths?: boolean }
32
+ } = {}
33
+ ) {
34
+ const results = await this.queryAll(...[
35
+ ProgressRepository.collectionTypeFilter(opts.include),
27
36
 
28
37
  Q.where('state', STATE.COMPLETED),
29
38
  Q.sortBy('updated_at', 'desc'),
30
39
 
31
40
  ...(limit ? [Q.take(limit)] : []),
32
- ]).then((r) => r.data.map((r) => r.content_id))
41
+ ])
42
+
43
+ return opts.onlyIds
44
+ ? results.data.map((r) => r.content_id)
45
+ : results.data
33
46
  }
34
47
 
35
- //this _specifically_ needs to get content_ids from ALL collection_types (including null)
48
+ //this _specifically_ needs to get content_ids from ALL collection_types (including self)
36
49
  async completedByContentIds(contentIds: number[]) {
37
50
  return this.queryAll(
38
51
  Q.where('content_id', Q.oneOf(contentIds)),
@@ -40,21 +53,20 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
40
53
  )
41
54
  }
42
55
 
43
- // null collection only
44
56
  async startedOrCompleted(opts: Parameters<typeof this.startedOrCompletedClauses>[0] = {}) {
45
57
  return this.queryAll(...this.startedOrCompletedClauses(opts))
46
58
  }
47
59
 
48
- // null collection only
49
60
  private startedOrCompletedClauses(
50
61
  opts: {
51
62
  brand?: string | null
63
+ include?: { aLaCarte?: boolean, learningPaths?: boolean },
52
64
  updatedAfter?: number,
53
65
  limit?: number,
54
66
  } = {}
55
67
  ) {
56
68
  const clauses: Q.Clause[] = [
57
- ProgressRepository.filterOutStandardContentsAccessedByLP,
69
+ ProgressRepository.collectionTypeFilter(opts.include),
58
70
 
59
71
  Q.or(Q.where('state', STATE.STARTED), Q.where('state', STATE.COMPLETED)),
60
72
  Q.sortBy('updated_at', 'desc'),
@@ -111,10 +123,8 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
111
123
  return await this.queryAll(...clauses)
112
124
  }
113
125
 
114
- // Two ways of checking this for a given content_id:
115
- // * grab both records (collection_type = self & and collection_type = learning-path-v2), and compare their updated_at timestamps.
116
- // * utilize the new last_interacted_a_la_carte, which is updated whenever the content is accessed OUTSIDE of an LP, and compare THIS with the self updated_at (which will be greater than if it was last accessed from LP)
117
- // I went with the second because it's an easier query
126
+ // utilize last_interacted_a_la_carte of the :self record, which is updated whenever the content is accessed
127
+ // a-la-carte (not in LP), and compare this with updated_at (which will be greater than if it was last accessed from LP)
118
128
  async getSomeProgressWhereLastAccessedFromMethod(contentIds: number[]) {
119
129
  const clauses = [
120
130
  Q.where('content_id', Q.oneOf(contentIds)),
@@ -136,27 +146,15 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
136
146
  return await this.queryAll(...clauses)
137
147
  }
138
148
 
139
- async getSomeProgressByContentIdsAndCollections(tuples: ContentIdCollectionTuple[]) {
140
- const clauses = []
141
-
142
- clauses.push(...tuples.map(tuple => Q.and(...tupleClauses(tuple))))
143
-
144
- return await this.queryAll(Q.or(...clauses))
145
-
146
- function tupleClauses(tuple: ContentIdCollectionTuple) {
147
- return [
148
- Q.where('content_id', tuple.contentId),
149
- Q.where('collection_type', tuple.collection?.type ?? COLLECTION_TYPE.SELF),
150
- Q.where('collection_id', tuple.collection?.id ?? COLLECTION_ID_SELF)
151
- ]
152
- }
149
+ async getSomeProgressByRecordIds(ids: string[]) {
150
+ return await this.readSome(ids)
153
151
  }
154
152
 
155
- recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number, {skipPush = false, fromLearningPath = false} = {}) {
153
+ recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number, {skipPush = false, accessedDirectly = true} = {}) {
156
154
  const id = ContentProgress.generateId(contentId, collection)
157
155
 
158
156
  if (collection?.type === COLLECTION_TYPE.LEARNING_PATH) {
159
- fromLearningPath = true
157
+ accessedDirectly = false
160
158
  }
161
159
 
162
160
  const result = this.upsertOne(id, (r) => {
@@ -172,7 +170,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
172
170
  }
173
171
  }
174
172
 
175
- if (!fromLearningPath) {
173
+ if (accessedDirectly && r.collection_type === COLLECTION_TYPE.SELF) {
176
174
  r.last_interacted_a_la_carte = r.updated_at
177
175
  }
178
176
 
@@ -206,10 +204,10 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
206
204
  recordProgressMany(
207
205
  contentProgresses: Record<string, number>, // Accept plain object
208
206
  collection: CollectionParameter | null,
209
- { skipPush = false, fromLearningPath = false }: { skipPush?: boolean; fromLearningPath?: boolean } = {}
207
+ { skipPush = false, accessedDirectly = true }: { skipPush?: boolean; accessedDirectly?: boolean } = {}
210
208
  ) {
211
209
  if (collection?.type === COLLECTION_TYPE.LEARNING_PATH) {
212
- fromLearningPath = true
210
+ accessedDirectly = false
213
211
  }
214
212
 
215
213
  const data = Object.fromEntries(
@@ -222,7 +220,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
222
220
 
223
221
  r.progress_percent = progressPct
224
222
 
225
- if (!fromLearningPath) {
223
+ if (accessedDirectly && r.collection_type === COLLECTION_TYPE.SELF) {
226
224
  r.last_interacted_a_la_carte = r.updated_at
227
225
  }
228
226
  },
@@ -242,18 +240,33 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
242
240
  return this.deleteSome(ids, { skipPush })
243
241
  }
244
242
 
245
- private static filterOutStandardContentsAccessedByLP =
246
- // LPs dont have last_interacted_a_la_carte set, hence the OR
247
- Q.or(
248
- Q.and( // a-la-carte content that's been accessed directly
249
- Q.where('collection_type', COLLECTION_TYPE.SELF),
250
- Q.where('collection_id', COLLECTION_ID_SELF),
251
- Q.where('last_interacted_a_la_carte', Q.notEq(null)),
252
- ),
253
- Q.and( // learning paths (parents)
254
- Q.where('collection_type', COLLECTION_TYPE.LEARNING_PATH),
255
- Q.where('content_id', Q.eq(Q.column('collection_id')))
243
+ static collectionTypeFilter(
244
+ params: {
245
+ aLaCarte?: boolean;
246
+ learningPaths?: boolean
247
+ } = {}) {
248
+ let clauses: Q.Where[] = []
249
+
250
+ if (params.aLaCarte) {
251
+ clauses.push(
252
+ Q.and( // a-la-carte content that's been accessed directly
253
+ Q.where('collection_type', COLLECTION_TYPE.SELF),
254
+ Q.where('collection_id', COLLECTION_ID_SELF),
255
+ Q.where('last_interacted_a_la_carte', Q.notEq(null)),
256
+ ),
256
257
  )
257
- )
258
+ }
258
259
 
260
+ if (params.learningPaths) {
261
+ clauses.push(
262
+ Q.and( // just parents
263
+ Q.where('collection_type', COLLECTION_TYPE.LEARNING_PATH),
264
+ Q.where('content_id', Q.eq(Q.column('collection_id')))
265
+ )
266
+ )
267
+ }
268
+
269
+ if (clauses.length === 0) return
270
+ return Q.or(...clauses)
271
+ }
259
272
  }