musora-content-services 2.151.1 → 2.153.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/.github/workflows/automated-testing.yml +20 -0
- package/CHANGELOG.md +24 -0
- package/jest.config.js +11 -2
- package/package.json +5 -1
- package/src/contentTypeConfig.js +13 -14
- package/src/infrastructure/http/interfaces/RequestOptions.ts +1 -1
- package/src/services/awards/internal/award-definitions.js +3 -3
- package/src/services/content-org/guided-courses.ts +1 -1
- package/src/services/contentProgress.js +1 -20
- package/src/services/dateUtils.js +9 -1
- package/src/services/forums/posts.ts +2 -2
- package/src/services/recommendations.js +17 -34
- package/src/services/reporting/reporting.ts +3 -4
- package/src/services/sanity.js +27 -59
- package/src/services/sync/adapters/lokijs.ts +5 -2
- package/src/services/sync/fetch.ts +2 -14
- package/src/services/sync/repositories/base.ts +4 -0
- package/src/services/sync/repositories/content-progress.ts +3 -3
- package/src/services/sync/store/index.ts +6 -1
- package/src/services/sync/strategies/base.ts +1 -1
- package/src/services/sync/telemetry/index.ts +1 -1
- package/src/services/urlBuilder.ts +1 -0
- package/src/services/user/streakCalculator.ts +1 -1
- package/test/SKIPPED_TESTS.md +151 -0
- package/test/initializeTests.js +2 -3
- package/test/{content.test.js → integration/content.test.js} +7 -23
- package/test/integration/contentProgress.test.js +73 -0
- package/test/{forum.test.js → integration/forum.test.js} +2 -4
- package/test/{sanityQueryService.test.js → integration/sanityQueryService.test.js} +143 -291
- package/test/{user → integration/user}/permissions.test.js +5 -4
- package/test/{learningPaths.test.js → live/learningPaths.test.js} +4 -4
- package/test/live/sanityQueryService.test.js +32 -0
- package/test/setupConsole.js +6 -0
- package/test/setupNetworkGuard.js +3 -0
- package/test/{HttpClient.test.js → unit/HttpClient.test.js} +5 -5
- package/test/{awards → unit/awards}/award-alacarte-observer.test.js +13 -12
- package/test/{awards → unit/awards}/award-auto-refresh.test.js +4 -3
- package/test/{awards → unit/awards}/award-calculations.test.js +3 -2
- package/test/{awards → unit/awards}/award-certificate-display.test.js +12 -11
- package/test/{awards → unit/awards}/award-collection-edge-cases.test.js +12 -11
- package/test/{awards → unit/awards}/award-collection-filtering.test.js +12 -11
- package/test/{awards → unit/awards}/award-completion-flow.test.js +15 -14
- package/test/{awards → unit/awards}/award-exclusion-handling.test.js +20 -19
- package/test/{awards → unit/awards}/award-multi-lesson.test.js +14 -13
- package/test/{awards → unit/awards}/award-observer-integration.test.js +14 -13
- package/test/{awards → unit/awards}/award-query-messages.test.js +30 -21
- package/test/{awards → unit/awards}/award-user-collection.test.js +11 -8
- package/test/{awards → unit/awards}/duplicate-prevention.test.js +12 -11
- package/test/unit/awards/helpers/index.js +3 -0
- package/test/{awards → unit/awards}/helpers/mock-setup.js +1 -1
- package/test/{awards → unit/awards}/helpers/progress-emitter.js +2 -2
- package/test/{awards → unit/awards}/message-generator.test.js +1 -1
- package/test/unit/contentLikes.test.js +62 -0
- package/test/unit/contentProgress.test.js +75 -0
- package/test/{dataContext.test.js → unit/dataContext.test.js} +2 -2
- package/test/unit/dateUtils.test.js +188 -0
- package/test/{imageSRCBuilder.test.js → unit/imageSRCBuilder.test.js} +2 -2
- package/test/{imageSRCVerify.test.js → unit/imageSRCVerify.test.js} +1 -1
- package/test/{lib → unit/lib}/filter.test.ts +10 -4
- package/test/{lib → unit/lib}/lastUpdated.test.js +6 -6
- package/test/{lib → unit/lib}/query.test.ts +1 -1
- package/test/{notifications.test.js → unit/notifications.test.js} +51 -39
- package/test/{progressRows.test.js → unit/progressRows.test.js} +53 -35
- package/test/unit/sanityQueryService.test.js +180 -0
- package/test/{streakMessage.test.js → unit/streakMessage.test.js} +18 -27
- package/test/unit/sync/adapters/idb-errors.test.ts +144 -0
- package/test/unit/sync/adapters/sqlite-errors.test.ts +173 -0
- package/test/unit/sync/helpers/TestModel.ts +44 -0
- package/test/unit/sync/helpers/index.ts +172 -0
- package/test/unit/sync/repositories/content-likes.test.ts +99 -0
- package/test/unit/sync/repositories/practices.test.ts +179 -0
- package/test/unit/sync/repositories/progress.test.ts +245 -0
- package/test/unit/sync/store/store-idb.test.ts +180 -0
- package/test/unit/sync/store/store.test.ts +274 -0
- package/test/unit/userActivity.test.js +99 -0
- package/tsconfig.json +15 -0
- package/test/awards/helpers/index.js +0 -3
- package/test/contentLikes.test.js +0 -95
- package/test/contentProgress.test.js +0 -279
- package/test/sync/adapter.ts +0 -9
- package/test/sync/initialize-sync-manager.js +0 -88
- package/test/sync/models/award-database-integration.test.js +0 -519
- package/test/userActivity.test.js +0 -118
- /package/test/{awards → unit/awards}/helpers/completion-mock.js +0 -0
- /package/test/{lib → unit/lib}/__snapshots__/filter.test.ts.snap +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Automated Testing
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
types: [opened, synchronize, reopened]
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
jobs:
|
|
8
|
+
unit-tests:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
timeout-minutes: 5
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: 20
|
|
16
|
+
cache: npm
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: npm ci
|
|
19
|
+
- name: Run unit tests
|
|
20
|
+
run: npm test
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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.153.0](https://github.com/railroadmedia/musora-content-services/compare/v2.152.1...v2.153.0) (2026-04-15)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 15 minutes ([#911](https://github.com/railroadmedia/musora-content-services/issues/911)) ([87594f8](https://github.com/railroadmedia/musora-content-services/commit/87594f8d33a90452df66d55e7c00d9b0eb15b30b))
|
|
11
|
+
* initial not bad melon test slop ([#918](https://github.com/railroadmedia/musora-content-services/issues/918)) ([008052b](https://github.com/railroadmedia/musora-content-services/commit/008052bbce724ae49fc12734e28957846be76ab3))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* loki adapter cjs/esm export issues in mcs-cli ([#920](https://github.com/railroadmedia/musora-content-services/issues/920)) ([480755e](https://github.com/railroadmedia/musora-content-services/commit/480755e816e28540cd123efd24ae7e299baecabe))
|
|
17
|
+
* remove flush session interval ([#867](https://github.com/railroadmedia/musora-content-services/issues/867)) ([67af03b](https://github.com/railroadmedia/musora-content-services/commit/67af03b5a0d8159d7754e86d94fa736ac47beab0))
|
|
18
|
+
* unref timer only in node ([92385d6](https://github.com/railroadmedia/musora-content-services/commit/92385d66659b76f1f41ebbfba3557aaea14f1abd))
|
|
19
|
+
|
|
20
|
+
### [2.152.1](https://github.com/railroadmedia/musora-content-services/compare/v2.152.0...v2.152.1) (2026-04-09)
|
|
21
|
+
|
|
22
|
+
## [2.152.0](https://github.com/railroadmedia/musora-content-services/compare/v2.151.1...v2.152.0) (2026-04-09)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* use sanity.musora.com cloudflare worker for caching ([#900](https://github.com/railroadmedia/musora-content-services/issues/900)) ([a6302f9](https://github.com/railroadmedia/musora-content-services/commit/a6302f96374fdea152f9fddd3ce8a8512e1c7b17))
|
|
28
|
+
|
|
5
29
|
### [2.151.1](https://github.com/railroadmedia/musora-content-services/compare/v2.151.0...v2.151.1) (2026-04-08)
|
|
6
30
|
|
|
7
31
|
|
package/jest.config.js
CHANGED
|
@@ -17,6 +17,8 @@ export default {
|
|
|
17
17
|
// Automatically clear mock calls, instances, contexts and results before every test
|
|
18
18
|
clearMocks: true,
|
|
19
19
|
|
|
20
|
+
testTimeout: 30000,
|
|
21
|
+
|
|
20
22
|
// Indicates whether the coverage information should be collected while executing the test
|
|
21
23
|
collectCoverage: true,
|
|
22
24
|
|
|
@@ -92,7 +94,10 @@ export default {
|
|
|
92
94
|
// moduleNameMapper: {},
|
|
93
95
|
|
|
94
96
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
95
|
-
modulePathIgnorePatterns: [
|
|
97
|
+
modulePathIgnorePatterns: [
|
|
98
|
+
'<rootDir>/test/live',
|
|
99
|
+
'<rootDir>/test/integration'
|
|
100
|
+
],
|
|
96
101
|
|
|
97
102
|
// Activates notifications for test results
|
|
98
103
|
// notify: false,
|
|
@@ -136,7 +141,11 @@ export default {
|
|
|
136
141
|
// setupFiles: [],
|
|
137
142
|
|
|
138
143
|
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
|
139
|
-
setupFilesAfterEnv: [
|
|
144
|
+
setupFilesAfterEnv: [
|
|
145
|
+
'dotenv/config',
|
|
146
|
+
'<rootDir>/test/setupConsole.js',
|
|
147
|
+
'<rootDir>/test/setupNetworkGuard.js'
|
|
148
|
+
],
|
|
140
149
|
|
|
141
150
|
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
|
142
151
|
// slowTestThreshold: 5,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musora-content-services",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.153.0",
|
|
4
4
|
"description": "A package for Musoras content services ",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
"@babel/preset-env": "^7.25.3",
|
|
27
27
|
"@babel/preset-typescript": "^7.27.1",
|
|
28
28
|
"@sentry/browser": "^10.21.0",
|
|
29
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
29
30
|
"@types/jest": "^30.0.0",
|
|
30
31
|
"babel-jest": "^29.7.0",
|
|
32
|
+
"better-sqlite3": "^12.9.0",
|
|
31
33
|
"dotenv": "^16.4.5",
|
|
34
|
+
"fake-indexeddb": "^6.2.5",
|
|
32
35
|
"jest": "^29.7.0",
|
|
36
|
+
"jest-environment-jsdom": "^30.3.0",
|
|
33
37
|
"jsdoc": "^4.0.3",
|
|
34
38
|
"jsdoc-babel": "^0.5.0",
|
|
35
39
|
"prettier": "3.4.2",
|
package/src/contentTypeConfig.js
CHANGED
|
@@ -8,6 +8,9 @@ export const CloudFrontURl = 'https://d3fzm1tzeyr5n3.cloudfront.net'
|
|
|
8
8
|
|
|
9
9
|
// This is used to pull related content by license, so we only show "consumable" content
|
|
10
10
|
export const SONG_TYPES = ['song', 'play-along', 'jam-track', 'song-tutorial-lesson']
|
|
11
|
+
|
|
12
|
+
export const parentReferenceField = 'parent_content_reference[0]'
|
|
13
|
+
export const grandParentReferenceField = 'parent_content_reference[1]'
|
|
11
14
|
// Oct 2025: It turns out content-meta categories are not really clear
|
|
12
15
|
// THis is used for the page_type field as a post processor so we include parents and children
|
|
13
16
|
// Duplicated in SanityGateway.php if you update this, update that
|
|
@@ -23,10 +26,6 @@ export const SINGLE_PARENT_TYPES = ['course-lesson', 'pack-bundle-lesson', 'song
|
|
|
23
26
|
|
|
24
27
|
export const LEARNING_PATH_LESSON = 'learning-path-lesson-v2'
|
|
25
28
|
|
|
26
|
-
export const parentField = 'parent_content_data[0]'
|
|
27
|
-
|
|
28
|
-
export const grandParentField = 'parent_content_data[1]'
|
|
29
|
-
|
|
30
29
|
export const genreField = `genre[]->{
|
|
31
30
|
name,
|
|
32
31
|
'slug': slug.current,
|
|
@@ -45,7 +44,7 @@ export const instructorField = `instructor[]->{
|
|
|
45
44
|
|
|
46
45
|
export const artistField = `select(
|
|
47
46
|
defined(artist) => artist->{ 'name': name, 'slug': slug.current, 'thumbnail': thumbnail_url.asset->url},
|
|
48
|
-
defined(
|
|
47
|
+
defined(parent_content_reference) => ${parentReferenceField}->artist->{ 'name': name, 'slug': slug.current, 'thumbnail': thumbnail_url.asset->url}
|
|
49
48
|
)`
|
|
50
49
|
|
|
51
50
|
export const DEFAULT_FIELDS = [
|
|
@@ -68,8 +67,8 @@ export const DEFAULT_FIELDS = [
|
|
|
68
67
|
"'slug' : slug.current",
|
|
69
68
|
"'permission_id': permission_v2",
|
|
70
69
|
'child_count',
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
`"parent_id": ${parentReferenceField}->railcontent_id`,
|
|
71
|
+
`"grandparent_id": ${grandParentReferenceField}->railcontent_id`,
|
|
73
72
|
'live_event_start_time',
|
|
74
73
|
'live_event_end_time',
|
|
75
74
|
'enrollment_start_time',
|
|
@@ -96,8 +95,8 @@ export const DEFAULT_CHILD_FIELDS = [
|
|
|
96
95
|
"'slug' : slug.current",
|
|
97
96
|
"'permission_id': permission_v2",
|
|
98
97
|
'child_count',
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
`"parent_id": ${parentReferenceField}->railcontent_id`,
|
|
99
|
+
`"grandparent_id": ${grandParentReferenceField}->railcontent_id`,
|
|
101
100
|
]
|
|
102
101
|
|
|
103
102
|
export const playAlongMp3sField = `{
|
|
@@ -118,7 +117,7 @@ export const descriptionField = 'description[0].children[0].text'
|
|
|
118
117
|
// this pulls both any defined resources for the document as well as any resources in the parent document
|
|
119
118
|
export const resourcesField = `[
|
|
120
119
|
... resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )},
|
|
121
|
-
...
|
|
120
|
+
... coalesce(parent_content_reference[]->resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )}, []),
|
|
122
121
|
]`
|
|
123
122
|
|
|
124
123
|
export const contentAwardField = "*[references(^._id) && _type == 'content-award'][0]"
|
|
@@ -405,7 +404,7 @@ export let contentTypeConfig = {
|
|
|
405
404
|
},
|
|
406
405
|
'progress-tracker': {
|
|
407
406
|
fields: [
|
|
408
|
-
'"parent_content_data":
|
|
407
|
+
'"parent_content_data": parent_content_reference[]->railcontent_id',
|
|
409
408
|
`"badge" : ${contentAwardField}.badge.asset->url`,
|
|
410
409
|
`"badge_rear" : ${contentAwardField}.badge_rear.asset->url`,
|
|
411
410
|
`"badge_logo" : ${contentAwardField}.logo.asset->url`,
|
|
@@ -528,9 +527,9 @@ export let contentTypeConfig = {
|
|
|
528
527
|
],
|
|
529
528
|
includeChildFields: true,
|
|
530
529
|
childFields: [
|
|
531
|
-
`"parent_data":
|
|
532
|
-
"id":
|
|
533
|
-
|
|
530
|
+
`"parent_data": ${parentReferenceField}->{
|
|
531
|
+
"id": railcontent_id,
|
|
532
|
+
title,
|
|
534
533
|
}`,
|
|
535
534
|
],
|
|
536
535
|
},
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
/** @typedef {Map} AwardDefinitionsMap */
|
|
7
7
|
/** @typedef {Map} ContentToAwardsMap */
|
|
8
8
|
|
|
9
|
+
import { globalConfig } from '../../config'
|
|
10
|
+
|
|
9
11
|
const STORAGE_KEY = 'musora_award_definitions_last_fetch'
|
|
10
12
|
|
|
11
13
|
class AwardDefinitionsService {
|
|
@@ -173,7 +175,6 @@ class AwardDefinitionsService {
|
|
|
173
175
|
|
|
174
176
|
async loadLastFetchFromStorage() {
|
|
175
177
|
try {
|
|
176
|
-
const { globalConfig } = await import('../../config')
|
|
177
178
|
if (!globalConfig.localStorage) {
|
|
178
179
|
return
|
|
179
180
|
}
|
|
@@ -194,8 +195,7 @@ class AwardDefinitionsService {
|
|
|
194
195
|
|
|
195
196
|
async saveLastFetchToStorage() {
|
|
196
197
|
try {
|
|
197
|
-
|
|
198
|
-
if (!globalConfig.localStorage) {
|
|
198
|
+
if (!globalConfig?.localStorage) {
|
|
199
199
|
return
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module GuidedCourses
|
|
3
3
|
*/
|
|
4
|
-
import { GET, POST } from '../../infrastructure/http/HttpClient
|
|
4
|
+
import { GET, POST } from '../../infrastructure/http/HttpClient'
|
|
5
5
|
import { contentStatusStarted, getProgressState } from '../contentProgress.js'
|
|
6
6
|
import './playlists-types.js'
|
|
7
7
|
|
|
@@ -416,7 +416,6 @@ export async function recordWatchSession(
|
|
|
416
416
|
mediaLengthSeconds,
|
|
417
417
|
currentSeconds,
|
|
418
418
|
secondsPlayed,
|
|
419
|
-
prevSession = null,
|
|
420
419
|
instrumentId = null,
|
|
421
420
|
categoryId = null,
|
|
422
421
|
isLivestream = false,
|
|
@@ -424,32 +423,14 @@ export async function recordWatchSession(
|
|
|
424
423
|
contentId = normalizeContentId(contentId)
|
|
425
424
|
collection = normalizeCollection(collection)
|
|
426
425
|
|
|
427
|
-
if (!prevSession) {
|
|
428
|
-
prevSession = {
|
|
429
|
-
pushInterval: null
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
426
|
// Track practice and progress locally (no immediate push)
|
|
434
427
|
await Promise.all([
|
|
435
428
|
trackPractice(contentId, secondsPlayed, { instrumentId, categoryId }),
|
|
436
429
|
trackProgress(contentId, collection, currentSeconds, mediaLengthSeconds, isLivestream),
|
|
437
430
|
])
|
|
438
|
-
|
|
439
|
-
if (!prevSession.pushInterval) {
|
|
440
|
-
prevSession.pushInterval = setInterval(() => {
|
|
441
|
-
flushWatchSession()
|
|
442
|
-
}, PUSH_INTERVAL)
|
|
443
|
-
}
|
|
444
|
-
return prevSession
|
|
445
431
|
}
|
|
446
432
|
|
|
447
|
-
export async function flushWatchSession(
|
|
448
|
-
if (shouldClearInterval && sessionToFlush?.pushInterval) {
|
|
449
|
-
clearInterval(sessionToFlush.pushInterval)
|
|
450
|
-
sessionToFlush.pushInterval = null
|
|
451
|
-
}
|
|
452
|
-
|
|
433
|
+
export async function flushWatchSession() {
|
|
453
434
|
db.contentProgress.requestPushUnsynced('flush-watch-session')
|
|
454
435
|
db.practices.requestPushUnsynced('flush-watch-session')
|
|
455
436
|
}
|
|
@@ -31,9 +31,17 @@ export function getMonday(date, timeZone = Intl.DateTimeFormat().resolvedOptions
|
|
|
31
31
|
export function getWeekNumber(date) {
|
|
32
32
|
return dayjs(date).isoWeek()
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// Decides if we need to convert or interpret the date based on whether it has timezone info.
|
|
36
|
+
export function toLocalDay(date, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
|
|
37
|
+
const hasTimezoneInfo = /Z$|[+-]\d{2}:?\d{2}$/.test(String(date))
|
|
38
|
+
// Has timezone info ? convert : interpret
|
|
39
|
+
return hasTimezoneInfo ? dayjs(date).tz(timeZone) : dayjs.tz(date, timeZone)
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
//Check if two dates are the same
|
|
35
43
|
export function isSameDate(date1, date2) {
|
|
36
|
-
return
|
|
44
|
+
return toLocalDay(date1).isSame(toLocalDay(date2), 'day')
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
// Check if two dates are consecutive days
|
|
@@ -23,7 +23,7 @@ export interface CreatePostParams {
|
|
|
23
23
|
* @throws {HttpError} - If the request fails.
|
|
24
24
|
*/
|
|
25
25
|
export async function createPost(threadId: number, params: CreatePostParams): Promise<ForumPost> {
|
|
26
|
-
const { generateForumPostUrl } = await import('../urlBuilder
|
|
26
|
+
const { generateForumPostUrl } = await import('../urlBuilder')
|
|
27
27
|
|
|
28
28
|
// Generate forum post URL
|
|
29
29
|
const contentUrl = generateForumPostUrl({
|
|
@@ -114,7 +114,7 @@ export async function fetchPosts(
|
|
|
114
114
|
* @throws {HttpError} - If the request fails.
|
|
115
115
|
*/
|
|
116
116
|
export async function likePost(postId: number, brand: string): Promise<void> {
|
|
117
|
-
const { generateForumPostUrl } = await import('../urlBuilder
|
|
117
|
+
const { generateForumPostUrl } = await import('../urlBuilder')
|
|
118
118
|
|
|
119
119
|
// Generate forum post URL
|
|
120
120
|
const contentUrl = generateForumPostUrl({
|
|
@@ -32,16 +32,6 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
|
|
|
32
32
|
if (!content_id) {
|
|
33
33
|
return []
|
|
34
34
|
}
|
|
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
35
|
content_id = parseInt(content_id)
|
|
46
36
|
const data = {
|
|
47
37
|
brand: brand,
|
|
@@ -56,8 +46,6 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
|
|
|
56
46
|
console.error('Fetch error:', error)
|
|
57
47
|
return null
|
|
58
48
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
49
|
}
|
|
62
50
|
|
|
63
51
|
/**
|
|
@@ -79,27 +67,25 @@ export async function rankCategories(brand, categories) {
|
|
|
79
67
|
if (categories.length === 0) {
|
|
80
68
|
return []
|
|
81
69
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const rankedCategories = []
|
|
70
|
+
const data = {
|
|
71
|
+
brand: brand,
|
|
72
|
+
user_id: globalConfig.sessionConfig.userId,
|
|
73
|
+
playlists: categories,
|
|
74
|
+
}
|
|
75
|
+
const url = `/rank_each_list/`
|
|
76
|
+
try {
|
|
77
|
+
const response = await recommenderClient.post(url, data)
|
|
78
|
+
const rankedCategories = []
|
|
92
79
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return rankedCategories
|
|
100
|
-
} catch (error) {
|
|
101
|
-
console.error('RankCategories fetch error:', error)
|
|
80
|
+
for (const rankedPlaylist of response['ranked_playlists']) {
|
|
81
|
+
rankedCategories.push({
|
|
82
|
+
slug: rankedPlaylist.playlist_id,
|
|
83
|
+
items: rankedPlaylist.ranked_items,
|
|
84
|
+
})
|
|
102
85
|
}
|
|
86
|
+
return rankedCategories
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('RankCategories fetch error:', error)
|
|
103
89
|
}
|
|
104
90
|
|
|
105
91
|
const defaultSorting = []
|
|
@@ -127,9 +113,6 @@ export async function rankItems(brand, content_ids) {
|
|
|
127
113
|
if (content_ids.length === 0) {
|
|
128
114
|
return []
|
|
129
115
|
}
|
|
130
|
-
if (brand === 'playbass') {
|
|
131
|
-
return content_ids
|
|
132
|
-
}
|
|
133
116
|
const data = {
|
|
134
117
|
brand: brand,
|
|
135
118
|
user_id: globalConfig.sessionConfig.userId,
|
|
@@ -11,9 +11,8 @@ import { HttpClient } from '../../infrastructure/http/HttpClient'
|
|
|
11
11
|
import { globalConfig } from '../config.js'
|
|
12
12
|
import { ReportResponse, ReportableType, IssueTypeMap, ReportIssueOption } from './types'
|
|
13
13
|
import { Brands } from '../../lib/brands'
|
|
14
|
-
import { generateContentUrl, generatePlaylistUrl, generateForumPostUrl, generateCommentUrl } from '../urlBuilder
|
|
15
|
-
import {fetchByRailContentId} from "
|
|
16
|
-
import {fetchByRailContentIds} from "../sanity";
|
|
14
|
+
import { generateContentUrl, generatePlaylistUrl, generateForumPostUrl, generateCommentUrl } from '../urlBuilder'
|
|
15
|
+
import {fetchByRailContentId, fetchByRailContentIds} from "../sanity";
|
|
17
16
|
import {addContextToContent} from "../contentAggregator";
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -122,7 +121,7 @@ export async function report<T extends ReportableType>(
|
|
|
122
121
|
id: params.id
|
|
123
122
|
})
|
|
124
123
|
} else if (params.type === 'forum_post') {
|
|
125
|
-
const { fetchPost } = await import('../forums/posts
|
|
124
|
+
const { fetchPost } = await import('../forums/posts')
|
|
126
125
|
const post = await fetchPost(params.id, params.brand)
|
|
127
126
|
|
|
128
127
|
if (post?.thread) {
|
package/src/services/sanity.js
CHANGED
|
@@ -33,8 +33,9 @@ import {
|
|
|
33
33
|
SONG_TYPES_WITH_CHILDREN,
|
|
34
34
|
liveFields,
|
|
35
35
|
postProcessBadge,
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
parentRecentTypes,
|
|
37
|
+
parentReferenceField,
|
|
38
|
+
grandParentReferenceField,
|
|
38
39
|
} from '../contentTypeConfig.js'
|
|
39
40
|
import { fetchSimilarItems } from './recommendations.js'
|
|
40
41
|
import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS, CONTENT_STATUSES } from '../contentMetaData.js'
|
|
@@ -952,7 +953,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
952
953
|
}
|
|
953
954
|
|
|
954
955
|
const parentQuery = addParent
|
|
955
|
-
? `"parent_content_data":
|
|
956
|
+
? `"parent_content_data": parent_content_reference[]->{
|
|
956
957
|
"id": railcontent_id,
|
|
957
958
|
title,
|
|
958
959
|
slug,
|
|
@@ -960,11 +961,12 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
960
961
|
"logo" : logo_image_url.asset->url,
|
|
961
962
|
"dark_mode_logo": dark_mode_logo_url.asset->url,
|
|
962
963
|
"light_mode_logo": light_mode_logo_url.asset->url,
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
964
|
+
...*[references(^._id) && _type == 'content-award'][0]{
|
|
965
|
+
"badge": badge.asset->url,
|
|
966
|
+
"badge_rear": badge_rear.asset->url,
|
|
967
|
+
"badge_logo": logo.asset->url,
|
|
968
|
+
}
|
|
969
|
+
},`
|
|
968
970
|
: ''
|
|
969
971
|
|
|
970
972
|
const fields = `${getFieldsForContentType()}
|
|
@@ -1106,19 +1108,6 @@ async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, cou
|
|
|
1106
1108
|
* @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
|
|
1107
1109
|
*/
|
|
1108
1110
|
export async function fetchSiblingContent(railContentId, brand = null) {
|
|
1109
|
-
const filterGetParent = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
|
|
1110
|
-
pullFutureContent: true,
|
|
1111
|
-
showMembershipRestrictedContent: true, // Show parent even without permissions
|
|
1112
|
-
}).buildFilter()
|
|
1113
|
-
const filterForParentList = await new FilterBuilder(
|
|
1114
|
-
`references(^._id) && _type == ^.parent_type`,
|
|
1115
|
-
{
|
|
1116
|
-
pullFutureContent: true,
|
|
1117
|
-
isParentFilter: true,
|
|
1118
|
-
showMembershipRestrictedContent: true, // Show parent even without permissions
|
|
1119
|
-
}
|
|
1120
|
-
).buildFilter()
|
|
1121
|
-
|
|
1122
1111
|
const childrenFilter = await new FilterBuilder(``, {
|
|
1123
1112
|
isChildrenFilter: true,
|
|
1124
1113
|
showMembershipRestrictedContent: true, // Show all lessons in sidebar, need_access applied on individual page
|
|
@@ -1126,20 +1115,20 @@ export async function fetchSiblingContent(railContentId, brand = null) {
|
|
|
1126
1115
|
|
|
1127
1116
|
const brandString = brand ? ` && brand == "${brand}"` : ''
|
|
1128
1117
|
const queryFields = getFieldsForContentType()
|
|
1129
|
-
|
|
1118
|
+
const courseCollectionFields = await getFieldsForContentTypeWithFilteredChildren('course-collection')
|
|
1130
1119
|
const query = `*[railcontent_id == ${railContentId}${brandString}]{
|
|
1131
1120
|
_type,
|
|
1132
1121
|
parent_type,
|
|
1133
1122
|
railcontent_id,
|
|
1134
|
-
'parent_id': ${
|
|
1135
|
-
'grandparent_id'
|
|
1136
|
-
'
|
|
1137
|
-
'
|
|
1138
|
-
|
|
1123
|
+
'parent_id': ${parentReferenceField}->railcontent_id,
|
|
1124
|
+
'grandparent_id': ${grandParentReferenceField}->railcontent_id,
|
|
1125
|
+
'collection_data': ${grandParentReferenceField}->{${courseCollectionFields}},
|
|
1126
|
+
'for-calculations': ${parentReferenceField}->{
|
|
1127
|
+
'siblings-list': child[]->railcontent_id,
|
|
1128
|
+
'parents-list': ${parentReferenceField}->child[]->railcontent_id
|
|
1139
1129
|
},
|
|
1140
|
-
"related_lessons" :
|
|
1130
|
+
"related_lessons" : ${parentReferenceField}->child[${childrenFilter}]->{${queryFields}}
|
|
1141
1131
|
}`
|
|
1142
|
-
|
|
1143
1132
|
let result = await fetchSanity(query, false, { processNeedAccess: true })
|
|
1144
1133
|
|
|
1145
1134
|
//there's no way in sanity to retrieve the index of an array, so we must calculate after fetch
|
|
@@ -1152,10 +1141,6 @@ export async function fetchSiblingContent(railContentId, brand = null) {
|
|
|
1152
1141
|
|
|
1153
1142
|
delete result['for-calculations']
|
|
1154
1143
|
|
|
1155
|
-
if (result['grandparent_id']) {
|
|
1156
|
-
result['collection_data'] = await fetchCourseCollectionData(result['grandparent_id'])
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
1144
|
result = { ...result, parentCount, currentParentIndex, siblingCount, currentSiblingIndex }
|
|
1160
1145
|
return result
|
|
1161
1146
|
} else {
|
|
@@ -1199,7 +1184,7 @@ export async function fetchRelatedLessons(railContentId) {
|
|
|
1199
1184
|
}
|
|
1200
1185
|
|
|
1201
1186
|
export async function fetchLiveEvent(brand, forcedContentId = null) {
|
|
1202
|
-
const LIVE_EXTRA_MINUTES =
|
|
1187
|
+
const LIVE_EXTRA_MINUTES = 15
|
|
1203
1188
|
//calendarIDs taken from addevent.php
|
|
1204
1189
|
// TODO import instructor calendars to Sanity
|
|
1205
1190
|
let defaultCalendarID = ''
|
|
@@ -1320,16 +1305,12 @@ export async function fetchByReference(
|
|
|
1320
1305
|
* @returns {Promise<int|null>}
|
|
1321
1306
|
*/
|
|
1322
1307
|
export async function fetchTopLevelParentId(railcontentId) {
|
|
1323
|
-
const parentFilter = 'railcontent_id in [...(^.parent_content_data[].id)] && (!defined(parent_content_data) || count(parent_content_data) == 0)'
|
|
1324
|
-
const statusFilter = "&& status in ['scheduled', 'published', 'archived', 'unlisted']"
|
|
1325
|
-
|
|
1326
1308
|
const query = `*[railcontent_id == ${railcontentId}]{
|
|
1327
|
-
railcontent_id,
|
|
1328
|
-
'top_parent': *[${parentFilter} ${statusFilter}][0].railcontent_id
|
|
1309
|
+
'top_parent': coalesce(${grandParentReferenceField}->railcontent_id, ${parentReferenceField}->railcontent_id, railcontent_id),
|
|
1329
1310
|
}`
|
|
1330
1311
|
let response = await fetchSanity(query, false, { processNeedAccess: false })
|
|
1331
1312
|
if (!response) return null
|
|
1332
|
-
return response['top_parent'] ??
|
|
1313
|
+
return response['top_parent'] ?? railcontentId
|
|
1333
1314
|
}
|
|
1334
1315
|
|
|
1335
1316
|
export async function getHierarchy(contentId, collection) {
|
|
@@ -1403,31 +1384,18 @@ async function fetchALaCarteHierarchyData(railcontentId) {
|
|
|
1403
1384
|
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
1404
1385
|
const query = `*[railcontent_id == ${topLevelId}]{
|
|
1405
1386
|
railcontent_id,
|
|
1406
|
-
'metadata': { brand, 'type': _type, 'parent_id': coalesce(
|
|
1387
|
+
'metadata': { brand, 'type': _type, 'parent_id': coalesce(${parentReferenceField}->railcontent_id, 0) },
|
|
1407
1388
|
'assignments': assignment[]{railcontent_id},
|
|
1408
1389
|
'children': child[${childrenFilter}]->{
|
|
1409
1390
|
railcontent_id,
|
|
1410
|
-
'metadata': {
|
|
1411
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1391
|
+
'metadata': { brand, 'type': _type, 'parent_id': coalesce(${parentReferenceField}->railcontent_id, 0) },
|
|
1412
1392
|
'assignments': assignment[]{railcontent_id},
|
|
1413
1393
|
'children': child[${childrenFilter}]->{
|
|
1414
1394
|
railcontent_id,
|
|
1415
|
-
'metadata': {
|
|
1416
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1395
|
+
'metadata': { brand, 'type': _type, 'parent_id': coalesce(${parentReferenceField}->railcontent_id, 0) },
|
|
1417
1396
|
'assignments': assignment[]{railcontent_id},
|
|
1418
|
-
'children': child[${childrenFilter}]->{
|
|
1419
|
-
railcontent_id,
|
|
1420
|
-
'metadata': {
|
|
1421
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1422
|
-
'assignments': assignment[]{railcontent_id},
|
|
1423
|
-
'children': child[${childrenFilter}]->{
|
|
1424
|
-
railcontent_id,
|
|
1425
|
-
'metadata': {
|
|
1426
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
1397
|
}
|
|
1430
|
-
}
|
|
1398
|
+
}
|
|
1431
1399
|
}`
|
|
1432
1400
|
return await fetchSanity(query, false, { processNeedAccess: false })
|
|
1433
1401
|
}
|
|
@@ -1517,7 +1485,7 @@ export async function fetchSanity(
|
|
|
1517
1485
|
}
|
|
1518
1486
|
const perspective = globalConfig.sanityConfig.perspective ?? 'published'
|
|
1519
1487
|
const api = globalConfig.sanityConfig.useCachedAPI ? 'apicdn' : 'api'
|
|
1520
|
-
const baseUrl = `https
|
|
1488
|
+
const baseUrl = `https://sanity.musora.com/${globalConfig.sanityConfig.projectId}/${api}/v${globalConfig.sanityConfig.version}/${globalConfig.sanityConfig.dataset}?perspective=${perspective}`
|
|
1521
1489
|
|
|
1522
1490
|
try {
|
|
1523
1491
|
const encodedQuery = encodeURIComponent(query)
|
|
@@ -2000,7 +1968,7 @@ export async function fetchTabData(
|
|
|
2000
1968
|
? `&& !(railcontent_id in [${excludeIds.join(',')}])`
|
|
2001
1969
|
: ''
|
|
2002
1970
|
|
|
2003
|
-
const excludeCoursesInCourseCollectionsFilter = `&& !(_type == 'course' && defined(
|
|
1971
|
+
const excludeCoursesInCourseCollectionsFilter = `&& !(_type == 'course' && defined(parent_content_reference) && count(parent_content_reference[]) > 0)`
|
|
2004
1972
|
|
|
2005
1973
|
filter = `brand == "${brand}" && (defined(railcontent_id)) ${includedFieldsFilter} ${progressFilter} ${excludedIdsFilter} ${excludeCoursesInCourseCollectionsFilter}`
|
|
2006
1974
|
const childrenFilter = await new FilterBuilder(``, {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { SyncTelemetry } from '../telemetry'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import _LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
|
|
4
4
|
|
|
5
5
|
import { deleteDatabase, lokiFatalError } from '@nozbe/watermelondb/adapters/lokijs/worker/lokiExtensions'
|
|
6
6
|
|
|
7
|
+
// Handle CJS/ESM interop: in Node.js ESM the default import is the exports object
|
|
8
|
+
const LokiJSAdapter = (_LokiJSAdapter as any).default ?? _LokiJSAdapter
|
|
9
|
+
|
|
7
10
|
export type LokiExtensions = {
|
|
8
11
|
onPersistenceError?: (err: Error) => void
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
export default class LokiPersistenceErrorAwareAdapter extends LokiJSAdapter {
|
|
14
|
+
export default class LokiPersistenceErrorAwareAdapter extends (LokiJSAdapter as typeof _LokiJSAdapter) {
|
|
12
15
|
constructor(options: any, extensions: LokiExtensions = {}) {
|
|
13
16
|
super(options);
|
|
14
17
|
const that = this;
|
|
@@ -122,7 +122,7 @@ export interface SyncResponseBase {
|
|
|
122
122
|
|
|
123
123
|
export type PushPayload = {
|
|
124
124
|
entries: ({
|
|
125
|
-
record:
|
|
125
|
+
record: Record<string, unknown>
|
|
126
126
|
meta: {
|
|
127
127
|
ids: {
|
|
128
128
|
id: string
|
|
@@ -135,23 +135,11 @@ export type PushPayload = {
|
|
|
135
135
|
ids: {
|
|
136
136
|
id: string
|
|
137
137
|
}
|
|
138
|
-
deleted_at:
|
|
138
|
+
deleted_at: number
|
|
139
139
|
}
|
|
140
140
|
})[]
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
interface ServerPushPayload {
|
|
144
|
-
entries: {
|
|
145
|
-
record: BaseModel | null
|
|
146
|
-
meta: {
|
|
147
|
-
ids: {
|
|
148
|
-
id: string
|
|
149
|
-
},
|
|
150
|
-
deleted_at: EpochMs | null
|
|
151
|
-
}
|
|
152
|
-
}[]
|
|
153
|
-
}
|
|
154
|
-
|
|
155
143
|
export function makeFetchRequest(input: RequestInfo, init?: RequestInit) {
|
|
156
144
|
return (userId: number, context: SyncContext) => new Request(globalConfig.baseUrl + input, {
|
|
157
145
|
...init,
|