musora-content-services 2.3.25 → 2.4.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/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +9 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/docs/Content-Organization.html +0 -0
- package/docs/ContentOrganization.html +1 -1
- package/docs/Gamification.html +1 -1
- package/docs/UserManagement.html +0 -0
- package/docs/UserManagementSystem.html +1 -1
- package/docs/api_types.js.html +1 -1
- package/docs/config.js.html +1 -1
- package/docs/content-org_content-org.js.html +1 -1
- package/docs/content-org_playlists-types.js.html +1 -1
- package/docs/content-org_playlists.js.html +1 -1
- package/docs/content.js.html +25 -10
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/gamification_awards.js.html +1 -1
- package/docs/gamification_gamification.js.html +1 -1
- package/docs/gamification_types.js.html +1 -1
- package/docs/global.html +1 -1
- package/docs/global.html#User +0 -0
- package/docs/index.html +1 -1
- package/docs/module-Awards.html +1 -1
- package/docs/module-Config.html +1 -1
- package/docs/module-Content-Services-V2.html +9 -8
- package/docs/module-Content-Services.html +763 -0
- package/docs/module-Permissions.html +1 -1
- package/docs/module-Playlists.html +1 -1
- package/docs/module-Railcontent-Services.html +1 -1
- package/docs/module-Sanity-Services.html +32 -32
- package/docs/module-Session-Management.html +0 -0
- package/docs/module-Sessions.html +1 -1
- package/docs/module-User-Activity.html +7 -7
- package/docs/module-User-Management.html +0 -0
- package/docs/module-User-Permissions.html +0 -0
- package/docs/module-UserManagement.html +1 -1
- package/docs/railcontent.js.html +1 -1
- package/docs/sanity.js.html +5 -3
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/docs/types.js.html +0 -0
- package/docs/userActivity.js.html +2 -4
- package/docs/user_management.js.html +1 -1
- package/docs/user_permissions.js.html +1 -1
- package/docs/user_sessions.js.html +1 -1
- package/docs/user_types.js.html +1 -1
- package/docs/user_user-management-system.js.html +1 -1
- package/docs/user_user-management.js.html +0 -0
- package/jest.config.js +0 -0
- package/jsdoc.json +0 -0
- package/link_mcs.sh +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +29 -5
- package/src/contentTypeConfig.js +3 -2
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/infrastructure/http/HttpClient.ts +120 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +45 -0
- package/src/infrastructure/http/index.ts +13 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +3 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +7 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +6 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +5 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +5 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +24 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/services/config.js +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +1 -1
- package/src/services/content.js +24 -9
- package/src/services/contentAggregator.js +18 -10
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/railcontent.js +28 -0
- package/src/services/recommendations.js +21 -25
- package/src/services/user/permissions.js +0 -0
- package/src/services/userActivity.js +23 -0
- package/test/HttpClient.test.js +257 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/initializeTests.js +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +0 -0
package/src/contentMetaData.js
CHANGED
|
@@ -310,7 +310,7 @@ const contentMetadata = {
|
|
|
310
310
|
sortBy: 'sort',
|
|
311
311
|
},
|
|
312
312
|
'play-along': {
|
|
313
|
-
name: 'Play
|
|
313
|
+
name: 'Play-Alongs',
|
|
314
314
|
icon: 'icon-play-alongs',
|
|
315
315
|
description:
|
|
316
316
|
'Add your drumming to high-quality drumless play-along tracks - with handy playback tools to help you create the perfect performance.',
|
|
@@ -354,7 +354,7 @@ const contentMetadata = {
|
|
|
354
354
|
'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
|
|
355
355
|
'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
|
|
356
356
|
],
|
|
357
|
-
type: ['Tutorials', 'Transcriptions', 'Play
|
|
357
|
+
type: ['Tutorials', 'Transcriptions', 'Play-Alongs', 'Jam Tracks'],
|
|
358
358
|
progress: PROGRESS_NAMES,
|
|
359
359
|
},
|
|
360
360
|
sortingOptions: {
|
|
@@ -395,7 +395,7 @@ const contentMetadata = {
|
|
|
395
395
|
'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
|
|
396
396
|
'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
|
|
397
397
|
],
|
|
398
|
-
type: ['Tutorials', 'Sheet Music', 'Play
|
|
398
|
+
type: ['Tutorials', 'Sheet Music', 'Play-Alongs', 'Jam Tracks'],
|
|
399
399
|
progress: PROGRESS_NAMES,
|
|
400
400
|
},
|
|
401
401
|
sortingOptions: {
|
|
@@ -411,6 +411,14 @@ const contentMetadata = {
|
|
|
411
411
|
Tabs.ExploreAll
|
|
412
412
|
],
|
|
413
413
|
},
|
|
414
|
+
'recent': {
|
|
415
|
+
name: 'Recent Lessons',
|
|
416
|
+
tabs: [
|
|
417
|
+
Tabs.RecentAll,
|
|
418
|
+
Tabs.RecentIncomplete,
|
|
419
|
+
Tabs.RecentCompleted
|
|
420
|
+
],
|
|
421
|
+
},
|
|
414
422
|
},
|
|
415
423
|
guitareo: {
|
|
416
424
|
instructor: {
|
|
@@ -438,7 +446,7 @@ const contentMetadata = {
|
|
|
438
446
|
'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
|
|
439
447
|
'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
|
|
440
448
|
],
|
|
441
|
-
type: ['Tutorials', 'Tabs', 'Play
|
|
449
|
+
type: ['Tutorials', 'Tabs', 'Play-Alongs', 'Jam Tracks'],
|
|
442
450
|
progress: PROGRESS_NAMES,
|
|
443
451
|
},
|
|
444
452
|
sortingOptions: {
|
|
@@ -454,6 +462,14 @@ const contentMetadata = {
|
|
|
454
462
|
Tabs.ExploreAll
|
|
455
463
|
],
|
|
456
464
|
},
|
|
465
|
+
'recent': {
|
|
466
|
+
name: 'Recent Lessons',
|
|
467
|
+
tabs: [
|
|
468
|
+
Tabs.RecentAll,
|
|
469
|
+
Tabs.RecentIncomplete,
|
|
470
|
+
Tabs.RecentCompleted
|
|
471
|
+
],
|
|
472
|
+
},
|
|
457
473
|
},
|
|
458
474
|
singeo: {
|
|
459
475
|
'student-review': {
|
|
@@ -470,7 +486,7 @@ const contentMetadata = {
|
|
|
470
486
|
'Blues', 'Christian', 'Classical', 'Country', 'Disco', 'Electronic', 'Folk', 'Funk', 'Hip-Hop/Rap', 'Holiday', 'Jazz', 'Soundtrack',
|
|
471
487
|
'World', 'Metal', 'Pop', 'R&B/Soul', 'Rock'
|
|
472
488
|
],
|
|
473
|
-
type: ['Tutorials', 'Sheet Music', 'Play
|
|
489
|
+
type: ['Tutorials', 'Sheet Music', 'Play-Alongs', 'Jam Tracks'],
|
|
474
490
|
progress: PROGRESS_NAMES,
|
|
475
491
|
},
|
|
476
492
|
sortingOptions: {
|
|
@@ -486,6 +502,14 @@ const contentMetadata = {
|
|
|
486
502
|
Tabs.ExploreAll
|
|
487
503
|
],
|
|
488
504
|
},
|
|
505
|
+
'recent': {
|
|
506
|
+
name: 'Recent Lessons',
|
|
507
|
+
tabs: [
|
|
508
|
+
Tabs.RecentAll,
|
|
509
|
+
Tabs.RecentIncomplete,
|
|
510
|
+
Tabs.RecentCompleted
|
|
511
|
+
],
|
|
512
|
+
},
|
|
489
513
|
}
|
|
490
514
|
}
|
|
491
515
|
|
package/src/contentTypeConfig.js
CHANGED
|
@@ -176,13 +176,14 @@ export const lessonTypesMapping = {
|
|
|
176
176
|
'transcriptions': transcriptionsLessonTypes,
|
|
177
177
|
'tabs': transcriptionsLessonTypes,
|
|
178
178
|
'sheet music': transcriptionsLessonTypes,
|
|
179
|
-
'play
|
|
179
|
+
'play-alongs': playAlongLessonTypes,
|
|
180
|
+
'jam tracks': ['jam-track'],
|
|
180
181
|
};
|
|
181
182
|
|
|
182
183
|
|
|
183
184
|
export const filterTypes = {
|
|
184
185
|
lessons: [...individualLessonsTypes, ...collectionLessonTypes],
|
|
185
|
-
songs: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes]
|
|
186
|
+
songs: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes, 'jam-track'],
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
export const recentTypes = {
|
package/src/index.d.ts
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
fetchContentPageUserData,
|
|
112
112
|
fetchContentProgress,
|
|
113
113
|
fetchHandler,
|
|
114
|
+
fetchLastInteractedChild,
|
|
114
115
|
fetchLikeCount,
|
|
115
116
|
fetchNextContentDataForParent,
|
|
116
117
|
fetchOwnedChallenges,
|
|
@@ -292,6 +293,7 @@ declare module 'musora-content-services' {
|
|
|
292
293
|
fetchGenreLessons,
|
|
293
294
|
fetchHandler,
|
|
294
295
|
fetchHierarchy,
|
|
296
|
+
fetchLastInteractedChild,
|
|
295
297
|
fetchLeaving,
|
|
296
298
|
fetchLessonContent,
|
|
297
299
|
fetchLessonsFeaturingThisContent,
|
package/src/index.js
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
fetchContentPageUserData,
|
|
112
112
|
fetchContentProgress,
|
|
113
113
|
fetchHandler,
|
|
114
|
+
fetchLastInteractedChild,
|
|
114
115
|
fetchLikeCount,
|
|
115
116
|
fetchNextContentDataForParent,
|
|
116
117
|
fetchOwnedChallenges,
|
|
@@ -291,6 +292,7 @@ export {
|
|
|
291
292
|
fetchGenreLessons,
|
|
292
293
|
fetchHandler,
|
|
293
294
|
fetchHierarchy,
|
|
295
|
+
fetchLastInteractedChild,
|
|
294
296
|
fetchLeaving,
|
|
295
297
|
fetchLessonContent,
|
|
296
298
|
fetchLessonsFeaturingThisContent,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { HeaderProvider } from './interfaces/HeaderProvider'
|
|
2
|
+
import { RequestExecutor } from './interfaces/RequestExecutor'
|
|
3
|
+
import { RequestOptions } from './interfaces/RequestOptions'
|
|
4
|
+
import { HttpError } from './interfaces/HttpError'
|
|
5
|
+
import { NetworkError } from './interfaces/NetworkError'
|
|
6
|
+
import { DefaultHeaderProvider } from './providers/DefaultHeaderProvider'
|
|
7
|
+
import { FetchRequestExecutor } from './executors/FetchRequestExecutor'
|
|
8
|
+
|
|
9
|
+
export class HttpClient {
|
|
10
|
+
private baseUrl: string
|
|
11
|
+
private token: string | null
|
|
12
|
+
private headerProvider: HeaderProvider
|
|
13
|
+
private requestExecutor: RequestExecutor
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
baseUrl: string,
|
|
17
|
+
token: string | null = null,
|
|
18
|
+
headerProvider: HeaderProvider = new DefaultHeaderProvider(),
|
|
19
|
+
requestExecutor: RequestExecutor = new FetchRequestExecutor()
|
|
20
|
+
) {
|
|
21
|
+
this.baseUrl = baseUrl
|
|
22
|
+
this.token = token
|
|
23
|
+
this.headerProvider = headerProvider
|
|
24
|
+
this.requestExecutor = requestExecutor
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public setToken(token: string): void {
|
|
28
|
+
this.token = token
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async get<T>(url: string, dataVersion: string | null = null): Promise<T> {
|
|
32
|
+
return this.request<T>(url, 'get', dataVersion)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async post<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
|
|
36
|
+
return this.request<T>(url, 'post', dataVersion, data)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async put<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
|
|
40
|
+
return this.request<T>(url, 'put', dataVersion, data)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async patch<T>(url: string, data: any, dataVersion: string | null = null): Promise<T> {
|
|
44
|
+
return this.request<T>(url, 'patch', dataVersion, data)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async delete<T>(url: string, dataVersion: string | null = null): Promise<T> {
|
|
48
|
+
return this.request<T>(url, 'delete', dataVersion)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async request<T>(
|
|
52
|
+
url: string,
|
|
53
|
+
method: string,
|
|
54
|
+
dataVersion: string | null = null,
|
|
55
|
+
body: any = null
|
|
56
|
+
): Promise<T> {
|
|
57
|
+
try {
|
|
58
|
+
const headers = this.buildHeaders(dataVersion)
|
|
59
|
+
const options = this.buildRequestOptions(method, headers, body)
|
|
60
|
+
const fullUrl = this.resolveUrl(url)
|
|
61
|
+
|
|
62
|
+
return await this.requestExecutor.execute<T>(fullUrl, options)
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
return this.handleError(error, url, method)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private buildHeaders(dataVersion: string | null): Record<string, string> {
|
|
69
|
+
const headers = this.headerProvider.getHeaders()
|
|
70
|
+
|
|
71
|
+
// Add data version if provided
|
|
72
|
+
if (dataVersion) {
|
|
73
|
+
headers['Data-Version'] = dataVersion
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add auth token if available
|
|
77
|
+
if (this.token) {
|
|
78
|
+
headers['Authorization'] = `Bearer ${this.token}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return headers
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private buildRequestOptions(
|
|
85
|
+
method: string,
|
|
86
|
+
headers: Record<string, string>,
|
|
87
|
+
body: any
|
|
88
|
+
): RequestOptions {
|
|
89
|
+
const options: RequestOptions = {
|
|
90
|
+
method,
|
|
91
|
+
headers,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add body for non-GET requests
|
|
95
|
+
if (body) {
|
|
96
|
+
options.body = JSON.stringify(body)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return options
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private resolveUrl(url: string): string {
|
|
103
|
+
return url.startsWith('/') ? this.baseUrl + url : url
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private handleError(error: any, url: string, method: string): never {
|
|
107
|
+
if ('status' in error) {
|
|
108
|
+
// This is our formatted HTTP error from above
|
|
109
|
+
throw error as HttpError
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Network or other errors
|
|
113
|
+
throw {
|
|
114
|
+
message: error.message || 'Network error',
|
|
115
|
+
url,
|
|
116
|
+
method,
|
|
117
|
+
originalError: error,
|
|
118
|
+
} as NetworkError
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RequestExecutor } from '../interfaces/RequestExecutor'
|
|
2
|
+
import { RequestOptions } from '../interfaces/RequestOptions'
|
|
3
|
+
import { HttpError } from '../interfaces/HttpError'
|
|
4
|
+
|
|
5
|
+
export class FetchRequestExecutor implements RequestExecutor {
|
|
6
|
+
async execute<T>(url: string, options: RequestOptions): Promise<T> {
|
|
7
|
+
const response = await fetch(url, options)
|
|
8
|
+
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
throw await this.createHttpError(response, url, options.method)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return this.parseResponse<T>(response)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private async createHttpError(
|
|
17
|
+
response: Response,
|
|
18
|
+
url: string,
|
|
19
|
+
method: string
|
|
20
|
+
): Promise<HttpError> {
|
|
21
|
+
const error: HttpError = {
|
|
22
|
+
status: response.status,
|
|
23
|
+
statusText: response.statusText,
|
|
24
|
+
url,
|
|
25
|
+
method,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
error.body = await response.json()
|
|
30
|
+
} catch (e) {
|
|
31
|
+
error.body = await response.text()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async parseResponse<T>(response: Response): Promise<T> {
|
|
38
|
+
const contentType = response.headers.get('content-type')
|
|
39
|
+
if (contentType && contentType.indexOf('application/json') !== -1) {
|
|
40
|
+
return (await response.json()) as T
|
|
41
|
+
} else {
|
|
42
|
+
return (await response.text()) as unknown as T
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Interfaces
|
|
2
|
+
export { RequestOptions } from './interfaces/RequestOptions'
|
|
3
|
+
export { HttpError } from './interfaces/HttpError'
|
|
4
|
+
export { NetworkError } from './interfaces/NetworkError'
|
|
5
|
+
export { HeaderProvider } from './interfaces/HeaderProvider'
|
|
6
|
+
export { RequestExecutor } from './interfaces/RequestExecutor'
|
|
7
|
+
|
|
8
|
+
// Implementations
|
|
9
|
+
export { DefaultHeaderProvider } from './providers/DefaultHeaderProvider'
|
|
10
|
+
export { FetchRequestExecutor } from './executors/FetchRequestExecutor'
|
|
11
|
+
|
|
12
|
+
// Main client
|
|
13
|
+
export { HttpClient } from './HttpClient'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HeaderProvider } from '../interfaces/HeaderProvider'
|
|
2
|
+
import { globalConfig } from '../../../services/config.js'
|
|
3
|
+
|
|
4
|
+
export class DefaultHeaderProvider implements HeaderProvider {
|
|
5
|
+
getHeaders(): Record<string, string> {
|
|
6
|
+
const headers: Record<string, string> = {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
Accept: 'application/json',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Handle timezone
|
|
12
|
+
if (globalConfig.localTimezoneString) {
|
|
13
|
+
headers['M-Client-Timezone'] = globalConfig.localTimezoneString
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Add CSRF token if present in document
|
|
17
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
18
|
+
if (csrfToken) {
|
|
19
|
+
headers['X-CSRF-TOKEN'] = csrfToken
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return headers
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/lib/lastUpdated.js
CHANGED
|
File without changes
|
package/src/services/config.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -232,7 +232,7 @@ export async function updatePlaylist(playlistId, {
|
|
|
232
232
|
...item_order && { item_order },
|
|
233
233
|
}
|
|
234
234
|
const url = `${BASE_PATH}/v1/user/playlists/${playlistId}`
|
|
235
|
-
return await fetchHandler(url, '
|
|
235
|
+
return await fetchHandler(url, 'PUT', null, data);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/**
|
package/src/services/content.js
CHANGED
|
@@ -21,23 +21,18 @@ import {recommendations} from "./recommendations";
|
|
|
21
21
|
|
|
22
22
|
export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
|
|
23
23
|
let recentContentIds = await fetchRecent(brand, pageName, { progress: 'recent' });
|
|
24
|
-
recentContentIds = recentContentIds.map(item => item.id);
|
|
25
24
|
|
|
26
25
|
let contentRows = await getContentRows(brand, pageName);
|
|
27
26
|
contentRows = Array.isArray(contentRows) ? contentRows : [];
|
|
28
27
|
contentRows.unshift({
|
|
29
28
|
id: 'recent',
|
|
30
29
|
title: 'Recent ' + capitalizeFirstLetter(pageName),
|
|
31
|
-
|
|
30
|
+
items: recentContentIds || []
|
|
32
31
|
});
|
|
33
32
|
|
|
34
33
|
const results = await Promise.all(
|
|
35
34
|
contentRows.map(async (row) => {
|
|
36
|
-
|
|
37
|
-
return { id: row.id, title: row.title, items: [] }
|
|
38
|
-
}
|
|
39
|
-
const data = await fetchByRailContentIds(row.content)
|
|
40
|
-
return { id: row.id, title: row.title, items: data }
|
|
35
|
+
return { id: row.id, title: row.title, items: row.items }
|
|
41
36
|
})
|
|
42
37
|
)
|
|
43
38
|
return results
|
|
@@ -163,7 +158,8 @@ export async function getRecent(brand, pageName, tabName = 'all', {
|
|
|
163
158
|
* @param {Object} params - Parameters for pagination.
|
|
164
159
|
* @param {number} [params.page=1] - The page number for pagination.
|
|
165
160
|
* @param {number} [params.limit=10] - The maximum number of content items per row.
|
|
166
|
-
* @returns {Promise<Object>} - The fetched content rows.
|
|
161
|
+
* @returns {Promise<Object>} - The fetched content rows with complete Sanity data instead of just content IDs.
|
|
162
|
+
* When contentRowId is provided, returns an object with type, data, and meta properties.
|
|
167
163
|
*
|
|
168
164
|
* @example
|
|
169
165
|
* getContentRows('drumeo', 'lessons', 'Your-Daily-Warmup', {
|
|
@@ -179,7 +175,26 @@ export async function getContentRows(brand, pageName, contentRowId , {
|
|
|
179
175
|
} = {}) {
|
|
180
176
|
const contentRow = contentRowId ? `&content_row_id=${contentRowId}` : ''
|
|
181
177
|
const url = `/api/content/v1/rows?brand=${brand}&page_name=${pageName}${contentRow}&page=${page}&limit=${limit}`;
|
|
182
|
-
|
|
178
|
+
const contentRows = await fetchHandler(url, 'get', null) || [];
|
|
179
|
+
const results = await Promise.all(
|
|
180
|
+
contentRows.map(async (row) => {
|
|
181
|
+
if (row.content.length === 0){
|
|
182
|
+
return { id: row.id, title: row.title, items: [] }
|
|
183
|
+
}
|
|
184
|
+
const data = await fetchByRailContentIds(row.content)
|
|
185
|
+
return { id: row.id, title: row.title, items: data }
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if (contentRowId) {
|
|
190
|
+
return {
|
|
191
|
+
type: TabResponseType.CATALOG,
|
|
192
|
+
data: results[0].items,
|
|
193
|
+
meta: {}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return results
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProgressStateByIds, getProgressPercentageByIds, getResumeTimeSecondsByIds } from "./contentProgress"
|
|
2
2
|
import { isContentLikedByIds } from "./contentLikes"
|
|
3
|
-
import { fetchLikeCount } from "./railcontent"
|
|
3
|
+
import { fetchLikeCount, fetchLastInteractedChild } from "./railcontent"
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -15,7 +15,8 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
15
15
|
addIsLiked = false,
|
|
16
16
|
addLikeCount = false,
|
|
17
17
|
addProgressStatus = false,
|
|
18
|
-
addResumeTimeSeconds = false
|
|
18
|
+
addResumeTimeSeconds = false,
|
|
19
|
+
addLastInteractedChild = false,
|
|
19
20
|
} = options
|
|
20
21
|
|
|
21
22
|
const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
|
|
@@ -35,13 +36,15 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
35
36
|
|
|
36
37
|
if(ids.length === 0) return false
|
|
37
38
|
|
|
38
|
-
const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData] = await Promise.all([
|
|
39
|
+
const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData] = await Promise.all([
|
|
39
40
|
addProgressPercentage ? getProgressPercentageByIds(ids) : Promise.resolve(null),
|
|
40
41
|
addProgressStatus ? getProgressStateByIds(ids) : Promise.resolve(null),
|
|
41
42
|
addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
|
|
42
43
|
addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
|
|
44
|
+
addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
|
|
43
45
|
])
|
|
44
|
-
|
|
46
|
+
console.log('ids', ids)
|
|
47
|
+
console.log('lastInteractedChildData', lastInteractedChildData)
|
|
45
48
|
const addContext = async (item) => ({
|
|
46
49
|
...item,
|
|
47
50
|
...(addProgressPercentage ? { progressPercentage: progressPercentageData?.[item.id] } : {}),
|
|
@@ -49,12 +52,17 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
49
52
|
...(addIsLiked ? { isLiked: isLikedData?.[item.id] } : {}),
|
|
50
53
|
...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
|
|
51
54
|
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
|
|
55
|
+
...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
|
|
52
56
|
})
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (dataField) {
|
|
58
|
+
data[dataField] = Array.isArray(data[dataField])
|
|
59
|
+
? await Promise.all(data[dataField].map(addContext))
|
|
60
|
+
: await addContext(data[dataField])
|
|
61
|
+
return data
|
|
62
|
+
} else {
|
|
63
|
+
return Array.isArray(data)
|
|
64
|
+
? await Promise.all(data.map(addContext))
|
|
65
|
+
: await addContext(data)
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
|
|
File without changes
|
|
@@ -821,6 +821,34 @@ export async function fetchUserPracticeNotes(date) {
|
|
|
821
821
|
return await fetchHandler(url, 'GET', null)
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Get the id and slug of last interacted child. Only valid for certain content types
|
|
827
|
+
*
|
|
828
|
+
* @async
|
|
829
|
+
* @function fetchLastInteractedChild
|
|
830
|
+
* @param {array} content_ids - Content ids of to get the last interacted child of
|
|
831
|
+
*
|
|
832
|
+
*
|
|
833
|
+
* @returns {Promise<Object>} - keyed object per valid content ids with the child
|
|
834
|
+
*
|
|
835
|
+
* @example
|
|
836
|
+
* try {
|
|
837
|
+
* const response = await fetchLastInteractedChild([191369, 410427]);
|
|
838
|
+
* console.log('child id', response[191369].content_id)
|
|
839
|
+
* console.log('child slug', response[191369].slug)
|
|
840
|
+
* } catch (error) {
|
|
841
|
+
* console.error('Failed to get children', error);
|
|
842
|
+
* }
|
|
843
|
+
*/
|
|
844
|
+
export async function fetchLastInteractedChild(content_ids) {
|
|
845
|
+
const params = new URLSearchParams();
|
|
846
|
+
content_ids.forEach(id => params.append('content_ids[]', id));
|
|
847
|
+
const url = `/api/content/v1/user/last_interacted_child?${params.toString()}`
|
|
848
|
+
return await fetchHandler(url, 'GET', null)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
|
|
824
852
|
function fetchAbsolute(url, params) {
|
|
825
853
|
if (globalConfig.sessionConfig.authToken) {
|
|
826
854
|
params.headers['Authorization'] = `Bearer ${globalConfig.sessionConfig.authToken}`
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @module Railcontent-Services
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {globalConfig} from './config.js'
|
|
6
|
-
import {
|
|
5
|
+
import { globalConfig } from './config.js'
|
|
6
|
+
import { HttpClient } from '../infrastructure/http/HttpClient'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Exported functions that are excluded from index generation.
|
|
@@ -36,7 +36,11 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
|
|
|
36
36
|
}
|
|
37
37
|
const url = `/similar_items/`
|
|
38
38
|
try {
|
|
39
|
-
const
|
|
39
|
+
const httpClient = new HttpClient(
|
|
40
|
+
globalConfig.recommendationsConfig.baseUrl,
|
|
41
|
+
globalConfig.recommendationsConfig.token
|
|
42
|
+
)
|
|
43
|
+
const response = await httpClient.post(url, data)
|
|
40
44
|
// we requested count + 1 then filtered out the extra potential value, so we need slice to the correct size if necessary
|
|
41
45
|
return response['similar_items'].filter((item) => item !== content_id).slice(0, count)
|
|
42
46
|
} catch (error) {
|
|
@@ -71,7 +75,11 @@ export async function rankCategories(brand, categories) {
|
|
|
71
75
|
}
|
|
72
76
|
const url = `/rank_each_list/`
|
|
73
77
|
try {
|
|
74
|
-
const
|
|
78
|
+
const httpClient = new HttpClient(
|
|
79
|
+
globalConfig.recommendationsConfig.baseUrl,
|
|
80
|
+
globalConfig.recommendationsConfig.token
|
|
81
|
+
)
|
|
82
|
+
const response = await httpClient.post(url, data)
|
|
75
83
|
let rankedCategories = {}
|
|
76
84
|
response['ranked_playlists'].forEach(
|
|
77
85
|
(category) =>
|
|
@@ -106,7 +114,11 @@ export async function rankItems(brand, content_ids) {
|
|
|
106
114
|
}
|
|
107
115
|
const url = `/rank_items/`
|
|
108
116
|
try {
|
|
109
|
-
const
|
|
117
|
+
const httpClient = new HttpClient(
|
|
118
|
+
globalConfig.recommendationsConfig.baseUrl,
|
|
119
|
+
globalConfig.recommendationsConfig.token
|
|
120
|
+
)
|
|
121
|
+
const response = await httpClient.post(url, data)
|
|
110
122
|
return response['ranked_content_ids']
|
|
111
123
|
} catch (error) {
|
|
112
124
|
console.error('Fetch error:', error)
|
|
@@ -114,31 +126,15 @@ export async function rankItems(brand, content_ids) {
|
|
|
114
126
|
}
|
|
115
127
|
}
|
|
116
128
|
|
|
117
|
-
export async function recommendations(brand, {section = ''} = {}) {
|
|
129
|
+
export async function recommendations(brand, { section = '' } = {}) {
|
|
118
130
|
section = section.toUpperCase().replace('-', '_')
|
|
119
|
-
const sectionString = section ? `§ion=${section}` : ''
|
|
131
|
+
const sectionString = section ? `§ion=${section}` : ''
|
|
120
132
|
const url = `/api/content/v1/recommendations?brand=${brand}${sectionString}`
|
|
121
133
|
try {
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
url,
|
|
125
|
-
globalConfig.sessionConfig.token,
|
|
126
|
-
globalConfig.baseUrl,
|
|
127
|
-
'get'
|
|
128
|
-
)
|
|
134
|
+
const httpClient = new HttpClient(globalConfig.baseUrl, globalConfig.sessionConfig.token)
|
|
135
|
+
return httpClient.get(url)
|
|
129
136
|
} catch (error) {
|
|
130
137
|
console.error('Fetch error:', error)
|
|
131
138
|
return null
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
|
-
|
|
135
|
-
async function fetchHandler(url, method = 'get', body = null) {
|
|
136
|
-
return fetchJSONHandler(
|
|
137
|
-
url,
|
|
138
|
-
globalConfig.recommendationsConfig.token,
|
|
139
|
-
globalConfig.recommendationsConfig.baseUrl,
|
|
140
|
-
method,
|
|
141
|
-
null,
|
|
142
|
-
body
|
|
143
|
-
)
|
|
144
|
-
}
|
|
File without changes
|