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 +12 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +4 -3
- package/src/index.d.ts +11 -9
- package/src/index.js +11 -9
- package/src/services/contentAggregator.js +19 -25
- package/src/services/contentProgress.js +93 -54
- package/src/services/progress-row/base.js +8 -12
- package/src/services/progress-row/rows/content-card.js +6 -13
- package/src/services/progress-row/rows/playlist-card.js +17 -23
- package/src/services/sanity.js +12 -4
- package/src/services/sync/models/ContentProgress.ts +1 -0
- package/src/services/sync/repositories/content-progress.ts +69 -56
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
package/src/contentTypeConfig.js
CHANGED
|
@@ -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
|
-
|
|
127
|
+
getProgressDataByRecordIds,
|
|
127
128
|
getProgressState,
|
|
128
129
|
getProgressStateByIds,
|
|
130
|
+
getProgressStateByRecordIds,
|
|
129
131
|
getResumeTimeSecondsByIds,
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
+
getProgressDataByRecordIds,
|
|
131
132
|
getProgressState,
|
|
132
133
|
getProgressStateByIds,
|
|
134
|
+
getProgressStateByRecordIds,
|
|
133
135
|
getResumeTimeSecondsByIds,
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
6
|
+
getProgressDataByRecordIds,
|
|
7
|
+
getProgressStateByRecordIds,
|
|
7
8
|
getResumeTimeSecondsByIds,
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
?
|
|
202
|
-
addIsLiked ? isContentLikedByIds(
|
|
203
|
-
addResumeTimeSeconds ?
|
|
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(
|
|
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
|
-
|
|
243
|
-
playlists.
|
|
244
|
-
|
|
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((
|
|
252
|
-
const
|
|
253
|
-
|
|
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
|
|
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
|
|
32
|
-
return
|
|
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<
|
|
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
|
|
262
|
+
* const progress = await getProgressDataByRecordIds(tuples)
|
|
259
263
|
* // Returns: { 123: { progress: 50, status: 'started', last_update: 123456 }, ... }
|
|
260
264
|
*/
|
|
261
265
|
|
|
262
|
-
|
|
263
|
-
|
|
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.
|
|
273
|
+
await db.contentProgress.getSomeProgressByRecordIds(ids).then(r => {
|
|
274
274
|
r.data.forEach(p => {
|
|
275
|
-
progress[p.
|
|
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
|
|
307
|
-
|
|
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.
|
|
307
|
+
await db.contentProgress.getSomeProgressByRecordIds(ids).then(r => {
|
|
311
308
|
r.data.forEach(p => {
|
|
312
|
-
progress[p.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
671
|
-
|
|
708
|
+
function filterGreaterThanProgress(progresses, external) {
|
|
672
709
|
// overwrite if LP progress greater, unless LP progress was reset to 0
|
|
673
|
-
|
|
674
|
-
const extPct =
|
|
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
|
-
|
|
681
|
-
|
|
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 ===
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
188
|
+
async function getCards(brand, limit, userPinnedItem) {
|
|
193
189
|
return Promise.all([
|
|
194
|
-
getContentCardMap(brand, limit,
|
|
190
|
+
getContentCardMap(brand, limit, userPinnedItem).catch(e => {
|
|
195
191
|
console.error('getContentCardMap failed:', e)
|
|
196
192
|
return new Map()
|
|
197
193
|
}),
|
|
198
|
-
getPlaylistCards(
|
|
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 =
|
|
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,
|
|
23
|
-
let recentContentIds = await getAllStartedOrCompleted({
|
|
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
|
-
|
|
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(
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1908
|
+
progressIds = await getAllStartedOrCompleted({ brand })
|
|
1901
1909
|
sortOrder = null
|
|
1902
1910
|
break
|
|
1903
1911
|
case 'incomplete':
|
|
@@ -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
|
-
|
|
12
|
-
async
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
])
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
return opts.onlyIds
|
|
23
|
+
? results.data.map((r) => r.content_id)
|
|
24
|
+
: results.data
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
])
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
115
|
-
//
|
|
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
|
|
140
|
-
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
207
|
+
{ skipPush = false, accessedDirectly = true }: { skipPush?: boolean; accessedDirectly?: boolean } = {}
|
|
210
208
|
) {
|
|
211
209
|
if (collection?.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
212
|
-
|
|
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 (
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Q.
|
|
255
|
-
|
|
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
|
}
|