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.
Files changed (85) hide show
  1. package/.github/workflows/automated-testing.yml +20 -0
  2. package/CHANGELOG.md +24 -0
  3. package/jest.config.js +11 -2
  4. package/package.json +5 -1
  5. package/src/contentTypeConfig.js +13 -14
  6. package/src/infrastructure/http/interfaces/RequestOptions.ts +1 -1
  7. package/src/services/awards/internal/award-definitions.js +3 -3
  8. package/src/services/content-org/guided-courses.ts +1 -1
  9. package/src/services/contentProgress.js +1 -20
  10. package/src/services/dateUtils.js +9 -1
  11. package/src/services/forums/posts.ts +2 -2
  12. package/src/services/recommendations.js +17 -34
  13. package/src/services/reporting/reporting.ts +3 -4
  14. package/src/services/sanity.js +27 -59
  15. package/src/services/sync/adapters/lokijs.ts +5 -2
  16. package/src/services/sync/fetch.ts +2 -14
  17. package/src/services/sync/repositories/base.ts +4 -0
  18. package/src/services/sync/repositories/content-progress.ts +3 -3
  19. package/src/services/sync/store/index.ts +6 -1
  20. package/src/services/sync/strategies/base.ts +1 -1
  21. package/src/services/sync/telemetry/index.ts +1 -1
  22. package/src/services/urlBuilder.ts +1 -0
  23. package/src/services/user/streakCalculator.ts +1 -1
  24. package/test/SKIPPED_TESTS.md +151 -0
  25. package/test/initializeTests.js +2 -3
  26. package/test/{content.test.js → integration/content.test.js} +7 -23
  27. package/test/integration/contentProgress.test.js +73 -0
  28. package/test/{forum.test.js → integration/forum.test.js} +2 -4
  29. package/test/{sanityQueryService.test.js → integration/sanityQueryService.test.js} +143 -291
  30. package/test/{user → integration/user}/permissions.test.js +5 -4
  31. package/test/{learningPaths.test.js → live/learningPaths.test.js} +4 -4
  32. package/test/live/sanityQueryService.test.js +32 -0
  33. package/test/setupConsole.js +6 -0
  34. package/test/setupNetworkGuard.js +3 -0
  35. package/test/{HttpClient.test.js → unit/HttpClient.test.js} +5 -5
  36. package/test/{awards → unit/awards}/award-alacarte-observer.test.js +13 -12
  37. package/test/{awards → unit/awards}/award-auto-refresh.test.js +4 -3
  38. package/test/{awards → unit/awards}/award-calculations.test.js +3 -2
  39. package/test/{awards → unit/awards}/award-certificate-display.test.js +12 -11
  40. package/test/{awards → unit/awards}/award-collection-edge-cases.test.js +12 -11
  41. package/test/{awards → unit/awards}/award-collection-filtering.test.js +12 -11
  42. package/test/{awards → unit/awards}/award-completion-flow.test.js +15 -14
  43. package/test/{awards → unit/awards}/award-exclusion-handling.test.js +20 -19
  44. package/test/{awards → unit/awards}/award-multi-lesson.test.js +14 -13
  45. package/test/{awards → unit/awards}/award-observer-integration.test.js +14 -13
  46. package/test/{awards → unit/awards}/award-query-messages.test.js +30 -21
  47. package/test/{awards → unit/awards}/award-user-collection.test.js +11 -8
  48. package/test/{awards → unit/awards}/duplicate-prevention.test.js +12 -11
  49. package/test/unit/awards/helpers/index.js +3 -0
  50. package/test/{awards → unit/awards}/helpers/mock-setup.js +1 -1
  51. package/test/{awards → unit/awards}/helpers/progress-emitter.js +2 -2
  52. package/test/{awards → unit/awards}/message-generator.test.js +1 -1
  53. package/test/unit/contentLikes.test.js +62 -0
  54. package/test/unit/contentProgress.test.js +75 -0
  55. package/test/{dataContext.test.js → unit/dataContext.test.js} +2 -2
  56. package/test/unit/dateUtils.test.js +188 -0
  57. package/test/{imageSRCBuilder.test.js → unit/imageSRCBuilder.test.js} +2 -2
  58. package/test/{imageSRCVerify.test.js → unit/imageSRCVerify.test.js} +1 -1
  59. package/test/{lib → unit/lib}/filter.test.ts +10 -4
  60. package/test/{lib → unit/lib}/lastUpdated.test.js +6 -6
  61. package/test/{lib → unit/lib}/query.test.ts +1 -1
  62. package/test/{notifications.test.js → unit/notifications.test.js} +51 -39
  63. package/test/{progressRows.test.js → unit/progressRows.test.js} +53 -35
  64. package/test/unit/sanityQueryService.test.js +180 -0
  65. package/test/{streakMessage.test.js → unit/streakMessage.test.js} +18 -27
  66. package/test/unit/sync/adapters/idb-errors.test.ts +144 -0
  67. package/test/unit/sync/adapters/sqlite-errors.test.ts +173 -0
  68. package/test/unit/sync/helpers/TestModel.ts +44 -0
  69. package/test/unit/sync/helpers/index.ts +172 -0
  70. package/test/unit/sync/repositories/content-likes.test.ts +99 -0
  71. package/test/unit/sync/repositories/practices.test.ts +179 -0
  72. package/test/unit/sync/repositories/progress.test.ts +245 -0
  73. package/test/unit/sync/store/store-idb.test.ts +180 -0
  74. package/test/unit/sync/store/store.test.ts +274 -0
  75. package/test/unit/userActivity.test.js +99 -0
  76. package/tsconfig.json +15 -0
  77. package/test/awards/helpers/index.js +0 -3
  78. package/test/contentLikes.test.js +0 -95
  79. package/test/contentProgress.test.js +0 -279
  80. package/test/sync/adapter.ts +0 -9
  81. package/test/sync/initialize-sync-manager.js +0 -88
  82. package/test/sync/models/award-database-integration.test.js +0 -519
  83. package/test/userActivity.test.js +0 -118
  84. /package/test/{awards → unit/awards}/helpers/completion-mock.js +0 -0
  85. /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: ['<rootDir>/test/live'],
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: ['dotenv/config'],
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.151.1",
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",
@@ -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(parent_content_data) => *[_type == ^.parent_content_data[0].type && railcontent_id == ^.parent_content_data[0].id][0].artist->{ 'name': name, 'slug': slug.current, 'thumbnail': thumbnail_url.asset->url}
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
- '"parent_id": parent_content_data[0].id',
72
- '"grandparent_id": parent_content_data[1].id',
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
- '"parent_id": parent_content_data[0].id',
100
- '"grandparent_id": parent_content_data[1].id',
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
- ... *[defined(resource) && railcontent_id in [...(^.parent_content_data[].id)]].resource[]{resource_name, _key, "resource_url": coalesce('${CloudFrontURl}'+string::split(resource_aws.asset->fileURL, '${AWSUrl}')[1], resource_url )},
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": parent_content_data[].id',
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": parent_content_data[0] {
532
- "id": id,
533
- "title": *[railcontent_id == ^.id][0].title,
530
+ `"parent_data": ${parentReferenceField}->{
531
+ "id": railcontent_id,
532
+ title,
534
533
  }`,
