musora-content-services 2.122.6 → 2.123.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 +28 -0
- package/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/src/contentMetaData.js +17 -0
- package/src/contentTypeConfig.js +2 -1
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -0
- package/src/services/content-org/learning-paths.ts +3 -7
- package/src/services/content.js +3 -3
- package/src/services/permissions/PermissionsAdapter.ts +12 -0
- package/src/services/progress-row/rows/content-card.js +3 -25
- package/src/services/recommendations.js +26 -13
- package/src/services/sanity.js +17 -3
- package/src/services/sync/repositories/content-progress.ts +3 -1
- package/src/services/sync/resolver.ts +9 -1
- package/src/services/sync/store/index.ts +7 -1
- package/src/services/sync/telemetry/index.ts +9 -6
- package/src/services/user/sessions.js +15 -2
- package/src/services/userActivity.js +5 -3
- package/test/sync/initialize-sync-manager.js +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
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.123.0](https://github.com/railroadmedia/musora-content-services/compare/v2.122.7...v2.123.0) (2026-01-28)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* better user impersonation ([#731](https://github.com/railroadmedia/musora-content-services/issues/731)) ([768022d](https://github.com/railroadmedia/musora-content-services/commit/768022df839b2c2e206d02985c82075b0d4de008))
|
|
11
|
+
* **T3PS-1537:** Playbass Recommendations V2 ([#744](https://github.com/railroadmedia/musora-content-services/issues/744)) ([b88ffbd](https://github.com/railroadmedia/musora-content-services/commit/b88ffbd85a9982371e108d9f23190a9217cf29d0))
|
|
12
|
+
* **TP-1080:** ignore resume time until past 10s window ([#749](https://github.com/railroadmedia/musora-content-services/issues/749)) ([c22f4c2](https://github.com/railroadmedia/musora-content-services/commit/c22f4c201ec60dbf9475eebbcee199dfea967bbf))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* makes methodIntroComplete function send formatted date matching getDailySession ([#725](https://github.com/railroadmedia/musora-content-services/issues/725)) ([6ac47a0](https://github.com/railroadmedia/musora-content-services/commit/6ac47a04dfa2ea4a0d355409b4be8fa743af1b48))
|
|
18
|
+
|
|
19
|
+
### [2.122.7](https://github.com/railroadmedia/musora-content-services/compare/v2.122.3...v2.122.7) (2026-01-28)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* add artist name to practices ([aba6c46](https://github.com/railroadmedia/musora-content-services/commit/aba6c46e03afd6635e74050170cbec5e3b1c9a63))
|
|
25
|
+
* add artist name to recent activity data ([78f0e47](https://github.com/railroadmedia/musora-content-services/commit/78f0e4783d575db30aa6667752ded4366e85b05a))
|
|
26
|
+
* filters out null field ([#742](https://github.com/railroadmedia/musora-content-services/issues/742)) ([cab0b58](https://github.com/railroadmedia/musora-content-services/commit/cab0b584a8f40b7404b869730783aea51a058d35))
|
|
27
|
+
* melon data user isolation ([#717](https://github.com/railroadmedia/musora-content-services/issues/717)) ([6893c3c](https://github.com/railroadmedia/musora-content-services/commit/6893c3c644e1eefcfeeb6439b460a46d853616d6))
|
|
28
|
+
* **T3PS-1586:** onboarding recommendation pinning ([#737](https://github.com/railroadmedia/musora-content-services/issues/737)) ([280305b](https://github.com/railroadmedia/musora-content-services/commit/280305b7706ba171752cb54e6d2ee504093b11d1))
|
|
29
|
+
* **T3PS-2003:** Display Shows like Individuals on the progress cards and progress areas ([5a61d7d](https://github.com/railroadmedia/musora-content-services/commit/5a61d7dfaf5fbed2cc8bdca751c12783acbe2b66))
|
|
30
|
+
* **T3PS-2004:** Play Your Favorite Songs on /method/ Shows Unreleased Content ([b7c32de](https://github.com/railroadmedia/musora-content-services/commit/b7c32deeffe8140969d2e11d853d255b0e184f30))
|
|
31
|
+
* **T3PS-2007:** getPracticeSessions for a given date fails if the content doesn’t have a thumbnail ([d3bb457](https://github.com/railroadmedia/musora-content-services/commit/d3bb45760634f8e8f8f8114859d258da5d3909e6))
|
|
32
|
+
|
|
5
33
|
### [2.122.6](https://github.com/railroadmedia/musora-content-services/compare/v2.122.5...v2.122.6) (2026-01-28)
|
|
6
34
|
|
|
7
35
|
|
package/CLAUDE.md
CHANGED
|
@@ -328,7 +328,7 @@ import { ContentLike, ContentProgress, Practice, PracticeDayNote } from 'musora-
|
|
|
328
328
|
// - DurabilityProvider: no-op (storage always available on mobile)
|
|
329
329
|
// - TabsProvider: no-op (single "tab" on mobile)
|
|
330
330
|
|
|
331
|
-
const manager = new SyncManager(context, db)
|
|
331
|
+
const manager = new SyncManager(userScope, context, db)
|
|
332
332
|
manager.registerStrategies(
|
|
333
333
|
ContentLike, ContentProgress, Practice, PracticeDayNote],
|
|
334
334
|
[initialStrategy, onlineStrategy, activityStrategy, hourlyPollingStrategy]
|
package/package.json
CHANGED
package/src/contentMetaData.js
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
const PROGRESS_NAMES = ['All', 'In Progress', 'Completed', 'Not Started']
|
|
5
5
|
const DIFFICULTY_STRINGS = ['Introductory', 'Beginner', 'Intermediate', 'Advanced', 'Expert']
|
|
6
6
|
|
|
7
|
+
export const CONTENT_STATUSES = {
|
|
8
|
+
PUBLISHED_ONLY: ['published'],
|
|
9
|
+
ADMIN_ALL: ['draft', 'scheduled', 'published', 'archived', 'unlisted'],
|
|
10
|
+
PUBLIC_WITH_SCHEDULED: ['published', 'scheduled'],
|
|
11
|
+
DRAFT_ONLY: ['draft'],
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const LESSON_TYPE_FILTER = [
|
|
8
15
|
{
|
|
9
16
|
title: 'Single Lessons',
|
|
@@ -49,6 +56,7 @@ export class LengthFilterOptions {
|
|
|
49
56
|
|
|
50
57
|
export class Tabs {
|
|
51
58
|
static ForYou = { name: 'For You', short_name: 'For You', value: 'tab,for you' }
|
|
59
|
+
static PlaybassAll = { name: 'All', short_name: 'All', value: 'tab,for you' }
|
|
52
60
|
static Individuals = { name: 'Individuals', short_name: 'Individuals', value: 'type,individuals', cardType: 'big' }
|
|
53
61
|
static Collections = { name: 'Collections', short_name: 'Collections', value: 'type,collections', cardType: 'big' }
|
|
54
62
|
static ExploreAll = { name: 'Explore All', short_name: 'Explore All', value: 'tab,explore all', icon: 'icon-filters', cardType: 'big'}
|
|
@@ -323,6 +331,15 @@ const contentMetadata = {
|
|
|
323
331
|
},
|
|
324
332
|
playbass: {
|
|
325
333
|
'songs-types': ['Tutorials', 'Tabs', 'Play-Alongs', 'Jam Tracks'],
|
|
334
|
+
lessons: {
|
|
335
|
+
tabs: [
|
|
336
|
+
Tabs.PlaybassAll,
|
|
337
|
+
Tabs.SingleLessons,
|
|
338
|
+
Tabs.Courses,
|
|
339
|
+
Tabs.SkillPacks,
|
|
340
|
+
Tabs.ExploreAll,
|
|
341
|
+
]
|
|
342
|
+
},
|
|
326
343
|
},
|
|
327
344
|
singeo: {
|
|
328
345
|
'songs-types': ['Tutorials', 'Sheet Music', 'Play-Alongs', 'Jam Tracks'],
|
package/src/contentTypeConfig.js
CHANGED
|
@@ -238,7 +238,7 @@ export const showsLessonTypes = [
|
|
|
238
238
|
'performance',
|
|
239
239
|
]
|
|
240
240
|
export const entertainmentLessonTypes = ['special', 'documentary-lesson', ...showsLessonTypes]
|
|
241
|
-
export const collectionLessonTypes = [...coursesLessonTypes
|
|
241
|
+
export const collectionLessonTypes = [...coursesLessonTypes]
|
|
242
242
|
|
|
243
243
|
export const lessonTypesMapping = {
|
|
244
244
|
lessons: singleLessonTypes,
|
|
@@ -350,6 +350,7 @@ export const recentTypes = {
|
|
|
350
350
|
...skillLessonTypes,
|
|
351
351
|
...transcriptionsLessonTypes,
|
|
352
352
|
...playAlongLessonTypes,
|
|
353
|
+
...showsLessonTypes,
|
|
353
354
|
'guided-course',
|
|
354
355
|
'learning-path-v2',
|
|
355
356
|
'live',
|
package/src/index.d.ts
CHANGED
|
@@ -211,7 +211,9 @@ import {
|
|
|
211
211
|
|
|
212
212
|
import {
|
|
213
213
|
getProgressRows,
|
|
214
|
+
getUserPinProgressKey,
|
|
214
215
|
pinProgressRow,
|
|
216
|
+
setUserPinnedProgressRow,
|
|
215
217
|
unpinProgressRow
|
|
216
218
|
} from './services/progress-row/base.js';
|
|
217
219
|
|
|
@@ -634,6 +636,7 @@ declare module 'musora-content-services' {
|
|
|
634
636
|
getUpgradePrice,
|
|
635
637
|
getUserData,
|
|
636
638
|
getUserMonthlyStats,
|
|
639
|
+
getUserPinProgressKey,
|
|
637
640
|
getUserSignature,
|
|
638
641
|
getUserWeeklyStats,
|
|
639
642
|
getWeekNumber,
|
|
@@ -702,6 +705,7 @@ declare module 'musora-content-services' {
|
|
|
702
705
|
sendAccountSetupEmail,
|
|
703
706
|
sendPasswordResetEmail,
|
|
704
707
|
setStudentViewForUser,
|
|
708
|
+
setUserPinnedProgressRow,
|
|
705
709
|
setUserSignature,
|
|
706
710
|
setupAccount,
|
|
707
711
|
startLearningPath,
|
package/src/index.js
CHANGED
|
@@ -215,7 +215,9 @@ import {
|
|
|
215
215
|
|
|
216
216
|
import {
|
|
217
217
|
getProgressRows,
|
|
218
|
+
getUserPinProgressKey,
|
|
218
219
|
pinProgressRow,
|
|
220
|
+
setUserPinnedProgressRow,
|
|
219
221
|
unpinProgressRow
|
|
220
222
|
} from './services/progress-row/base.js';
|
|
221
223
|
|
|
@@ -633,6 +635,7 @@ export {
|
|
|
633
635
|
getUpgradePrice,
|
|
634
636
|
getUserData,
|
|
635
637
|
getUserMonthlyStats,
|
|
638
|
+
getUserPinProgressKey,
|
|
636
639
|
getUserSignature,
|
|
637
640
|
getUserWeeklyStats,
|
|
638
641
|
getWeekNumber,
|
|
@@ -701,6 +704,7 @@ export {
|
|
|
701
704
|
sendAccountSetupEmail,
|
|
702
705
|
sendPasswordResetEmail,
|
|
703
706
|
setStudentViewForUser,
|
|
707
|
+
setUserPinnedProgressRow,
|
|
704
708
|
setUserSignature,
|
|
705
709
|
setupAccount,
|
|
706
710
|
startLearningPath,
|
|
@@ -442,14 +442,10 @@ export async function completeMethodIntroVideo(
|
|
|
442
442
|
return response
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
async function methodIntroVideoCompleteActions(
|
|
446
|
-
|
|
447
|
-
learningPathId: number,
|
|
448
|
-
userDate: Date
|
|
449
|
-
) {
|
|
450
|
-
const stringDate = userDate.toISOString().split('T')[0]
|
|
445
|
+
async function methodIntroVideoCompleteActions(brand: string, learningPathId: number, userDate: Date) {
|
|
446
|
+
const dateWithTimezone = formatLocalDateTime(userDate)
|
|
451
447
|
const url: string = `${LEARNING_PATHS_PATH}/method-intro-video-complete-actions`
|
|
452
|
-
const body = { brand: brand, learningPathId: learningPathId, userDate:
|
|
448
|
+
const body = { brand: brand, learningPathId: learningPathId, userDate: dateWithTimezone }
|
|
453
449
|
return (await POST(url, body)) as DailySessionResponse
|
|
454
450
|
}
|
|
455
451
|
|
package/src/services/content.js
CHANGED
|
@@ -471,10 +471,10 @@ export async function getRecommendedForYou(brand, rowId = null, {
|
|
|
471
471
|
} = {}) {
|
|
472
472
|
const requiredItems = page * limit;
|
|
473
473
|
const data = await recommendations( brand, {limit: requiredItems})
|
|
474
|
+
const title = brand === 'playbass' ? "You Might Like" : "Recommended For You"
|
|
474
475
|
if (!data || !data.length) {
|
|
475
|
-
return { id: 'recommended', title:
|
|
476
|
+
return { id: 'recommended', title: title, items: [] };
|
|
476
477
|
}
|
|
477
|
-
|
|
478
478
|
// Apply pagination before calling fetchByRailContentIds
|
|
479
479
|
const startIndex = (page - 1) * limit;
|
|
480
480
|
const paginatedData = data.slice(startIndex, startIndex + limit);
|
|
@@ -491,7 +491,7 @@ export async function getRecommendedForYou(brand, rowId = null, {
|
|
|
491
491
|
};
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
return { id: 'recommended', title:
|
|
494
|
+
return { id: 'recommended', title: title, items: contents }
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
|
|
@@ -14,6 +14,8 @@ export interface UserPermissions {
|
|
|
14
14
|
permissions: string[]
|
|
15
15
|
/** Whether the user is an admin */
|
|
16
16
|
isAdmin: boolean
|
|
17
|
+
/** Whether the user is a moderator */
|
|
18
|
+
isModerator: boolean
|
|
17
19
|
/** Whether the user has basic membership */
|
|
18
20
|
isABasicMember: boolean
|
|
19
21
|
/** User's access level (v2 - for future use) */
|
|
@@ -108,4 +110,14 @@ export abstract class PermissionsAdapter {
|
|
|
108
110
|
isAdmin(userPermissions: UserPermissions): boolean {
|
|
109
111
|
return userPermissions?.isAdmin ?? false
|
|
110
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if user is a moderator.
|
|
116
|
+
*
|
|
117
|
+
* @param userPermissions - The user's permissions
|
|
118
|
+
* @returns True if user is moderator
|
|
119
|
+
*/
|
|
120
|
+
isModerator(userPermissions: UserPermissions): boolean {
|
|
121
|
+
return userPermissions?.isModerator ?? false
|
|
122
|
+
}
|
|
111
123
|
}
|
|
@@ -58,7 +58,7 @@ function generateContentPromises(contents) {
|
|
|
58
58
|
const allRecentTypeSet = new Set(Object.values(recentTypes).flat())
|
|
59
59
|
contents.forEach((content) => {
|
|
60
60
|
const type = content.type
|
|
61
|
-
if (!allRecentTypeSet.has(type)
|
|
61
|
+
if (!allRecentTypeSet.has(type)) return
|
|
62
62
|
let childHasParent = Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0
|
|
63
63
|
if (!childHasParent) {
|
|
64
64
|
promises.push(processContentItem(content))
|
|
@@ -100,28 +100,6 @@ export async function processContentItem(content) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
if (contentType === 'show') {
|
|
104
|
-
const shows = await fetchShows(content.brand, content.type)
|
|
105
|
-
const showIds = shows.map((item) => item.id)
|
|
106
|
-
const progressOnItems = await getProgressStateByIds(showIds)
|
|
107
|
-
const completedShows = content.completed_children
|
|
108
|
-
const progressTimestamp = content.progressTimestamp
|
|
109
|
-
const wasPinned = content.pinned ?? false
|
|
110
|
-
if (content.progressStatus === 'completed') {
|
|
111
|
-
// this could be handled more gracefully if there was a parent content type for shows
|
|
112
|
-
// Update Dec 3rd. We updated almost everything to the DocumentaryType :D, but there's still a few
|
|
113
|
-
const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
|
|
114
|
-
content = shows.find((lesson) => lesson.id === nextByProgress)
|
|
115
|
-
content.completed_children = completedShows
|
|
116
|
-
content.progressTimestamp = progressTimestamp
|
|
117
|
-
content.pinned = wasPinned
|
|
118
|
-
}
|
|
119
|
-
content.child_count = shows.length
|
|
120
|
-
content.progressPercentage = Math.round((completedShows / shows.length) * 100)
|
|
121
|
-
if (completedShows === shows.length) {
|
|
122
|
-
ctaText = 'Revisit Show'
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
103
|
return {
|
|
126
104
|
id: content.id,
|
|
127
105
|
progressType: 'content',
|
|
@@ -138,7 +116,7 @@ export async function processContentItem(content) {
|
|
|
138
116
|
subtitle:
|
|
139
117
|
collectionLessonTypes.includes(content.type) || content.lesson_count > 1
|
|
140
118
|
? `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
|
|
141
|
-
: contentType === 'lesson' && isLive === false
|
|
119
|
+
: (contentType === 'lesson' || contentType === 'show') && isLive === false
|
|
142
120
|
? `${content.progressPercentage}% Complete`
|
|
143
121
|
: `${content.difficulty_string} • ${content.artist_name}`,
|
|
144
122
|
},
|
|
@@ -166,7 +144,7 @@ function getDefaultCTATextForContent(content, contentType) {
|
|
|
166
144
|
contentType === 'jam track'
|
|
167
145
|
)
|
|
168
146
|
ctaText = 'Replay Song'
|
|
169
|
-
if (contentType === 'lesson') ctaText = 'Revisit Lesson'
|
|
147
|
+
if (contentType === 'lesson' || contentType === 'show') ctaText = 'Revisit Lesson'
|
|
170
148
|
if (contentType === 'song tutorial' || collectionLessonTypes.includes(content.type))
|
|
171
149
|
ctaText = 'Revisit Lessons'
|
|
172
150
|
if (contentType === 'course-collection') ctaText = 'View Lessons'
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { globalConfig } from './config.js'
|
|
6
6
|
import { GET, HttpClient } from '../infrastructure/http/HttpClient.ts'
|
|
7
|
+
import { fetchByRailContentIds } from './sanity.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Exported functions that are excluded from index generation.
|
|
@@ -31,20 +32,32 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
|
|
|
31
32
|
if (!content_id) {
|
|
32
33
|
return []
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
if (brand === 'playbass') {
|
|
36
|
+
// V2 launch customization for playbass
|
|
37
|
+
const content = (await fetchByRailContentIds([content_id], 'tab-data'))[0] ?? []
|
|
38
|
+
if (!content) {
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
const section = content.page_type === 'song' ? 'song' : ''
|
|
42
|
+
const recs = await recommendations('playbass', {section: section})
|
|
43
|
+
return recs.slice(0, count)
|
|
44
|
+
} else {
|
|
45
|
+
content_id = parseInt(content_id)
|
|
46
|
+
const data = {
|
|
47
|
+
brand: brand,
|
|
48
|
+
content_ids: content_id,
|
|
49
|
+
num_similar: count + 1,
|
|
50
|
+
}
|
|
51
|
+
const url = `/similar_items/`
|
|
52
|
+
try {
|
|
53
|
+
const response = await recommenderClient.post(url, data)
|
|
54
|
+
return response['similar_items'].filter((item) => item !== content_id).slice(0, count)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Fetch error:', error)
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
47
59
|
}
|
|
60
|
+
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
/**
|
package/src/services/sanity.js
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
liveFields,
|
|
36
36
|
} from '../contentTypeConfig.js'
|
|
37
37
|
import { fetchSimilarItems, recommendations } from './recommendations.js'
|
|
38
|
-
import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS } from '../contentMetaData.js'
|
|
38
|
+
import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS, CONTENT_STATUSES } from '../contentMetaData.js'
|
|
39
39
|
import { GET } from '../infrastructure/http/HttpClient.ts'
|
|
40
40
|
|
|
41
41
|
import { globalConfig } from './config.js'
|
|
@@ -115,7 +115,7 @@ export async function fetchLeaving(brand, { pageNumber = 1, contentPerPage = 20
|
|
|
115
115
|
}
|
|
116
116
|
const query = await buildQuery(
|
|
117
117
|
filterString,
|
|
118
|
-
{ pullFutureContent: false, availableContentStatuses:
|
|
118
|
+
{ pullFutureContent: false, availableContentStatuses: CONTENT_STATUSES.PUBLISHED_ONLY },
|
|
119
119
|
getFieldsForContentType('leaving'),
|
|
120
120
|
sortOrder
|
|
121
121
|
)
|
|
@@ -142,7 +142,7 @@ export async function fetchReturning(brand, { pageNumber = 1, contentPerPage = 2
|
|
|
142
142
|
}
|
|
143
143
|
const query = await buildQuery(
|
|
144
144
|
filterString,
|
|
145
|
-
{ pullFutureContent: true, availableContentStatuses:
|
|
145
|
+
{ pullFutureContent: true, availableContentStatuses: CONTENT_STATUSES.DRAFT_ONLY },
|
|
146
146
|
getFieldsForContentType('returning'),
|
|
147
147
|
sortOrder
|
|
148
148
|
)
|
|
@@ -615,6 +615,7 @@ export async function fetchAll(
|
|
|
615
615
|
useDefaultFields = true,
|
|
616
616
|
customFields = [],
|
|
617
617
|
progress = 'all',
|
|
618
|
+
onlyPublished = true
|
|
618
619
|
} = {}
|
|
619
620
|
) {
|
|
620
621
|
let config = contentTypeConfig[type] ?? {}
|
|
@@ -663,6 +664,9 @@ export async function fetchAll(
|
|
|
663
664
|
if (type == 'instructor') {
|
|
664
665
|
customFilter = '&& coach_card_image != null'
|
|
665
666
|
}
|
|
667
|
+
if (onlyPublished) {
|
|
668
|
+
customFilter = ' && status == "published" '
|
|
669
|
+
}
|
|
666
670
|
// Determine the group by clause
|
|
667
671
|
let query = ''
|
|
668
672
|
let entityFieldsString = ''
|
|
@@ -1907,8 +1911,18 @@ export async function fetchTabData(
|
|
|
1907
1911
|
),
|
|
1908
1912
|
length_in_seconds
|
|
1909
1913
|
),`
|
|
1914
|
+
|
|
1915
|
+
// Check if user is admin to determine available content statuses
|
|
1916
|
+
const adapter = getPermissionsAdapter()
|
|
1917
|
+
const userData = await adapter.fetchUserPermissions()
|
|
1918
|
+
const isAdminORModerator = adapter.isAdmin(userData) || adapter.isModerator(userData)
|
|
1919
|
+
|
|
1910
1920
|
const filterWithRestrictions = await new FilterBuilder(filter, {
|
|
1911
1921
|
showMembershipRestrictedContent: true,
|
|
1922
|
+
availableContentStatuses: isAdminORModerator
|
|
1923
|
+
? CONTENT_STATUSES.ADMIN_ALL
|
|
1924
|
+
: CONTENT_STATUSES.PUBLISHED_ONLY,
|
|
1925
|
+
pullFutureContent: isAdminORModerator ? true : false,
|
|
1912
1926
|
}).buildFilter()
|
|
1913
1927
|
query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
|
|
1914
1928
|
sortOrder: sortOrder,
|
|
@@ -154,7 +154,9 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
154
154
|
r.progress_percent = progressPct
|
|
155
155
|
|
|
156
156
|
if (typeof resumeTime != 'undefined') {
|
|
157
|
-
r.resume_time_seconds
|
|
157
|
+
if (resumeTime >= 10 || r.resume_time_seconds !== null) {
|
|
158
|
+
r.resume_time_seconds = Math.floor(resumeTime)
|
|
159
|
+
}
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
if (!fromLearningPath) {
|
|
@@ -7,6 +7,7 @@ export type SyncResolution = {
|
|
|
7
7
|
tuplesForUpdate: [BaseModel, SyncEntry][]
|
|
8
8
|
tuplesForRestore: [BaseModel, SyncEntry][]
|
|
9
9
|
idsForDestroy: RecordId[]
|
|
10
|
+
recordsForSynced: BaseModel[]
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export type SyncResolverComparator<T extends BaseModel = BaseModel> = (serverEntry: SyncEntryNonDeleted<T>, localModel: T) => 'SERVER' | 'LOCAL'
|
|
@@ -24,7 +25,8 @@ export default class SyncResolver {
|
|
|
24
25
|
entriesForCreate: [],
|
|
25
26
|
tuplesForUpdate: [],
|
|
26
27
|
tuplesForRestore: [],
|
|
27
|
-
idsForDestroy: []
|
|
28
|
+
idsForDestroy: [],
|
|
29
|
+
recordsForSynced: []
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -58,6 +60,9 @@ export default class SyncResolver {
|
|
|
58
60
|
} else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
|
|
59
61
|
// local is older, so update it with server's
|
|
60
62
|
this.resolution.tuplesForUpdate.push([local, server])
|
|
63
|
+
} else {
|
|
64
|
+
// server is older - can happen with clock skew - just mark as synced
|
|
65
|
+
this.resolution.recordsForSynced.push(local)
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -69,6 +74,9 @@ export default class SyncResolver {
|
|
|
69
74
|
} else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
|
|
70
75
|
// local is older, so update it with server's
|
|
71
76
|
this.resolution.tuplesForUpdate.push([local, server])
|
|
77
|
+
} else {
|
|
78
|
+
// server is older - can happen with clock skew - just mark as synced
|
|
79
|
+
this.resolution.recordsForSynced.push(local)
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -956,9 +956,15 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
956
956
|
r._raw._changed = ''
|
|
957
957
|
})
|
|
958
958
|
})
|
|
959
|
+
const syncedBuilds = result.recordsForSynced.map((record) => {
|
|
960
|
+
return record.prepareUpdate((r) => {
|
|
961
|
+
r._raw._status = 'synced'
|
|
962
|
+
r._raw._changed = ''
|
|
963
|
+
})
|
|
964
|
+
})
|
|
959
965
|
|
|
960
966
|
return [
|
|
961
|
-
[...destroyedBuilds, ...createdBuilds, ...updatedBuilds, ...restoreDestroyBuilds],
|
|
967
|
+
[...destroyedBuilds, ...createdBuilds, ...updatedBuilds, ...restoreDestroyBuilds, ...syncedBuilds],
|
|
962
968
|
[...restoreCreateBuilds],
|
|
963
969
|
]
|
|
964
970
|
}
|
|
@@ -67,14 +67,18 @@ export class SyncTelemetry {
|
|
|
67
67
|
Sentry,
|
|
68
68
|
level,
|
|
69
69
|
pretty,
|
|
70
|
-
}: { Sentry: SentryLike; level?: keyof typeof SeverityLevel; pretty?: boolean }
|
|
70
|
+
}: { Sentry: SentryLike; level?: SeverityLevel | keyof typeof SeverityLevel; pretty?: boolean }
|
|
71
71
|
) {
|
|
72
72
|
this.userScope = userScope
|
|
73
73
|
this.Sentry = Sentry
|
|
74
|
-
|
|
75
|
-
typeof level
|
|
76
|
-
?
|
|
77
|
-
: SeverityLevel
|
|
74
|
+
const normalizedLevel =
|
|
75
|
+
typeof level === 'number'
|
|
76
|
+
? level
|
|
77
|
+
: typeof level === 'string' && level in SeverityLevel
|
|
78
|
+
? SeverityLevel[level]
|
|
79
|
+
: undefined
|
|
80
|
+
|
|
81
|
+
this.level = typeof normalizedLevel === 'number' ? normalizedLevel : SeverityLevel.LOG
|
|
78
82
|
this.pretty = typeof pretty !== 'undefined' ? pretty : true
|
|
79
83
|
|
|
80
84
|
watermelonLogger.log = (message: unknown) => this.log(message instanceof Error ? message : ['[Watermelon]', message].join(' '))
|
|
@@ -177,7 +181,6 @@ export class SyncTelemetry {
|
|
|
177
181
|
|
|
178
182
|
_log(level: SeverityLevel, consoleMethod: 'info' | 'log' | 'warn' | 'error', message: unknown, extra?: any) {
|
|
179
183
|
if (this.level > level || this.shouldIgnoreMessage(message)) return
|
|
180
|
-
|
|
181
184
|
this._ignoreConsole = true
|
|
182
185
|
console[consoleMethod](...this.formattedConsoleMessage(message, extra))
|
|
183
186
|
this._ignoreConsole = false
|
|
@@ -131,6 +131,7 @@ export async function generateAuthSessionUrl(userId, redirectTo) {
|
|
|
131
131
|
headers.Authorization = `Bearer ${globalConfig.sessionConfig.authToken}`
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// generate auth key
|
|
134
135
|
const response = await fetch(`${baseUrl}/v1/auth-key`, {
|
|
135
136
|
method: 'GET',
|
|
136
137
|
headers,
|
|
@@ -144,11 +145,23 @@ export async function generateAuthSessionUrl(userId, redirectTo) {
|
|
|
144
145
|
const authKeyResponse = await response.json()
|
|
145
146
|
const authKey = authKeyResponse.data || authKeyResponse.auth_key
|
|
146
147
|
|
|
148
|
+
const absoluteRedirectTo = new URL(redirectTo)
|
|
149
|
+
const relativeRedirectTo = absoluteRedirectTo.pathname + absoluteRedirectTo.search
|
|
150
|
+
|
|
147
151
|
const params = new URLSearchParams({
|
|
148
152
|
user_id: userId.toString(),
|
|
149
153
|
auth_key: authKey,
|
|
150
|
-
redirect_to:
|
|
154
|
+
redirect_to: relativeRedirectTo,
|
|
151
155
|
})
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
// generate link that will *consume* the auth key
|
|
158
|
+
if (globalConfig.isMA) {
|
|
159
|
+
if (!absoluteRedirectTo.hostname.endsWith('.musora.com')) {
|
|
160
|
+
throw new Error('Bad redirect URL - must be a musora.com domain')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return `${absoluteRedirectTo.origin}/auth?${params.toString()}`
|
|
164
|
+
} else {
|
|
165
|
+
throw new Error('Not implemented - MA deep links don\'t accept auth keys')
|
|
166
|
+
}
|
|
154
167
|
}
|
|
@@ -513,6 +513,7 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
|
|
|
513
513
|
title: content.title,
|
|
514
514
|
parent_id: content.parent_id || null,
|
|
515
515
|
navigateTo: content.navigateTo,
|
|
516
|
+
artist_name: content.artist_name || null,
|
|
516
517
|
}
|
|
517
518
|
})
|
|
518
519
|
return recentActivityData
|
|
@@ -792,12 +793,12 @@ async function formatPracticeMeta(practices = []) {
|
|
|
792
793
|
return {
|
|
793
794
|
id: practice.id,
|
|
794
795
|
auto: practice.auto,
|
|
795
|
-
thumbnail: practice.content_id ? content
|
|
796
|
-
thumbnail_url: practice.content_id ? content
|
|
796
|
+
thumbnail: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
|
|
797
|
+
thumbnail_url: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
|
|
797
798
|
duration: practice.duration_seconds || 0,
|
|
798
799
|
duration_seconds: practice.duration_seconds || 0,
|
|
799
800
|
content_url: content?.url || null,
|
|
800
|
-
title: practice.content_id ? content
|
|
801
|
+
title: practice.content_id ? content?.title : practice?.title || practice.content_id,
|
|
801
802
|
category_id: practice.category_id,
|
|
802
803
|
instrument_id: practice.instrument_id,
|
|
803
804
|
content_type: getFormattedType(content?.type || '', content?.brand || null),
|
|
@@ -808,6 +809,7 @@ async function formatPracticeMeta(practices = []) {
|
|
|
808
809
|
content_slug: content?.slug || null,
|
|
809
810
|
parent_id: content?.parent_id || null,
|
|
810
811
|
navigateTo: content?.navigateTo || null,
|
|
812
|
+
artist_name: content?.artist_name || null,
|
|
811
813
|
}
|
|
812
814
|
})
|
|
813
815
|
}
|
|
@@ -35,9 +35,10 @@ export function initializeSyncManager(userId) {
|
|
|
35
35
|
},
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const userScope = { initialId: userId, getCurrentId: () => userId }
|
|
39
|
+
SyncTelemetry.setInstance(new SyncTelemetry(userScope, { Sentry: dummySentry, level: SeverityLevel.WARNING, pretty: false }))
|
|
39
40
|
|
|
40
|
-
const adapter = syncAdapter(
|
|
41
|
+
const adapter = syncAdapter()
|
|
41
42
|
const db = syncDatabaseFactory(adapter)
|
|
42
43
|
|
|
43
44
|
const context = new SyncContext({
|
|
@@ -74,7 +75,7 @@ export function initializeSyncManager(userId) {
|
|
|
74
75
|
stop: () => {},
|
|
75
76
|
},
|
|
76
77
|
})
|
|
77
|
-
const manager = new SyncManager(context, db)
|
|
78
|
+
const manager = new SyncManager(userScope, context, db)
|
|
78
79
|
|
|
79
80
|
const initialStrategy = manager.createStrategy(InitialStrategy)
|
|
80
81
|
|