musora-content-services 2.96.1 → 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/.claude/settings.local.json +9 -0
- package/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/index.d.ts +8 -12
- package/src/index.js +8 -12
- 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 -200
- package/src/services/sync/repositories/content-progress.ts +35 -10
- package/test/sanityQueryService.test.js +0 -116
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
|
|
12
|
+
### [2.96.2](https://github.com/railroadmedia/musora-content-services/compare/v2.96.1...v2.96.2) (2025-12-09)
|
|
13
|
+
|
|
5
14
|
### [2.96.1](https://github.com/railroadmedia/musora-content-services/compare/v2.96.0...v2.96.1) (2025-12-09)
|
|
6
15
|
|
|
7
16
|
|
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';
|
|
@@ -267,7 +271,6 @@ import {
|
|
|
267
271
|
fetchComingSoon,
|
|
268
272
|
fetchCommentModContentData,
|
|
269
273
|
fetchContentRows,
|
|
270
|
-
fetchFoundation,
|
|
271
274
|
fetchHierarchy,
|
|
272
275
|
fetchLearningPathHierarchy,
|
|
273
276
|
fetchLeaving,
|
|
@@ -275,15 +278,10 @@ import {
|
|
|
275
278
|
fetchLessonsFeaturingThisContent,
|
|
276
279
|
fetchLiveEvent,
|
|
277
280
|
fetchMetadata,
|
|
278
|
-
fetchMethod,
|
|
279
|
-
fetchMethodChildren,
|
|
280
|
-
fetchMethodChildrenIds,
|
|
281
|
-
fetchMethodPreviousNextLesson,
|
|
282
281
|
fetchMethodV2IntroVideo,
|
|
283
282
|
fetchMethodV2Structure,
|
|
284
283
|
fetchMethodV2StructureFromId,
|
|
285
284
|
fetchNewReleases,
|
|
286
|
-
fetchNextPreviousLesson,
|
|
287
285
|
fetchOtherSongVersions,
|
|
288
286
|
fetchOwnedContent,
|
|
289
287
|
fetchPackAll,
|
|
@@ -432,6 +430,7 @@ import {
|
|
|
432
430
|
declare module 'musora-content-services' {
|
|
433
431
|
export {
|
|
434
432
|
addContextToContent,
|
|
433
|
+
addContextToLearningPaths,
|
|
435
434
|
addItemToPlaylist,
|
|
436
435
|
applyCloudflareWrapper,
|
|
437
436
|
applySanityTransformations,
|
|
@@ -504,7 +503,6 @@ declare module 'musora-content-services' {
|
|
|
504
503
|
fetchEnrollmentPageMetadata,
|
|
505
504
|
fetchFollowedThreads,
|
|
506
505
|
fetchForumCategories,
|
|
507
|
-
fetchFoundation,
|
|
508
506
|
fetchGenreBySlug,
|
|
509
507
|
fetchGenreLessons,
|
|
510
508
|
fetchGenres,
|
|
@@ -527,16 +525,11 @@ declare module 'musora-content-services' {
|
|
|
527
525
|
fetchLiveEventPollingState,
|
|
528
526
|
fetchMemberships,
|
|
529
527
|
fetchMetadata,
|
|
530
|
-
fetchMethod,
|
|
531
|
-
fetchMethodChildren,
|
|
532
|
-
fetchMethodChildrenIds,
|
|
533
|
-
fetchMethodPreviousNextLesson,
|
|
534
528
|
fetchMethodV2IntroVideo,
|
|
535
529
|
fetchMethodV2Structure,
|
|
536
530
|
fetchMethodV2StructureFromId,
|
|
537
531
|
fetchNewReleases,
|
|
538
532
|
fetchNextContentDataForParent,
|
|
539
|
-
fetchNextPreviousLesson,
|
|
540
533
|
fetchNotificationSettings,
|
|
541
534
|
fetchNotifications,
|
|
542
535
|
fetchOtherSongVersions,
|
|
@@ -602,6 +595,7 @@ declare module 'musora-content-services' {
|
|
|
602
595
|
getMethodCard,
|
|
603
596
|
getMonday,
|
|
604
597
|
getNavigateTo,
|
|
598
|
+
getNavigateToForMethod,
|
|
605
599
|
getNavigateToForPlaylists,
|
|
606
600
|
getNewAndUpcoming,
|
|
607
601
|
getOnboardingRecommendedContent,
|
|
@@ -609,6 +603,7 @@ declare module 'musora-content-services' {
|
|
|
609
603
|
getPracticeNotes,
|
|
610
604
|
getPracticeSessions,
|
|
611
605
|
getProgressDataByIds,
|
|
606
|
+
getProgressDataByIdsAndCollections,
|
|
612
607
|
getProgressRows,
|
|
613
608
|
getProgressState,
|
|
614
609
|
getProgressStateByIds,
|
|
@@ -617,6 +612,7 @@ declare module 'musora-content-services' {
|
|
|
617
612
|
getRecommendedForYou,
|
|
618
613
|
getReportIssueOptions,
|
|
619
614
|
getResumeTimeSecondsByIds,
|
|
615
|
+
getResumeTimeSecondsByIdsAndCollections,
|
|
620
616
|
getSanityDate,
|
|
621
617
|
getScheduleContentRows,
|
|
622
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';
|
|
@@ -271,7 +275,6 @@ import {
|
|
|
271
275
|
fetchComingSoon,
|
|
272
276
|
fetchCommentModContentData,
|
|
273
277
|
fetchContentRows,
|
|
274
|
-
fetchFoundation,
|
|
275
278
|
fetchHierarchy,
|
|
276
279
|
fetchLearningPathHierarchy,
|
|
277
280
|
fetchLeaving,
|
|
@@ -279,15 +282,10 @@ import {
|
|
|
279
282
|
fetchLessonsFeaturingThisContent,
|
|
280
283
|
fetchLiveEvent,
|
|
281
284
|
fetchMetadata,
|
|
282
|
-
fetchMethod,
|
|
283
|
-
fetchMethodChildren,
|
|
284
|
-
fetchMethodChildrenIds,
|
|
285
|
-
fetchMethodPreviousNextLesson,
|
|
286
285
|
fetchMethodV2IntroVideo,
|
|
287
286
|
fetchMethodV2Structure,
|
|
288
287
|
fetchMethodV2StructureFromId,
|
|
289
288
|
fetchNewReleases,
|
|
290
|
-
fetchNextPreviousLesson,
|
|
291
289
|
fetchOtherSongVersions,
|
|
292
290
|
fetchOwnedContent,
|
|
293
291
|
fetchPackAll,
|
|
@@ -431,6 +429,7 @@ import {
|
|
|
431
429
|
|
|
432
430
|
export {
|
|
433
431
|
addContextToContent,
|
|
432
|
+
addContextToLearningPaths,
|
|
434
433
|
addItemToPlaylist,
|
|
435
434
|
applyCloudflareWrapper,
|
|
436
435
|
applySanityTransformations,
|
|
@@ -503,7 +502,6 @@ export {
|
|
|
503
502
|
fetchEnrollmentPageMetadata,
|
|
504
503
|
fetchFollowedThreads,
|
|
505
504
|
fetchForumCategories,
|
|
506
|
-
fetchFoundation,
|
|
507
505
|
fetchGenreBySlug,
|
|
508
506
|
fetchGenreLessons,
|
|
509
507
|
fetchGenres,
|
|
@@ -526,16 +524,11 @@ export {
|
|
|
526
524
|
fetchLiveEventPollingState,
|
|
527
525
|
fetchMemberships,
|
|
528
526
|
fetchMetadata,
|
|
529
|
-
fetchMethod,
|
|
530
|
-
fetchMethodChildren,
|
|
531
|
-
fetchMethodChildrenIds,
|
|
532
|
-
fetchMethodPreviousNextLesson,
|
|
533
527
|
fetchMethodV2IntroVideo,
|
|
534
528
|
fetchMethodV2Structure,
|
|
535
529
|
fetchMethodV2StructureFromId,
|
|
536
530
|
fetchNewReleases,
|
|
537
531
|
fetchNextContentDataForParent,
|
|
538
|
-
fetchNextPreviousLesson,
|
|
539
532
|
fetchNotificationSettings,
|
|
540
533
|
fetchNotifications,
|
|
541
534
|
fetchOtherSongVersions,
|
|
@@ -601,6 +594,7 @@ export {
|
|
|
601
594
|
getMethodCard,
|
|
602
595
|
getMonday,
|
|
603
596
|
getNavigateTo,
|
|
597
|
+
getNavigateToForMethod,
|
|
604
598
|
getNavigateToForPlaylists,
|
|
605
599
|
getNewAndUpcoming,
|
|
606
600
|
getOnboardingRecommendedContent,
|
|
@@ -608,6 +602,7 @@ export {
|
|
|
608
602
|
getPracticeNotes,
|
|
609
603
|
getPracticeSessions,
|
|
610
604
|
getProgressDataByIds,
|
|
605
|
+
getProgressDataByIdsAndCollections,
|
|
611
606
|
getProgressRows,
|
|
612
607
|
getProgressState,
|
|
613
608
|
getProgressStateByIds,
|
|
@@ -616,6 +611,7 @@ export {
|
|
|
616
611
|
getRecommendedForYou,
|
|
617
612
|
getReportIssueOptions,
|
|
618
613
|
getResumeTimeSecondsByIds,
|
|
614
|
+
getResumeTimeSecondsByIdsAndCollections,
|
|
619
615
|
getSanityDate,
|
|
620
616
|
getScheduleContentRows,
|
|
621
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
|
@@ -866,206 +866,6 @@ export async function fetchAllFilterOptions(
|
|
|
866
866
|
return includeTabs ? { ...results, tabs, catalogName } : results
|
|
867
867
|
}
|
|
868
868
|
|
|
869
|
-
//Daniel Nov 14 2025 note - keeping this for when we migrate foundations to packs, so we know what fields to use.
|
|
870
|
-
/**
|
|
871
|
-
* Fetch the Foundations 2019.
|
|
872
|
-
* @param {string} slug - The slug of the method.
|
|
873
|
-
* @returns {Promise<Object|null>} - The fetched foundation data or null if not found.
|
|
874
|
-
*/
|
|
875
|
-
export async function fetchFoundation(slug) {
|
|
876
|
-
const filterParams = {}
|
|
877
|
-
const query = await buildQuery(
|
|
878
|
-
`_type == 'foundation' && slug.current == "${slug}"`,
|
|
879
|
-
filterParams,
|
|
880
|
-
getFieldsForContentType('foundation'),
|
|
881
|
-
{
|
|
882
|
-
sortOrder: 'published_on asc',
|
|
883
|
-
isSingle: true,
|
|
884
|
-
}
|
|
885
|
-
)
|
|
886
|
-
return fetchSanity(query, false)
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Fetch the Method (learning-paths) for a specific brand.
|
|
891
|
-
* @param {string} brand - The brand for which to fetch methods.
|
|
892
|
-
* @param {string} slug - The slug of the method.
|
|
893
|
-
* @returns {Promise<Object|null>} - The fetched methods data or null if not found.
|
|
894
|
-
*/
|
|
895
|
-
//todo BEH-1446 depreciated. remove all old method functions
|
|
896
|
-
export async function fetchMethod(brand, slug) {
|
|
897
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
898
|
-
|
|
899
|
-
const query = `*[_type == 'learning-path' && brand == "${brand}" && slug.current == "${slug}"] {
|
|
900
|
-
"description": ${descriptionField},
|
|
901
|
-
"instructors":instructor[]->name,
|
|
902
|
-
published_on,
|
|
903
|
-
"id": railcontent_id,
|
|
904
|
-
railcontent_id,
|
|
905
|
-
"slug": slug.current,
|
|
906
|
-
status,
|
|
907
|
-
title,
|
|
908
|
-
video,
|
|
909
|
-
length_in_seconds,
|
|
910
|
-
parent_content_data,
|
|
911
|
-
"breadcrumbs_data": parent_content_data[] {
|
|
912
|
-
"id": id,
|
|
913
|
-
"title": *[railcontent_id == ^.id][0].title,
|
|
914
|
-
"url": *[railcontent_id == ^.id][0].web_url_path
|
|
915
|
-
} | order(length(url)),
|
|
916
|
-
"type": _type,
|
|
917
|
-
"permission_id": permission_v2,
|
|
918
|
-
"levels": child[${childrenFilter}]->
|
|
919
|
-
{
|
|
920
|
-
"id": railcontent_id,
|
|
921
|
-
published_on,
|
|
922
|
-
child_count,
|
|
923
|
-
difficulty,
|
|
924
|
-
difficulty_string,
|
|
925
|
-
"thumbnail": thumbnail.asset->url,
|
|
926
|
-
"instructor": instructor[]->{name},
|
|
927
|
-
title,
|
|
928
|
-
"type": _type,
|
|
929
|
-
"description": ${descriptionField},
|
|
930
|
-
"url": web_url_path,
|
|
931
|
-
web_url_path,
|
|
932
|
-
xp,
|
|
933
|
-
total_xp
|
|
934
|
-
}
|
|
935
|
-
} | order(published_on asc)`
|
|
936
|
-
return fetchSanity(query, false)
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* Fetch the child courses for a specific method by Railcontent ID.
|
|
941
|
-
* @param {string} railcontentId - The Railcontent ID of the current lesson.
|
|
942
|
-
* @returns {Promise<Object|null>} - The fetched next lesson data or null if not found.
|
|
943
|
-
*/
|
|
944
|
-
export async function fetchMethodChildren(railcontentId) {
|
|
945
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
946
|
-
|
|
947
|
-
const query = `*[railcontent_id == ${railcontentId}]{
|
|
948
|
-
"child_count":coalesce(count(child[${childrenFilter}]->), 0),
|
|
949
|
-
"id": railcontent_id,
|
|
950
|
-
"description": ${descriptionField},
|
|
951
|
-
"thumbnail": thumbnail.asset->url,
|
|
952
|
-
title,
|
|
953
|
-
xp,
|
|
954
|
-
total_xp,
|
|
955
|
-
parent_content_data,
|
|
956
|
-
"resources": ${resourcesField},
|
|
957
|
-
"breadcrumbs_data": parent_content_data[] {
|
|
958
|
-
"id": id,
|
|
959
|
-
"title": *[railcontent_id == ^.id][0].title,
|
|
960
|
-
"url": *[railcontent_id == ^.id][0].web_url_path
|
|
961
|
-
} | order(length(url)),
|
|
962
|
-
'children': child[(${childrenFilter})]->{
|
|
963
|
-
${getFieldsForContentType('method')}
|
|
964
|
-
},
|
|
965
|
-
}[0..1]`
|
|
966
|
-
return fetchSanity(query, true)
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* Fetch the next lesson for a specific method by Railcontent ID.
|
|
971
|
-
* @param {string} railcontentId - The Railcontent ID of the current lesson.
|
|
972
|
-
* @param {string} methodId - The RailcontentID of the method
|
|
973
|
-
* @returns {Promise<Object|null>} - object with `nextLesson` and `previousLesson` attributes
|
|
974
|
-
* @example
|
|
975
|
-
* fetchMethodPreviousNextLesson(241284, 241247)
|
|
976
|
-
* .then(data => { console.log('nextLesson', data.nextLesson); console.log('prevlesson', data.prevLesson);})
|
|
977
|
-
* .catch(error => console.error(error));
|
|
978
|
-
*/
|
|
979
|
-
export async function fetchMethodPreviousNextLesson(railcontentId, methodId) {
|
|
980
|
-
const sortedChildren = await fetchMethodChildrenIds(methodId)
|
|
981
|
-
const index = sortedChildren.indexOf(Number(railcontentId))
|
|
982
|
-
let nextId = sortedChildren[index + 1]
|
|
983
|
-
let previousId = sortedChildren[index - 1]
|
|
984
|
-
let ids = []
|
|
985
|
-
if (nextId) ids.push(nextId)
|
|
986
|
-
if (previousId) ids.push(previousId)
|
|
987
|
-
let nextPrev = await fetchByRailContentIds(ids)
|
|
988
|
-
const nextLesson = nextPrev.find((elem) => {
|
|
989
|
-
return elem['id'] === nextId
|
|
990
|
-
})
|
|
991
|
-
const prevLesson = nextPrev.find((elem) => {
|
|
992
|
-
return elem['id'] === previousId
|
|
993
|
-
})
|
|
994
|
-
return { nextLesson, prevLesson }
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
/**
|
|
998
|
-
* Fetch all children of a specific method by Railcontent ID.
|
|
999
|
-
* @param {string} railcontentId - The Railcontent ID of the method.
|
|
1000
|
-
* @returns {Promise<Array<Object>|null>} - The fetched children data or null if not found.
|
|
1001
|
-
*/
|
|
1002
|
-
export async function fetchMethodChildrenIds(railcontentId) {
|
|
1003
|
-
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
1004
|
-
|
|
1005
|
-
const query = `*[ railcontent_id == ${railcontentId}]{
|
|
1006
|
-
'children': child[${childrenFilter}]-> {
|
|
1007
|
-
'id': railcontent_id,
|
|
1008
|
-
'type' : _type,
|
|
1009
|
-
'children': child[${childrenFilter}]-> {
|
|
1010
|
-
'id': railcontent_id,
|
|
1011
|
-
'type' : _type,
|
|
1012
|
-
'children': child[${childrenFilter}]-> {
|
|
1013
|
-
'id': railcontent_id,
|
|
1014
|
-
'type' : _type,
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}`
|
|
1019
|
-
let allChildren = await fetchSanity(query, false)
|
|
1020
|
-
return getChildrenToDepth(allChildren, 4)
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function getChildrenToDepth(parent, depth = 1) {
|
|
1024
|
-
let allChildrenIds = []
|
|
1025
|
-
if (parent && parent['children'] && depth > 0) {
|
|
1026
|
-
parent['children'].forEach((child) => {
|
|
1027
|
-
if (!child['children']) {
|
|
1028
|
-
allChildrenIds.push(child['id'])
|
|
1029
|
-
}
|
|
1030
|
-
allChildrenIds = allChildrenIds.concat(getChildrenToDepth(child, depth - 1))
|
|
1031
|
-
})
|
|
1032
|
-
}
|
|
1033
|
-
return allChildrenIds
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Fetch the next and previous lessons for a specific lesson by Railcontent ID.
|
|
1038
|
-
* @param {string} railcontentId - The Railcontent ID of the current lesson.
|
|
1039
|
-
* @returns {Promise<Object|null>} - The fetched next and previous lesson data or null if found.
|
|
1040
|
-
*/
|
|
1041
|
-
export async function fetchNextPreviousLesson(railcontentId) {
|
|
1042
|
-
const document = await fetchLessonContent(railcontentId)
|
|
1043
|
-
if (document.parent_content_data && document.parent_content_data.length > 0) {
|
|
1044
|
-
const lastElement = document.parent_content_data[document.parent_content_data.length - 1]
|
|
1045
|
-
const results = await fetchMethodPreviousNextLesson(railcontentId, lastElement.id)
|
|
1046
|
-
return results
|
|
1047
|
-
}
|
|
1048
|
-
const processedData = processMetadata(document.brand, document.type, true)
|
|
1049
|
-
let sortBy = processedData?.sortBy ?? 'published_on'
|
|
1050
|
-
const isDesc = sortBy.startsWith('-')
|
|
1051
|
-
sortBy = isDesc ? sortBy.substring(1) : sortBy
|
|
1052
|
-
let sortValue = document[sortBy]
|
|
1053
|
-
if (sortValue == null) {
|
|
1054
|
-
sortBy = 'railcontent_id'
|
|
1055
|
-
sortValue = document['railcontent_id']
|
|
1056
|
-
}
|
|
1057
|
-
const isNumeric = !isNaN(sortValue)
|
|
1058
|
-
let prevComparison = isNumeric ? `${sortBy} <= ${sortValue}` : `${sortBy} <= "${sortValue}"`
|
|
1059
|
-
let nextComparison = isNumeric ? `${sortBy} >= ${sortValue}` : `${sortBy} >= "${sortValue}"`
|
|
1060
|
-
const fields = getFieldsForContentType(document.type)
|
|
1061
|
-
const query = `{
|
|
1062
|
-
"prevLesson": *[brand == "${document.brand}" && status == "${document.status}" && _type == "${document.type}" && ${prevComparison} && railcontent_id != ${railcontentId}] | order(${sortBy} desc){${fields}}[0...1][0],
|
|
1063
|
-
"nextLesson": *[brand == "${document.brand}" && status == "${document.status}" && _type == "${document.type}" && ${nextComparison} && railcontent_id != ${railcontentId}] | order(${sortBy} asc){${fields}}[0...1][0]
|
|
1064
|
-
}`
|
|
1065
|
-
|
|
1066
|
-
return await fetchSanity(query, true)
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
869
|
/**
|
|
1070
870
|
* Fetch the next piece of content under a parent by Railcontent ID
|
|
1071
871
|
* @param {int} railcontentId - The Railcontent ID of the parent content
|
|
@@ -2200,8 +2000,11 @@ export async function fetchMethodV2Structure(brand) {
|
|
|
2200
2000
|
const _type = 'method-v2'
|
|
2201
2001
|
const query = `*[_type == '${_type}' && brand == '${brand}'][0...1]{
|
|
2202
2002
|
'sanity_id': _id,
|
|
2003
|
+
brand,
|
|
2004
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2203
2005
|
'learning_paths': child[]->{
|
|
2204
2006
|
'id': railcontent_id,
|
|
2007
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2205
2008
|
'children': child[]->railcontent_id
|
|
2206
2009
|
}
|
|
2207
2010
|
}`
|
|
@@ -2217,8 +2020,11 @@ export async function fetchMethodV2StructureFromId(contentId) {
|
|
|
2217
2020
|
const _type = "method-v2";
|
|
2218
2021
|
const query = `*[_type == '${_type}' && brand == *[railcontent_id == ${contentId}][0].brand][0...1]{
|
|
2219
2022
|
'sanity_id': _id,
|
|
2023
|
+
brand,
|
|
2024
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2220
2025
|
'learning_paths': child[]->{
|
|
2221
2026
|
'id': railcontent_id,
|
|
2027
|
+
'intro_video_id': intro_video->railcontent_id,
|
|
2222
2028
|
'children': child[]->railcontent_id
|
|
2223
2029
|
}
|
|
2224
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
|
}
|
|
@@ -10,7 +10,6 @@ import { fetchLessonsFeaturingThisContent } from '../src/services/sanity.js'
|
|
|
10
10
|
|
|
11
11
|
const {
|
|
12
12
|
fetchSongById,
|
|
13
|
-
fetchArtists,
|
|
14
13
|
fetchReturning,
|
|
15
14
|
fetchLeaving,
|
|
16
15
|
fetchComingSoon,
|
|
@@ -21,25 +20,20 @@ const {
|
|
|
21
20
|
fetchByRailContentIds,
|
|
22
21
|
fetchAll,
|
|
23
22
|
fetchAllFilterOptions,
|
|
24
|
-
fetchFoundation,
|
|
25
|
-
fetchMethod,
|
|
26
23
|
fetchRelatedLessons,
|
|
27
24
|
fetchAllPacks,
|
|
28
25
|
fetchPackAll,
|
|
29
26
|
fetchLessonContent,
|
|
30
27
|
fetchLiveEvent,
|
|
31
|
-
fetchCoachLessons,
|
|
32
28
|
fetchByReference,
|
|
33
29
|
fetchScheduledReleases,
|
|
34
30
|
getSortOrder,
|
|
35
31
|
fetchShowsData,
|
|
36
32
|
fetchMetadata,
|
|
37
|
-
fetchNextPreviousLesson,
|
|
38
33
|
fetchHierarchy,
|
|
39
34
|
fetchTopLevelParentId,
|
|
40
35
|
fetchOtherSongVersions,
|
|
41
36
|
fetchCommentModContentData,
|
|
42
|
-
fetchMethodPreviousNextLesson,
|
|
43
37
|
fetchSanity,
|
|
44
38
|
} = require('../src/services/sanity.js')
|
|
45
39
|
|
|
@@ -77,13 +71,6 @@ describe('Sanity Queries', function() {
|
|
|
77
71
|
expect(response).toBeDefined()
|
|
78
72
|
})
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
test('fetchArtists', async () => {
|
|
82
|
-
const response = await fetchArtists('drumeo')
|
|
83
|
-
const artistNames = response.map((x) => x.name)
|
|
84
|
-
expect(artistNames).toContain('Audioslave')
|
|
85
|
-
}, 10000)
|
|
86
|
-
|
|
87
74
|
test('fetchSongArtistCount', async () => {
|
|
88
75
|
const response = await fetchSongArtistCount('drumeo')
|
|
89
76
|
log(response)
|
|
@@ -301,13 +288,6 @@ describe('Sanity Queries', function() {
|
|
|
301
288
|
expect(sort).toBe('published_on asc')
|
|
302
289
|
})
|
|
303
290
|
|
|
304
|
-
test('fetchMethod', async () => {
|
|
305
|
-
const response = await fetchMethod('drumeo', 'drumeo-method')
|
|
306
|
-
log(response)
|
|
307
|
-
expect(response).toBeDefined()
|
|
308
|
-
expect(response.levels.length).toBeGreaterThan(0)
|
|
309
|
-
})
|
|
310
|
-
|
|
311
291
|
test('fetchAll-WithProgress', async () => {
|
|
312
292
|
const ids = [410213, 410215]
|
|
313
293
|
let response = await fetchAll('drumeo', 'song', {
|
|
@@ -332,13 +312,6 @@ describe('Sanity Queries', function() {
|
|
|
332
312
|
expect(response.meta.totalResults).toBe(0)
|
|
333
313
|
})
|
|
334
314
|
|
|
335
|
-
test('fetchFoundation', async () => {
|
|
336
|
-
const response = await fetchFoundation('foundations-2019')
|
|
337
|
-
log(response)
|
|
338
|
-
expect(response.units.length).toBeGreaterThan(0)
|
|
339
|
-
expect(response.type).toBe('foundation')
|
|
340
|
-
})
|
|
341
|
-
|
|
342
315
|
test('fetchPackAll', async () => {
|
|
343
316
|
const response = await fetchPackAll(212899) //https://web-staging-one.musora.com/admin/studio/publishing/structure/pack;pack_212899%2Cinspect%3Don
|
|
344
317
|
log(response)
|
|
@@ -357,35 +330,6 @@ describe('Sanity Queries', function() {
|
|
|
357
330
|
expect(response[0].id).toBe(212899)
|
|
358
331
|
})
|
|
359
332
|
|
|
360
|
-
test('fetchCoachLessons', async () => {
|
|
361
|
-
const response = await fetchCoachLessons('drumeo', 411493, {})
|
|
362
|
-
expect(response.entity.length).toBeGreaterThan(0)
|
|
363
|
-
})
|
|
364
|
-
test('fetchCoachLessons-WithTypeFilters', async () => {
|
|
365
|
-
const response = await fetchAllFilterOptions('drumeo', ['type,course', 'type,live'], '', '', 'coach-lessons', '', [], 31880)
|
|
366
|
-
log(response)
|
|
367
|
-
expect(response.meta.filterOptions.difficulty).toBeDefined()
|
|
368
|
-
expect(response.meta.filterOptions.type).toBeDefined()
|
|
369
|
-
expect(response.meta.filterOptions.lifestyle).toBeDefined()
|
|
370
|
-
expect(response.meta.filterOptions.genre).toBeDefined()
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
test('fetchCoachLessons-WithTypeFilters-InvalidContentType', async () => {
|
|
374
|
-
const brand = 'drumeo'
|
|
375
|
-
const coachId = 31880
|
|
376
|
-
const invalidContentType = 'course' // Not 'coach-lessons'
|
|
377
|
-
|
|
378
|
-
await expect(fetchAllFilterOptions(brand, ['type,course', 'type,live'], '', '', invalidContentType, '', [], coachId)).rejects.toThrow(`Invalid contentType: 'course' for coachId. It must be 'coach-lessons'.`)
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
test('fetchCoachLessons-IncludedFields', async () => {
|
|
382
|
-
const response = await fetchCoachLessons('drumeo', 31880, {
|
|
383
|
-
includedFields: ['genre,Pop/Rock', 'difficulty,Beginner'],
|
|
384
|
-
})
|
|
385
|
-
log(response)
|
|
386
|
-
expect(response.entity.length).toBeGreaterThan(0)
|
|
387
|
-
})
|
|
388
|
-
|
|
389
333
|
test('fetchAll-IncludedFields', async () => {
|
|
390
334
|
let response = await fetchAll('drumeo', 'instructor', { includedFields: ['is_active'] })
|
|
391
335
|
console.log(response)
|
|
@@ -488,66 +432,6 @@ describe('Sanity Queries', function() {
|
|
|
488
432
|
expect(response).toBeDefined()
|
|
489
433
|
})
|
|
490
434
|
|
|
491
|
-
test('fetchNextPreviousLesson-Show-With-Episodes', async () => {
|
|
492
|
-
const id = 227136
|
|
493
|
-
const document = await fetchByRailContentId(id, 'behind-the-scenes')
|
|
494
|
-
const response = await fetchNextPreviousLesson(id)
|
|
495
|
-
log(response)
|
|
496
|
-
expect(response.prevLesson).toBeDefined()
|
|
497
|
-
expect(response.prevLesson.sort).toBeLessThanOrEqual(document.sort)
|
|
498
|
-
expect(response.nextLesson).toBeDefined()
|
|
499
|
-
expect(response.nextLesson.sort).toBeGreaterThanOrEqual(document.sort)
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
test('fetchMethodNextPreviousLesson-Last', async () => {
|
|
503
|
-
const id = 260171
|
|
504
|
-
const methodId = 259060
|
|
505
|
-
const response = await fetchMethodPreviousNextLesson(id, methodId)
|
|
506
|
-
log(response)
|
|
507
|
-
expect(response.prevLesson).toBeDefined()
|
|
508
|
-
expect(response.prevLesson.id).toBe(260170)
|
|
509
|
-
expect(response.prevLesson.type).toBe('course-part')
|
|
510
|
-
expect(response.nextLesson).not.toBeDefined()
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
test('fetchNextPreviousLesson-Method-Lesson', async () => {
|
|
514
|
-
const id = 241265
|
|
515
|
-
const response = await fetchNextPreviousLesson(id)
|
|
516
|
-
log(response)
|
|
517
|
-
expect(response.prevLesson).toBeDefined()
|
|
518
|
-
expect(response.prevLesson.id).toBe(241264)
|
|
519
|
-
expect(response.prevLesson.type).toBe('learning-path-lesson')
|
|
520
|
-
expect(response.nextLesson).toBeDefined()
|
|
521
|
-
expect(response.nextLesson.id).toBe(241267)
|
|
522
|
-
expect(response.nextLesson.type).toBe('learning-path-lesson')
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
test('fetchNextPreviousLesson-Quick-Tips', async () => {
|
|
526
|
-
const id = 412277
|
|
527
|
-
const response = await fetchNextPreviousLesson(id)
|
|
528
|
-
const document = await fetchByRailContentId(id, 'quick-tips')
|
|
529
|
-
const documentPublishedOn = new Date(document.published_on)
|
|
530
|
-
const prevDocumentPublishedOn = new Date(response.prevLesson.published_on)
|
|
531
|
-
const nextDocumentPublishedOn = new Date(response.nextLesson.published_on)
|
|
532
|
-
expect(response.prevLesson).toBeDefined()
|
|
533
|
-
expect(prevDocumentPublishedOn.getTime()).toBeLessThan(documentPublishedOn.getTime())
|
|
534
|
-
expect(response.nextLesson).toBeDefined()
|
|
535
|
-
expect(documentPublishedOn.getTime()).toBeLessThan(nextDocumentPublishedOn.getTime())
|
|
536
|
-
})
|
|
537
|
-
|
|
538
|
-
test('fetchNextPreviousLesson-Song', async () => {
|
|
539
|
-
const id = 414041
|
|
540
|
-
const response = await fetchNextPreviousLesson(id)
|
|
541
|
-
const document = await fetchByRailContentId(id, 'song')
|
|
542
|
-
const documentPublishedOn = new Date(document.published_on)
|
|
543
|
-
const prevDocumentPublishedOn = new Date(response.prevLesson.published_on)
|
|
544
|
-
const nextDocumentPublishedOn = new Date(response.nextLesson.published_on)
|
|
545
|
-
expect(response.prevLesson).toBeDefined()
|
|
546
|
-
expect(prevDocumentPublishedOn.getTime()).toBeLessThanOrEqual(documentPublishedOn.getTime())
|
|
547
|
-
expect(response.nextLesson).toBeDefined()
|
|
548
|
-
expect(documentPublishedOn.getTime()).toBeLessThanOrEqual(nextDocumentPublishedOn.getTime())
|
|
549
|
-
})
|
|
550
|
-
|
|
551
435
|
test('fetchTopLevelParentId', async () => {
|
|
552
436
|
let contentId = await fetchTopLevelParentId(241250)
|
|
553
437
|
expect(contentId).toBe(241247)
|