535
534
  ],
536
535
  },
@@ -2,6 +2,6 @@ export interface RequestOptions {
2
2
  method: string
3
3
  headers: Record<string, string>
4
4
  credentials?: "omit" | "same-origin" | "include"
5
- body?: string
5
+ body?: string | FormData
6
6
  cache?: RequestCache
7
7
  }
@@ -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
- const { globalConfig } = await import('../../config')
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.ts'
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(sessionToFlush = null, shouldClearInterval = true) {
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 dayjs(date1).isSame(dayjs(date2), 'day')
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.ts')
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.ts')
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
- if (brand !== 'playbass') {
83
- const data = {
84
- brand: brand,
85
- user_id: globalConfig.sessionConfig.userId,
86
- playlists: categories,
87
- }
88
- const url = `/rank_each_list/`
89
- try {
90
- const response = await recommenderClient.post(url, data)
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
- for (const rankedPlaylist of response['ranked_playlists']) {
94
- rankedCategories.push({
95
- slug: rankedPlaylist.playlist_id,
96
- items: rankedPlaylist.ranked_items,
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.ts'
15
- import {fetchByRailContentId} from "../../index";
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.ts')
124
+ const { fetchPost } = await import('../forums/posts')
126
125
  const post = await fetchPost(params.id, params.brand)
127
126
 
128
127
  if (post?.thread) {
@@ -33,8 +33,9 @@ import {
33
33
  SONG_TYPES_WITH_CHILDREN,
34
34
  liveFields,
35
35
  postProcessBadge,
36
- parentField,
37
- grandParentField, parentRecentTypes,
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": *[railcontent_id in [...(^.parent_content_data[].id)]]{
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
- "badge": *[references(^._id) && _type == 'content-award'][0].badge.asset->url,
964
- "badge_rear": *[references(^._id) && _type == 'content-award'][0].badge_rear.asset->url,
965
- "badge_logo": *[references(^._id) && _type == 'content-award'][0].logo.asset->url,
966
- 'parentCount': coalesce(count(parent_content_data), 0)
967
- } | order(parentCount desc),`
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': ${parentField}.id,
1135
- 'grandparent_id':${grandParentField}.id,
1136
- 'for-calculations': *[${filterGetParent}][0]{
1137
- 'siblings-list': child[]->railcontent_id,
1138
- 'parents-list': *[${filterForParentList}][0].child[]->railcontent_id
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" : *[${filterGetParent}][0].child[${childrenFilter}]->{${queryFields}}
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 = 30
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'] ?? response['railcontent_id']
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(parent_content_data[0].id, 0) },
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://${globalConfig.sanityConfig.projectId}.${api}.sanity.io/v${globalConfig.sanityConfig.version}/data/query/${globalConfig.sanityConfig.dataset}?perspective=${perspective}`
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(parent_content_data))`
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 LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
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: BaseModel
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: EpochMs
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,