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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.96.2",
3
+ "version": "2.97.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.d.ts CHANGED
@@ -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 contentStatusReset(learningPathId, collection)
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
- async function getById(contentId, dataKey, defaultValue) {
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
  }
@@ -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, { Q } from './base'
2
- import ContentProgress, { COLLECTION_TYPE, COLLECTION_ID_SELF, STATE } from '../models/ContentProgress'
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: { type: COLLECTION_TYPE; id: number } | null = null) {
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
- { collection }: { collection?: { type: COLLECTION_TYPE; id: number } | null } = {}
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: { type: COLLECTION_TYPE; id: number } | null = null
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
- recordProgress(contentId: number, collection: { type: COLLECTION_TYPE; id: number } | null, progressPct: number, resumeTime?: number) {
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: { type: COLLECTION_TYPE; id: number } | null,
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: { type: COLLECTION_TYPE; id: number } | null
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: { type: COLLECTION_TYPE; id: number } | null) {
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: { type: COLLECTION_TYPE; id: number } | null
230
+ collection: CollectionParameter | null
206
231
  ) {
207
232
  return `${contentId}:${collection?.type || COLLECTION_TYPE.SELF}:${collection?.id || COLLECTION_ID_SELF}`
208
233
  }