musora-content-services 2.96.2 → 2.97.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 +7 -0
- package/package.json +1 -1
- package/src/index.d.ts +8 -0
- package/src/index.js +8 -0
- package/src/services/content-org/learning-paths.ts +15 -2
- package/src/services/contentAggregator.js +161 -2
- package/src/services/contentProgress.js +122 -4
- package/src/services/sanity.js +6 -0
- package/src/services/sync/repositories/content-progress.ts +35 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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.97.0](https://github.com/railroadmedia/musora-content-services/compare/v2.96.2...v2.97.0) (2025-12-09)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEH-1458:** method addcontexttocontent ([#633](https://github.com/railroadmedia/musora-content-services/issues/633)) ([95a2b0a](https://github.com/railroadmedia/musora-content-services/commit/95a2b0ae3f011e17f92b78ad4fcd1ebe7a2342ca))
|
|
11
|
+
|
|
5
12
|
### [2.96.2](https://github.com/railroadmedia/musora-content-services/compare/v2.96.1...v2.96.2) (2025-12-09)
|
|
6
13
|
|
|
7
14
|
### [2.96.1](https://github.com/railroadmedia/musora-content-services/compare/v2.96.0...v2.96.1) (2025-12-09)
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
|
|
91
91
|
import {
|
|
92
92
|
addContextToContent,
|
|
93
|
+
addContextToLearningPaths,
|
|
93
94
|
getNavigateToForPlaylists
|
|
94
95
|
} from './services/contentAggregator.js';
|
|
95
96
|
|
|
@@ -111,10 +112,13 @@ import {
|
|
|
111
112
|
getAllStartedOrCompleted,
|
|
112
113
|
getLastInteractedOf,
|
|
113
114
|
getNavigateTo,
|
|
115
|
+
getNavigateToForMethod,
|
|
114
116
|
getProgressDataByIds,
|
|
117
|
+
getProgressDataByIdsAndCollections,
|
|
115
118
|
getProgressState,
|
|
116
119
|
getProgressStateByIds,
|
|
117
120
|
getResumeTimeSecondsByIds,
|
|
121
|
+
getResumeTimeSecondsByIdsAndCollections,
|
|
118
122
|
getStartedOrCompletedProgressOnly,
|
|
119
123
|
recordWatchSession
|
|
120
124
|
} from './services/contentProgress.js';
|
|
@@ -426,6 +430,7 @@ import {
|
|
|
426
430
|
declare module 'musora-content-services' {
|
|
427
431
|
export {
|
|
428
432
|
addContextToContent,
|
|
433
|
+
addContextToLearningPaths,
|
|
429
434
|
addItemToPlaylist,
|
|
430
435
|
applyCloudflareWrapper,
|
|
431
436
|
applySanityTransformations,
|
|
@@ -590,6 +595,7 @@ declare module 'musora-content-services' {
|
|
|
590
595
|
getMethodCard,
|
|
591
596
|
getMonday,
|
|
592
597
|
getNavigateTo,
|
|
598
|
+
getNavigateToForMethod,
|
|
593
599
|
getNavigateToForPlaylists,
|
|
594
600
|
getNewAndUpcoming,
|
|
595
601
|
getOnboardingRecommendedContent,
|
|
@@ -597,6 +603,7 @@ declare module 'musora-content-services' {
|
|
|
597
603
|
getPracticeNotes,
|
|
598
604
|
getPracticeSessions,
|
|
599
605
|
getProgressDataByIds,
|
|
606
|
+
getProgressDataByIdsAndCollections,
|
|
600
607
|
getProgressRows,
|
|
601
608
|
getProgressState,
|
|
602
609
|
getProgressStateByIds,
|
|
@@ -605,6 +612,7 @@ declare module 'musora-content-services' {
|
|
|
605
612
|
getRecommendedForYou,
|
|
606
613
|
getReportIssueOptions,
|
|
607
614
|
getResumeTimeSecondsByIds,
|
|
615
|
+
getResumeTimeSecondsByIdsAndCollections,
|
|
608
616
|
getSanityDate,
|
|
609
617
|
getScheduleContentRows,
|
|
610
618
|
getSortOrder,
|
package/src/index.js
CHANGED
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
|
|
95
95
|
import {
|
|
96
96
|
addContextToContent,
|
|
97
|
+
addContextToLearningPaths,
|
|
97
98
|
getNavigateToForPlaylists
|
|
98
99
|
} from './services/contentAggregator.js';
|
|
99
100
|
|
|
@@ -115,10 +116,13 @@ import {
|
|
|
115
116
|
getAllStartedOrCompleted,
|
|
116
117
|
getLastInteractedOf,
|
|
117
118
|
getNavigateTo,
|
|
119
|
+
getNavigateToForMethod,
|
|
118
120
|
getProgressDataByIds,
|
|
121
|
+
getProgressDataByIdsAndCollections,
|
|
119
122
|
getProgressState,
|
|
120
123
|
getProgressStateByIds,
|
|
121
124
|
getResumeTimeSecondsByIds,
|
|
125
|
+
getResumeTimeSecondsByIdsAndCollections,
|
|
122
126
|
getStartedOrCompletedProgressOnly,
|
|
123
127
|
recordWatchSession
|
|
124
128
|
} from './services/contentProgress.js';
|
|
@@ -425,6 +429,7 @@ import {
|
|
|
425
429
|
|
|
426
430
|
export {
|
|
427
431
|
addContextToContent,
|
|
432
|
+
addContextToLearningPaths,
|
|
428
433
|
addItemToPlaylist,
|
|
429
434
|
applyCloudflareWrapper,
|
|
430
435
|
applySanityTransformations,
|
|
@@ -589,6 +594,7 @@ export {
|
|
|
589
594
|
getMethodCard,
|
|
590
595
|
getMonday,
|
|
591
596
|
getNavigateTo,
|
|
597
|
+
getNavigateToForMethod,
|
|
592
598
|
getNavigateToForPlaylists,
|
|
593
599
|
getNewAndUpcoming,
|
|
594
600
|
getOnboardingRecommendedContent,
|
|
@@ -596,6 +602,7 @@ export {
|
|
|
596
602
|
getPracticeNotes,
|
|
597
603
|
getPracticeSessions,
|
|
598
604
|
getProgressDataByIds,
|
|
605
|
+
getProgressDataByIdsAndCollections,
|
|
599
606
|
getProgressRows,
|
|
600
607
|
getProgressState,
|
|
601
608
|
getProgressStateByIds,
|
|
@@ -604,6 +611,7 @@ export {
|
|
|
604
611
|
getRecommendedForYou,
|
|
605
612
|
getReportIssueOptions,
|
|
606
613
|
getResumeTimeSecondsByIds,
|
|
614
|
+
getResumeTimeSecondsByIdsAndCollections,
|
|
607
615
|
getSanityDate,
|
|
608
616
|
getScheduleContentRows,
|
|
609
617
|
getSortOrder,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { COLLECTION_TYPE, STATE } from '../sync/models/ContentProgress'
|
|
16
16
|
import { SyncWriteDTO } from '../sync'
|
|
17
17
|
import { ContentProgress } from '../sync/models'
|
|
18
|
+
import { CollectionParameter } from '../sync/repositories/content-progress'
|
|
18
19
|
|
|
19
20
|
const BASE_PATH: string = `/api/content-org`
|
|
20
21
|
const LEARNING_PATHS_PATH = `${BASE_PATH}/v1/user/learning-paths`
|
|
@@ -39,6 +40,11 @@ interface DailySession {
|
|
|
39
40
|
learning_path_id: number
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
interface CollectionObject {
|
|
44
|
+
id: number
|
|
45
|
+
type: COLLECTION_TYPE.LEARNING_PATH
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* Gets today's daily session for the user.
|
|
44
50
|
* @param brand
|
|
@@ -308,10 +314,11 @@ export async function completeLearningPathIntroVideo(
|
|
|
308
314
|
|
|
309
315
|
response.intro_video_response = await completeIfNotCompleted(introVideoId)
|
|
310
316
|
|
|
311
|
-
const collection = { id: learningPathId, type: COLLECTION_TYPE.LEARNING_PATH }
|
|
317
|
+
const collection: CollectionObject = { id: learningPathId, type: COLLECTION_TYPE.LEARNING_PATH }
|
|
312
318
|
|
|
313
319
|
if (!lessonsToImport) {
|
|
314
|
-
response.learning_path_reset_response = await
|
|
320
|
+
response.learning_path_reset_response = await resetIfPossible(learningPathId, collection)
|
|
321
|
+
|
|
315
322
|
} else {
|
|
316
323
|
response.lesson_import_response = await contentsStatusCompleted(lessonsToImport, collection)
|
|
317
324
|
}
|
|
@@ -327,6 +334,12 @@ async function completeIfNotCompleted(
|
|
|
327
334
|
return introVideoStatus !== 'completed' ? await contentStatusCompleted(contentId) : null
|
|
328
335
|
}
|
|
329
336
|
|
|
337
|
+
async function resetIfPossible(contentId: number, collection: CollectionParameter = null): Promise<SyncWriteDTO<ContentProgress, any> | null> {
|
|
338
|
+
const status = await getProgressState(contentId, collection)
|
|
339
|
+
|
|
340
|
+
return status !== '' ? await contentStatusReset(contentId, collection) : null
|
|
341
|
+
}
|
|
342
|
+
|
|
330
343
|
export async function onContentCompletedLearningPathListener(event) {
|
|
331
344
|
console.log('if')
|
|
332
345
|
if (event?.collection?.type !== 'learning-path-v2') return
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getNavigateTo,
|
|
3
|
+
getNavigateToForMethod,
|
|
3
4
|
getProgressDataByIds,
|
|
5
|
+
getProgressDataByIdsAndCollections,
|
|
4
6
|
getProgressStateByIds,
|
|
5
7
|
getResumeTimeSecondsByIds,
|
|
8
|
+
getResumeTimeSecondsByIdsAndCollections,
|
|
6
9
|
} from './contentProgress'
|
|
7
10
|
import { isContentLikedByIds } from './contentLikes'
|
|
8
11
|
import { fetchLastInteractedChild, fetchLikeCount } from './railcontent'
|
|
12
|
+
import {COLLECTION_TYPE} from "./sync/models/ContentProgress";
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Combine sanity data with BE contextual data.
|
|
@@ -54,12 +58,11 @@ import { fetchLastInteractedChild, fetchLikeCount } from './railcontent'
|
|
|
54
58
|
*
|
|
55
59
|
*/
|
|
56
60
|
|
|
57
|
-
// need to add method support.
|
|
58
|
-
// this means returning collection_type and collection_id
|
|
59
61
|
export async function addContextToContent(dataPromise, ...dataArgs) {
|
|
60
62
|
const lastArg = dataArgs[dataArgs.length - 1]
|
|
61
63
|
const options = typeof lastArg === 'object' && !Array.isArray(lastArg) ? lastArg : {}
|
|
62
64
|
|
|
65
|
+
// todo: merge addProgressData with addResumeTimeSeconds to one watermelon call
|
|
63
66
|
const {
|
|
64
67
|
collection = null, // this is needed for different collection types like learning paths. has .id and .type
|
|
65
68
|
dataField = null,
|
|
@@ -115,6 +118,121 @@ export async function addContextToContent(dataPromise, ...dataArgs) {
|
|
|
115
118
|
return await processItems(data, addContext, dataField, isDataAnArray, dataField_includeParent)
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Enriches method content (learning paths) with contextual data.
|
|
123
|
+
*
|
|
124
|
+
* Key behaviors:
|
|
125
|
+
* 1. Enriches all learning paths in a method structure
|
|
126
|
+
* 2. Auto-sets collection for learning-path-v2 items when no collection specified
|
|
127
|
+
* 3. Enriches intro videos when dataField_includeIntroVideo is true
|
|
128
|
+
*
|
|
129
|
+
* @param dataPromise - promise or method that provides sanity data
|
|
130
|
+
* @param dataArgs - Arguments to pass to the dataPromise
|
|
131
|
+
* @param options - Same as addContextToContent, plus:
|
|
132
|
+
* @param options.dataField_includeIntroVideo - If true, adds progress to intro_video field where it exists
|
|
133
|
+
*
|
|
134
|
+
* @returns {Promise<Object | false>} - Enriched data or false if no data found
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* // Enrich method structure with all learning paths
|
|
138
|
+
* const method = await addContextToMethodContent(fetchMethodV2Structure, brand, {
|
|
139
|
+
* dataField: 'learningPaths',
|
|
140
|
+
* dataField_includeIntroVideo: true,
|
|
141
|
+
* addProgressStatus: true,
|
|
142
|
+
* addProgressPercentage: true,
|
|
143
|
+
* })
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* // Enrich single learning path with intro video
|
|
147
|
+
* const lp = await addContextToMethodContent(fetchByRailContentId, lpId, 'learning-path-v2', {
|
|
148
|
+
* collection: { id: lpId, type: 'learning-path-v2' },
|
|
149
|
+
* dataField: 'children',
|
|
150
|
+
* dataField_includeParent: true,
|
|
151
|
+
* dataField_includeIntroVideo: true,
|
|
152
|
+
* addProgressStatus: true,
|
|
153
|
+
* })
|
|
154
|
+
*/
|
|
155
|
+
export async function addContextToLearningPaths(dataPromise, ...dataArgs) {
|
|
156
|
+
const lastArg = dataArgs[dataArgs.length - 1]
|
|
157
|
+
const options = typeof lastArg === 'object' && !Array.isArray(lastArg) ? lastArg : {}
|
|
158
|
+
|
|
159
|
+
// todo: merge addProgressData with addResumeTimeSeconds to one watermelon call
|
|
160
|
+
const {
|
|
161
|
+
dataField = null,
|
|
162
|
+
dataField_includeParent = false,
|
|
163
|
+
dataField_includeIntroVideo = false,
|
|
164
|
+
addProgressPercentage = false,
|
|
165
|
+
addProgressStatus = false,
|
|
166
|
+
addProgressTimestamp = false,
|
|
167
|
+
addIsLiked = false,
|
|
168
|
+
addLikeCount = false,
|
|
169
|
+
addResumeTimeSeconds = false,
|
|
170
|
+
addNavigateTo = false,
|
|
171
|
+
} = options
|
|
172
|
+
|
|
173
|
+
const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
|
|
174
|
+
|
|
175
|
+
let data = await dataPromise(...dataParam)
|
|
176
|
+
const isDataAnArray = Array.isArray(data)
|
|
177
|
+
if (isDataAnArray && data.length === 0) return data
|
|
178
|
+
if (!data) return false
|
|
179
|
+
|
|
180
|
+
let items = extractItemsWithCollectionFromMethodData(data, dataField, isDataAnArray, dataField_includeParent, dataField_includeIntroVideo) ?? []
|
|
181
|
+
if (items.length === 0) return data
|
|
182
|
+
|
|
183
|
+
let ids = items.map((item) => (
|
|
184
|
+
{
|
|
185
|
+
contentId: item.content?.id,
|
|
186
|
+
collection: item.collection
|
|
187
|
+
})
|
|
188
|
+
).filter(obj => obj.contentId)
|
|
189
|
+
|
|
190
|
+
const justIds = ids.map(obj => obj.contentId)
|
|
191
|
+
|
|
192
|
+
const [
|
|
193
|
+
progressData,
|
|
194
|
+
isLikedData,
|
|
195
|
+
resumeTimeData,
|
|
196
|
+
navigateToData,
|
|
197
|
+
] = await Promise.all([
|
|
198
|
+
addProgressPercentage || addProgressStatus || addProgressTimestamp
|
|
199
|
+
? getProgressDataByIdsAndCollections(ids) : Promise.resolve(null),
|
|
200
|
+
addIsLiked ? isContentLikedByIds(justIds) : Promise.resolve(null),
|
|
201
|
+
addResumeTimeSeconds ? getResumeTimeSecondsByIdsAndCollections(ids) : Promise.resolve(null),
|
|
202
|
+
addNavigateTo ? getNavigateToForMethod(items) : Promise.resolve(null),
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
const addContext = async (item) => {
|
|
206
|
+
const itemId = item.id || 0
|
|
207
|
+
const enrichedItem = {
|
|
208
|
+
...item,
|
|
209
|
+
...(addProgressPercentage ? { progressPercentage: progressData?.[itemId]?.progress } : {}),
|
|
210
|
+
...(addProgressStatus ? { progressStatus: progressData?.[itemId]?.status } : {}),
|
|
211
|
+
...(addProgressTimestamp ? { progressTimestamp: progressData?.[itemId]?.last_update } : {}),
|
|
212
|
+
...(addIsLiked ? { isLiked: isLikedData?.[itemId] } : {}),
|
|
213
|
+
...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(itemId) } : {}),
|
|
214
|
+
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[itemId] } : {}),
|
|
215
|
+
...(addNavigateTo ? { navigateTo: navigateToData?.[itemId] } : {}),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Enrich intro_video if it exists and flag is set
|
|
219
|
+
if (dataField_includeIntroVideo && item?.intro_video?.id) {
|
|
220
|
+
enrichedItem.intro_video = {
|
|
221
|
+
...item.intro_video,
|
|
222
|
+
...(addProgressPercentage ? { progressPercentage: progressData?.[item.intro_video.id]?.progress } : {}),
|
|
223
|
+
...(addProgressStatus ? { progressStatus: progressData?.[item.intro_video.id]?.status } : {}),
|
|
224
|
+
...(addProgressTimestamp ? { progressTimestamp: progressData?.[item.intro_video.id]?.last_update } : {}),
|
|
225
|
+
...(addIsLiked ? { isLiked: isLikedData?.[item.intro_video.id] } : {}),
|
|
226
|
+
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.intro_video.id] } : {}),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return enrichedItem
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return await processItems(data, addContext, dataField, isDataAnArray, dataField_includeParent)
|
|
234
|
+
}
|
|
235
|
+
|
|
118
236
|
export async function getNavigateToForPlaylists(data, { dataField = null } = {}) {
|
|
119
237
|
let playlists = extractItemsFromData(data, dataField, false, false)
|
|
120
238
|
let allIds = []
|
|
@@ -191,6 +309,47 @@ function extractItemsFromData(data, dataField, isParentArray, includeParent) {
|
|
|
191
309
|
return items
|
|
192
310
|
}
|
|
193
311
|
|
|
312
|
+
function extractItemsWithCollectionFromMethodData(data, dataField, isDataAnArray, includeParent, includeIntroVideo) {
|
|
313
|
+
let items = [] // array of tuples {}
|
|
314
|
+
|
|
315
|
+
const extractLearningPathItems = (item) => {
|
|
316
|
+
if (item.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
317
|
+
const c = {type: COLLECTION_TYPE.LEARNING_PATH, id: item.id}
|
|
318
|
+
|
|
319
|
+
if (!dataField || (dataField && includeParent)) {
|
|
320
|
+
items.push(...getDataTuple([item], c))
|
|
321
|
+
}
|
|
322
|
+
if (includeIntroVideo) {
|
|
323
|
+
items.push(...getDataTuple([item.intro_video], null))
|
|
324
|
+
}
|
|
325
|
+
if (dataField) {
|
|
326
|
+
items.push(...getDataTuple(item[dataField], c))
|
|
327
|
+
}
|
|
328
|
+
} else { // is a lesson id, cant determine which collection it belongs to
|
|
329
|
+
// do not add it as we cant determine collection
|
|
330
|
+
// items.push(...getDataTuple([data], collection))
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (isDataAnArray) {
|
|
335
|
+
for (const item of data) {
|
|
336
|
+
extractLearningPathItems(item)
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
extractLearningPathItems(data)
|
|
340
|
+
}
|
|
341
|
+
return items
|
|
342
|
+
|
|
343
|
+
function getDataTuple(data, collection) {
|
|
344
|
+
const tuples = []
|
|
345
|
+
for (const item of data) {
|
|
346
|
+
const coll = collection || null
|
|
347
|
+
tuples.push({content: item, collection: coll})
|
|
348
|
+
}
|
|
349
|
+
return tuples
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
194
353
|
async function processItems(data, addContext, dataField, isParentArray, includeParent) {
|
|
195
354
|
if (dataField) {
|
|
196
355
|
if (isParentArray) {
|
|
@@ -4,13 +4,15 @@ import { COLLECTION_TYPE, STATE } from './sync/models/ContentProgress'
|
|
|
4
4
|
import { trackUserPractice, findIncompleteLesson } from './userActivity'
|
|
5
5
|
import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
|
|
6
6
|
import { emitContentCompleted } from './progress-events'
|
|
7
|
+
import {getDailySession} from "./content-org/learning-paths.js";
|
|
8
|
+
import {getToday} from "./dateUtils.js";
|
|
7
9
|
|
|
8
10
|
const STATE_STARTED = STATE.STARTED
|
|
9
11
|
const STATE_COMPLETED = STATE.COMPLETED
|
|
10
12
|
const MAX_DEPTH = 3
|
|
11
13
|
|
|
12
|
-
export async function getProgressState(contentId) {
|
|
13
|
-
return getById(contentId, 'state', '')
|
|
14
|
+
export async function getProgressState(contentId, collection = null) {
|
|
15
|
+
return getById(contentId, collection, 'state', '')
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export async function getProgressStateByIds(contentIds, collection = null) {
|
|
@@ -26,6 +28,68 @@ export async function getResumeTimeSecondsByIds(contentIds, collection = null) {
|
|
|
26
28
|
)
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export async function getResumeTimeSecondsByIdsAndCollections(tuples) {
|
|
32
|
+
return getByIdsAndCollections(tuples, 'resume_time_seconds', 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getNavigateToForMethod(data) {
|
|
36
|
+
let navigateToData = {}
|
|
37
|
+
|
|
38
|
+
const brand = data[0].content.brand || null
|
|
39
|
+
const dailySessionResponse = await getDailySession(brand, getToday())
|
|
40
|
+
const dailySession = dailySessionResponse?.daily_session || null
|
|
41
|
+
const activeLearningPathId = dailySessionResponse?.active_learning_path_id || null
|
|
42
|
+
|
|
43
|
+
for (const tuple of data) {
|
|
44
|
+
if (!tuple) continue
|
|
45
|
+
|
|
46
|
+
const {content, collection} = tuple
|
|
47
|
+
|
|
48
|
+
const findFirstIncomplete = (progresses) =>
|
|
49
|
+
Object.keys(progresses).find(id => progresses[id] !== STATE_COMPLETED) || null
|
|
50
|
+
|
|
51
|
+
const findChildById = (children, id) =>
|
|
52
|
+
children?.find(child => child.id === Number(id)) || null
|
|
53
|
+
|
|
54
|
+
const getFirstOrIncompleteChild = async (content, collection) => {
|
|
55
|
+
const childrenIds = content?.children.map(child => child.id) || []
|
|
56
|
+
if (childrenIds.length === 0) return null
|
|
57
|
+
|
|
58
|
+
const progresses = await getProgressStateByIds(childrenIds, collection)
|
|
59
|
+
const incompleteId = findFirstIncomplete(progresses)
|
|
60
|
+
|
|
61
|
+
return incompleteId ? findChildById(content.children, incompleteId) : content.children[0]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const getDailySessionNavigateTo = async (content, dailySession, collection) => {
|
|
65
|
+
const dailiesIds = dailySession?.map(item => item.content_ids).flat() || []
|
|
66
|
+
const progresses = await getProgressStateByIds(dailiesIds, collection)
|
|
67
|
+
const incompleteId = findFirstIncomplete(progresses)
|
|
68
|
+
|
|
69
|
+
return incompleteId ? findChildById(content.children, incompleteId) : null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// does not support passing in 'method-v2' type yet
|
|
73
|
+
if (content.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
74
|
+
let navigateTo = null
|
|
75
|
+
|
|
76
|
+
if (content.id === activeLearningPathId) {
|
|
77
|
+
navigateTo = await getDailySessionNavigateTo(content, dailySession, collection)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!navigateTo) {
|
|
81
|
+
navigateTo = await getFirstOrIncompleteChild(content, collection)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
navigateToData[content.id] =buildNavigateTo(navigateTo, null, collection)
|
|
85
|
+
|
|
86
|
+
} else {
|
|
87
|
+
navigateToData[content.id] = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return navigateToData
|
|
91
|
+
}
|
|
92
|
+
|
|
29
93
|
export async function getNavigateTo(data, collection = null) {
|
|
30
94
|
collection = normalizeCollection(collection)
|
|
31
95
|
let navigateToData = {}
|
|
@@ -178,10 +242,52 @@ export async function getProgressDataByIds(contentIds, collection) {
|
|
|
178
242
|
return progress
|
|
179
243
|
}
|
|
180
244
|
|
|
181
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Get progress data for multiple content IDs, each with their own collection context.
|
|
247
|
+
* Useful when fetching progress for tuples that belong to different collections.
|
|
248
|
+
*
|
|
249
|
+
* @param {Array<{contentId: number, collection: {type: string, id: number}|null}>} tuples - Array of objects with contentId and collection
|
|
250
|
+
* @returns {Promise<Object>} - Object mapping content IDs to progress data
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* const tuples = [
|
|
254
|
+
* { contentId: 123, collection: { id: 456, type: 'learning-path-v2' } },
|
|
255
|
+
* { contentId: 789, collection: { id: 101, type: 'learning-path-v2' } },
|
|
256
|
+
* { contentId: 111, collection: null }
|
|
257
|
+
* ]
|
|
258
|
+
* const progress = await getProgressDataByIdsAndCollections(tuples)
|
|
259
|
+
* // Returns: { 123: { progress: 50, status: 'started', last_update: 123456 }, ... }
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
// todo: warning: this doesnt work with having 2 items with same contentId but different collection, because
|
|
263
|
+
// of the response structure here with contentId as key
|
|
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, {
|
|
267
|
+
last_update: 0,
|
|
268
|
+
progress: 0,
|
|
269
|
+
status: '',
|
|
270
|
+
collection: {},
|
|
271
|
+
}]))
|
|
272
|
+
|
|
273
|
+
await db.contentProgress.getSomeProgressByContentIdsAndCollection(tuples).then(r => {
|
|
274
|
+
r.data.forEach(p => {
|
|
275
|
+
progress[p.content_id] = {
|
|
276
|
+
last_update: p.updated_at,
|
|
277
|
+
progress: p.progress_percent,
|
|
278
|
+
status: p.state,
|
|
279
|
+
collection: (p.collection_type && p.collection_id) ? {type: p.collection_type, id: p.collection_id} : null
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return progress
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function getById(contentId, collection, dataKey, defaultValue) {
|
|
182
288
|
if (!contentId) return defaultValue
|
|
183
289
|
return db.contentProgress
|
|
184
|
-
.getOneProgressByContentId(contentId)
|
|
290
|
+
.getOneProgressByContentId(contentId, collection)
|
|
185
291
|
.then((r) => r.data?.[dataKey] ?? defaultValue)
|
|
186
292
|
}
|
|
187
293
|
|
|
@@ -197,6 +303,18 @@ async function getByIds(contentIds, collection, dataKey, defaultValue) {
|
|
|
197
303
|
return progress
|
|
198
304
|
}
|
|
199
305
|
|
|
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]))
|
|
309
|
+
|
|
310
|
+
await db.contentProgress.getSomeProgressByContentIdsAndCollection(tuples).then(r => {
|
|
311
|
+
r.data.forEach(p => {
|
|
312
|
+
progress[p.content_id] = p[dataKey] ?? defaultValue
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
return progress
|
|
316
|
+
}
|
|
317
|
+
|
|
200
318
|
export async function getAllStarted(limit = null) {
|
|
201
319
|
return db.contentProgress.startedIds(limit).then((r) => r.data.map((id) => parseInt(id)))
|
|
202
320
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -2000,8 +2000,11 @@ export async function fetchMethodV2Structure(brand) {
|
|
|
2000
2000
|
const _type = 'method-v2'
|
|
2001
2001
|
const query = `*[_type == '${_type}' && brand == '${brand}'][0...1]{
|
|
2002
2002
|
'sanity_id': _id,
|
|
2003
|
+
brand,
|
|
2004
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2003
2005
|
'learning_paths': child[]->{
|
|
2004
2006
|
'id': railcontent_id,
|
|
2007
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2005
2008
|
'children': child[]->railcontent_id
|
|
2006
2009
|
}
|
|
2007
2010
|
}`
|
|
@@ -2017,8 +2020,11 @@ export async function fetchMethodV2StructureFromId(contentId) {
|
|
|
2017
2020
|
const _type = "method-v2";
|
|
2018
2021
|
const query = `*[_type == '${_type}' && brand == *[railcontent_id == ${contentId}][0].brand][0...1]{
|
|
2019
2022
|
'sanity_id': _id,
|
|
2023
|
+
brand,
|
|
2024
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2020
2025
|
'learning_paths': child[]->{
|
|
2021
2026
|
'id': railcontent_id,
|
|
2027
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2022
2028
|
'children': child[]->railcontent_id
|
|
2023
2029
|
}
|
|
2024
2030
|
}`
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import SyncRepository, {
|
|
2
|
-
import ContentProgress, { COLLECTION_TYPE,
|
|
1
|
+
import SyncRepository, {Q} from './base'
|
|
2
|
+
import ContentProgress, {COLLECTION_ID_SELF, COLLECTION_TYPE, STATE} from '../models/ContentProgress'
|
|
3
3
|
|
|
4
|
+
interface ContentIdCollectionTuple {
|
|
5
|
+
contentId: number,
|
|
6
|
+
collection: CollectionParameter | null,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CollectionParameter {
|
|
10
|
+
type: COLLECTION_TYPE,
|
|
11
|
+
id: number,
|
|
12
|
+
}
|
|
4
13
|
export default class ProgressRepository extends SyncRepository<ContentProgress> {
|
|
5
14
|
// null collection only
|
|
6
15
|
async startedIds(limit?: number) {
|
|
@@ -77,7 +86,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
77
86
|
return clauses
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
async mostRecentlyUpdatedId(contentIds: number[], collection:
|
|
89
|
+
async mostRecentlyUpdatedId(contentIds: number[], collection: CollectionParameter | null = null) {
|
|
81
90
|
return this.queryOneId(
|
|
82
91
|
Q.where('content_id', Q.oneOf(contentIds)),
|
|
83
92
|
Q.where('collection_type', collection?.type ?? COLLECTION_TYPE.SELF),
|
|
@@ -89,7 +98,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
89
98
|
|
|
90
99
|
async getOneProgressByContentId(
|
|
91
100
|
contentId: number,
|
|
92
|
-
|
|
101
|
+
collection: CollectionParameter | null = null
|
|
93
102
|
) {
|
|
94
103
|
const clauses = [
|
|
95
104
|
Q.where('content_id', contentId),
|
|
@@ -102,7 +111,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
102
111
|
|
|
103
112
|
async getSomeProgressByContentIds(
|
|
104
113
|
contentIds: number[],
|
|
105
|
-
collection:
|
|
114
|
+
collection: CollectionParameter | null = null
|
|
106
115
|
) {
|
|
107
116
|
const clauses = [
|
|
108
117
|
Q.where('content_id', Q.oneOf(contentIds)),
|
|
@@ -113,7 +122,23 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
113
122
|
return await this.queryAll(...clauses)
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
|
|
125
|
+
async getSomeProgressByContentIdsAndCollection(tuples: ContentIdCollectionTuple[]) {
|
|
126
|
+
const clauses = []
|
|
127
|
+
|
|
128
|
+
clauses.push(...tuples.map(tuple => Q.and(...tupleClauses(tuple))))
|
|
129
|
+
|
|
130
|
+
return await this.queryAll(Q.or(...clauses))
|
|
131
|
+
|
|
132
|
+
function tupleClauses(tuple: ContentIdCollectionTuple) {
|
|
133
|
+
return [
|
|
134
|
+
Q.where('content_id', tuple.contentId),
|
|
135
|
+
Q.where('collection_type', tuple.collection?.type ?? COLLECTION_TYPE.SELF),
|
|
136
|
+
Q.where('collection_id', tuple.collection?.id ?? COLLECTION_ID_SELF)
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number) {
|
|
117
142
|
const id = ProgressRepository.generateId(contentId, collection)
|
|
118
143
|
|
|
119
144
|
const result = this.upsertOne(id, (r) => {
|
|
@@ -156,7 +181,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
156
181
|
|
|
157
182
|
recordProgresses(
|
|
158
183
|
contentIds: number[],
|
|
159
|
-
collection:
|
|
184
|
+
collection: CollectionParameter | null,
|
|
160
185
|
progressPct: number
|
|
161
186
|
) {
|
|
162
187
|
return this.upsertSome(
|
|
@@ -178,7 +203,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
178
203
|
|
|
179
204
|
recordProgressesTentative(
|
|
180
205
|
contentProgresses: Record<string, number>, // Accept plain object
|
|
181
|
-
collection:
|
|
206
|
+
collection: CollectionParameter | null
|
|
182
207
|
) {
|
|
183
208
|
const data = Object.fromEntries(
|
|
184
209
|
Object.entries(contentProgresses).map(([contentId, progressPct]) => [
|
|
@@ -196,13 +221,13 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
196
221
|
return this.upsertSomeTentative(data)
|
|
197
222
|
}
|
|
198
223
|
|
|
199
|
-
eraseProgress(contentId: number, collection:
|
|
224
|
+
eraseProgress(contentId: number, collection: CollectionParameter | null) {
|
|
200
225
|
return this.deleteOne(ProgressRepository.generateId(contentId, collection))
|
|
201
226
|
}
|
|
202
227
|
|
|
203
228
|
private static generateId(
|
|
204
229
|
contentId: number,
|
|
205
|
-
collection:
|
|
230
|
+
collection: CollectionParameter | null
|
|
206
231
|
) {
|
|
207
232
|
return `${contentId}:${collection?.type || COLLECTION_TYPE.SELF}:${collection?.id || COLLECTION_ID_SELF}`
|
|
208
233
|
}
|