musora-content-services 2.167.2 → 2.167.4

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 (140) hide show
  1. package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
  2. package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
  3. package/.idea/codeStyles/Project.xml +61 -0
  4. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  5. package/.idea/modules.xml +8 -0
  6. package/.idea/musora-content-services.iml +8 -0
  7. package/.idea/php.xml +19 -0
  8. package/.idea/prettier.xml +6 -0
  9. package/.idea/vcs.xml +7 -0
  10. package/.yarn/install-state.gz +0 -0
  11. package/CHANGELOG.md +10 -0
  12. package/package.json +2 -1
  13. package/src/contentTypeConfig.js +1 -0
  14. package/src/lib/sanity/query.ts +5 -0
  15. package/src/services/config.js +15 -0
  16. package/src/services/content/artist.ts +1 -3
  17. package/src/services/content/counts.ts +47 -0
  18. package/src/services/content/genre.ts +1 -3
  19. package/src/services/content/instructor.ts +1 -3
  20. package/src/services/multi-user-accounts/multi-user-accounts.ts +1 -0
  21. package/src/services/userActivity.js +2 -0
  22. package/src/version-info.js +1 -0
  23. package/.claude/settings.local.json +0 -53
  24. package/test/initializeTests.js +0 -60
  25. package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
  26. package/test/integration/awards/internal/image-utils.test.ts +0 -86
  27. package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
  28. package/test/integration/content-progress/contentProgress.test.ts +0 -335
  29. package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
  30. package/test/integration/content-progress/e2eProgress.test.ts +0 -612
  31. package/test/integration/content-progress/getters.test.ts +0 -334
  32. package/test/integration/content-progress/helpers.test.ts +0 -263
  33. package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
  34. package/test/integration/forums.test.ts +0 -209
  35. package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
  36. package/test/integration/initializeTestDB.ts +0 -80
  37. package/test/integration/learning-paths.test.ts +0 -854
  38. package/test/integration/notifications.test.ts +0 -178
  39. package/test/integration/progress-row/content-card.test.ts +0 -283
  40. package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
  41. package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
  42. package/test/integration/sync/fetch.test.ts +0 -224
  43. package/test/integration/sync/repositories/content-likes.test.ts +0 -99
  44. package/test/integration/sync/repositories/practices.test.ts +0 -179
  45. package/test/integration/sync/repositories/progress.test.ts +0 -245
  46. package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
  47. package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
  48. package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
  49. package/test/integration/sync/store/store-idb.test.ts +0 -180
  50. package/test/integration/sync/store/store.test.ts +0 -361
  51. package/test/integration/user/permissions.test.js +0 -20
  52. package/test/integration/userActivity.test.ts +0 -102
  53. package/test/live/README.md +0 -29
  54. package/test/live/content.test.js +0 -116
  55. package/test/localStorageMock.js +0 -21
  56. package/test/log.js +0 -5
  57. package/test/mockData/award-definitions.js +0 -171
  58. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
  59. package/test/mockData/mockData_progress_content.json +0 -182
  60. package/test/mockData/mockData_sanity_progress_content.json +0 -1451
  61. package/test/mockData/mockData_user_practices.json +0 -9
  62. package/test/setupConsole.js +0 -6
  63. package/test/setupNetworkGuard.js +0 -3
  64. package/test/setupTimers.js +0 -13
  65. package/test/unit/HttpClient.test.ts +0 -267
  66. package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
  67. package/test/unit/awards/award-auto-refresh.test.ts +0 -84
  68. package/test/unit/awards/award-calculations.test.ts +0 -34
  69. package/test/unit/awards/award-callbacks.test.ts +0 -144
  70. package/test/unit/awards/award-certificate-display.test.ts +0 -330
  71. package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
  72. package/test/unit/awards/award-collection-filtering.test.ts +0 -287
  73. package/test/unit/awards/award-completion-flow.test.ts +0 -306
  74. package/test/unit/awards/award-multi-lesson.test.ts +0 -243
  75. package/test/unit/awards/award-observer-integration.test.ts +0 -327
  76. package/test/unit/awards/award-query-messages.test.ts +0 -448
  77. package/test/unit/awards/award-user-collection.test.ts +0 -416
  78. package/test/unit/awards/duplicate-prevention.test.ts +0 -120
  79. package/test/unit/awards/helpers/completion-mock.ts +0 -56
  80. package/test/unit/awards/helpers/index.ts +0 -3
  81. package/test/unit/awards/helpers/mock-setup.ts +0 -79
  82. package/test/unit/awards/helpers/progress-emitter.ts +0 -45
  83. package/test/unit/awards/message-generator.test.ts +0 -162
  84. package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
  85. package/test/unit/content-progress/helpers.test.ts +0 -329
  86. package/test/unit/content-progress/navigateTo.test.ts +0 -392
  87. package/test/unit/contentLikes.test.ts +0 -62
  88. package/test/unit/contentMetaData.test.ts +0 -58
  89. package/test/unit/dataContext.test.ts +0 -38
  90. package/test/unit/dateUtils.test.ts +0 -188
  91. package/test/unit/endScreen.test.js +0 -712
  92. package/test/unit/eventsAPI.test.ts +0 -159
  93. package/test/unit/imageSRCBuilder.test.ts +0 -37
  94. package/test/unit/imageSRCVerify.test.ts +0 -155
  95. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
  96. package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
  97. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
  98. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
  99. package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
  100. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
  101. package/test/unit/lib/filter.test.ts +0 -1154
  102. package/test/unit/lib/lastUpdated.test.ts +0 -21
  103. package/test/unit/lib/query.test.ts +0 -1469
  104. package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
  105. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
  106. package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
  107. package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
  108. package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
  109. package/test/unit/playerSettings.test.ts +0 -73
  110. package/test/unit/progress-row/content-card.test.ts +0 -314
  111. package/test/unit/progress-row/playlist-card.test.ts +0 -104
  112. package/test/unit/progressRows.test.ts +0 -243
  113. package/test/unit/recommendations.test.js +0 -47
  114. package/test/unit/sanityQueryService.test.ts +0 -200
  115. package/test/unit/search.test.ts +0 -44
  116. package/test/unit/sentry.test.ts +0 -62
  117. package/test/unit/services/progress-internal/bubble.test.ts +0 -279
  118. package/test/unit/services/progress.test.ts +0 -396
  119. package/test/unit/streakMessage.test.ts +0 -251
  120. package/test/unit/sync/context.test.ts +0 -51
  121. package/test/unit/sync/effects/logout-warning.test.ts +0 -158
  122. package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
  123. package/test/unit/sync/errors/sync-errors.test.ts +0 -106
  124. package/test/unit/sync/errors/validators.test.ts +0 -61
  125. package/test/unit/sync/helpers/TestModel.ts +0 -44
  126. package/test/unit/sync/helpers/index.ts +0 -172
  127. package/test/unit/sync/manager.test.ts +0 -303
  128. package/test/unit/sync/models/user-award-progress.test.ts +0 -82
  129. package/test/unit/sync/resolver.test.ts +0 -229
  130. package/test/unit/sync/retry.test.ts +0 -314
  131. package/test/unit/sync/run-scope.test.ts +0 -23
  132. package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
  133. package/test/unit/sync/store/push-coalescer.test.ts +0 -156
  134. package/test/unit/sync/store-configs.test.ts +0 -37
  135. package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
  136. package/test/unit/sync/utils/event-emitter.test.ts +0 -64
  137. package/test/unit/sync/utils/throttle.test.ts +0 -245
  138. package/test/unit/url-builder.test.ts +0 -72
  139. package/test/unit/user/account.test.ts +0 -66
  140. package/test/unit/user/permissions.test.js +0 -191
@@ -1,368 +0,0 @@
1
- import {
2
- decorate,
3
- decorateAll,
4
- decorateAsync,
5
- decorateAllAsync,
6
- type Decoratable,
7
- type FieldDecorator,
8
- type FieldDecoratorAsync,
9
- } from '../../../../../src/lib/sanity/decorators/base'
10
-
11
- describe('base decorator', () => {
12
- describe('decorate', () => {
13
- test('sets field on single object', () => {
14
- const item: Decoratable = { id: 1 }
15
- decorate(item, 'mark', () => 'A')
16
- expect(item.mark).toBe('A')
17
- })
18
-
19
- test('sets field on every item in array', () => {
20
- const items: Decoratable[] = [{ id: 1 }, { id: 2 }, { id: 3 }]
21
- decorate(items, 'mark', (i) => i.id)
22
- expect(items.map((i) => i.mark)).toEqual([1, 2, 3])
23
- })
24
-
25
- test('returns the same reference it was given', () => {
26
- const items: Decoratable[] = [{ id: 1 }]
27
- const result = decorate(items, 'mark', () => true)
28
- expect(result).toBe(items)
29
- })
30
-
31
- test('recurses children up to 3 levels deep', () => {
32
- const tree: Decoratable = {
33
- id: 1,
34
- children: [
35
- {
36
- id: 2,
37
- children: [
38
- {
39
- id: 3,
40
- children: [{ id: 4 }],
41
- },
42
- ],
43
- },
44
- ],
45
- }
46
- decorate(tree, 'visited', () => true)
47
-
48
- expect(tree.visited).toBe(true)
49
- const lvl1 = (tree.children as Decoratable[])[0]
50
- const lvl2 = (lvl1.children as Decoratable[])[0]
51
- const lvl3 = (lvl2.children as Decoratable[])[0]
52
- expect(lvl1.visited).toBe(true)
53
- expect(lvl2.visited).toBe(true)
54
- expect(lvl3.visited).toBeUndefined()
55
- })
56
-
57
- test('ignores missing or non-array children', () => {
58
- const item: Decoratable = { id: 1 }
59
- expect(() => decorate(item, 'mark', () => 1)).not.toThrow()
60
- expect(item.mark).toBe(1)
61
-
62
- const bad: Decoratable = { id: 1, children: 'not-an-array' as unknown as Decoratable[] }
63
- expect(() => decorate(bad, 'mark', () => 1)).not.toThrow()
64
- expect(bad.mark).toBe(1)
65
- })
66
-
67
- test('passes the visited item to the compute callback', () => {
68
- const item: Decoratable = { id: 7, label: 'x' }
69
- const compute = jest.fn(() => 'ok')
70
- decorate(item, 'mark', compute)
71
- expect(compute).toHaveBeenCalledTimes(1)
72
- expect(compute).toHaveBeenCalledWith(item)
73
- })
74
- })
75
-
76
- describe('decorateAll', () => {
77
- test('applies every decorator on a single walk', () => {
78
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }] }]
79
- const a: FieldDecorator<Decoratable> = { field: 'a', compute: () => 'A' }
80
- const b: FieldDecorator<Decoratable> = { field: 'b', compute: () => 'B' }
81
- decorateAll(items, [a, b])
82
-
83
- expect(items[0].a).toBe('A')
84
- expect(items[0].b).toBe('B')
85
- const child = (items[0].children as Decoratable[])[0]
86
- expect(child.a).toBe('A')
87
- expect(child.b).toBe('B')
88
- })
89
-
90
- test('empty decorator list is a no-op', () => {
91
- const items: Decoratable[] = [{ id: 1 }]
92
- decorateAll(items, [])
93
- expect(Object.keys(items[0])).toEqual(['id'])
94
- })
95
-
96
- test('visits each item exactly once even with multiple decorators', () => {
97
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }, { id: 3 }] }]
98
- const seen: number[] = []
99
- const probe: FieldDecorator<Decoratable> = {
100
- field: 'probe',
101
- compute: (item) => {
102
- seen.push(item.id as number)
103
- return true
104
- },
105
- }
106
- decorateAll(items, [probe, probe])
107
- expect(seen.sort()).toEqual([1, 1, 2, 2, 3, 3])
108
- })
109
-
110
- test('recurse:false decorator runs only at top level; recurse:true descends', () => {
111
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }, { id: 3 }] }]
112
- const topOnlyHits: number[] = []
113
- const everyHits: number[] = []
114
- const topOnly: FieldDecorator<Decoratable> = {
115
- field: 'top',
116
- compute: (item) => {
117
- topOnlyHits.push(item.id as number)
118
- return true
119
- },
120
- recurse: false,
121
- }
122
- const every: FieldDecorator<Decoratable> = {
123
- field: 'every',
124
- compute: (item) => {
125
- everyHits.push(item.id as number)
126
- return true
127
- },
128
- }
129
- decorateAll(items, [topOnly, every])
130
- expect(topOnlyHits).toEqual([1])
131
- expect(everyHits.sort()).toEqual([1, 2, 3])
132
- expect(items[0].top).toBe(true)
133
- const child = (items[0].children as Decoratable[])[0]
134
- expect(child.top).toBeUndefined()
135
- expect(child.every).toBe(true)
136
- })
137
-
138
- test('all decorators recurse:false → children untouched, walk skipped', () => {
139
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }] }]
140
- const seen: number[] = []
141
- const dec: FieldDecorator<Decoratable> = {
142
- field: 'mark',
143
- compute: (item) => {
144
- seen.push(item.id as number)
145
- return true
146
- },
147
- recurse: false,
148
- }
149
- decorateAll(items, [dec])
150
- expect(seen).toEqual([1])
151
- const child = (items[0].children as Decoratable[])[0]
152
- expect(child.mark).toBeUndefined()
153
- })
154
- })
155
-
156
- describe('decorateAsync', () => {
157
- test('sets resolved value on single object', async () => {
158
- const item: Decoratable = { id: 1 }
159
- await decorateAsync(item, 'mark', async () => 'A')
160
- expect(item.mark).toBe('A')
161
- })
162
-
163
- test('sets resolved value on every item in array', async () => {
164
- const items: Decoratable[] = [{ id: 1 }, { id: 2 }, { id: 3 }]
165
- await decorateAsync(items, 'mark', async (i) => i.id)
166
- expect(items.map((i) => i.mark)).toEqual([1, 2, 3])
167
- })
168
-
169
- test('returns same reference it was given', async () => {
170
- const items: Decoratable[] = [{ id: 1 }]
171
- const result = await decorateAsync(items, 'mark', async () => true)
172
- expect(result).toBe(items)
173
- })
174
-
175
- test('recurses children up to 3 levels deep', async () => {
176
- const tree: Decoratable = {
177
- id: 1,
178
- children: [
179
- {
180
- id: 2,
181
- children: [
182
- {
183
- id: 3,
184
- children: [{ id: 4 }],
185
- },
186
- ],
187
- },
188
- ],
189
- }
190
- await decorateAsync(tree, 'visited', async () => true)
191
-
192
- expect(tree.visited).toBe(true)
193
- const lvl1 = (tree.children as Decoratable[])[0]
194
- const lvl2 = (lvl1.children as Decoratable[])[0]
195
- const lvl3 = (lvl2.children as Decoratable[])[0]
196
- expect(lvl1.visited).toBe(true)
197
- expect(lvl2.visited).toBe(true)
198
- expect(lvl3.visited).toBeUndefined()
199
- })
200
-
201
- test('propagates rejection from compute', async () => {
202
- const item: Decoratable = { id: 1 }
203
- const failing = decorateAsync(item, 'mark', async () => {
204
- throw new Error('boom')
205
- })
206
- await expect(failing).rejects.toThrow('boom')
207
- })
208
- })
209
-
210
- describe('decorateAllAsync', () => {
211
- test('applies every async decorator on a single walk', async () => {
212
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }] }]
213
- const a: FieldDecoratorAsync<Decoratable> = {
214
- field: 'a',
215
- compute: async () => 'A',
216
- }
217
- const b: FieldDecoratorAsync<Decoratable> = {
218
- field: 'b',
219
- compute: async () => 'B',
220
- }
221
- await decorateAllAsync(items, [a, b])
222
-
223
- expect(items[0].a).toBe('A')
224
- expect(items[0].b).toBe('B')
225
- const child = (items[0].children as Decoratable[])[0]
226
- expect(child.a).toBe('A')
227
- expect(child.b).toBe('B')
228
- })
229
-
230
- test('empty decorator list is a no-op', async () => {
231
- const items: Decoratable[] = [{ id: 1 }]
232
- await decorateAllAsync(items, [])
233
- expect(Object.keys(items[0])).toEqual(['id'])
234
- })
235
-
236
- test('runs decorators for one item in parallel', async () => {
237
- const items: Decoratable[] = [{ id: 1 }]
238
- let active = 0
239
- let peak = 0
240
- const tracker = async () => {
241
- active++
242
- peak = Math.max(peak, active)
243
- await new Promise((r) => setTimeout(r, 5))
244
- active--
245
- return true
246
- }
247
- await decorateAllAsync(items, [
248
- { field: 'a', compute: tracker },
249
- { field: 'b', compute: tracker },
250
- { field: 'c', compute: tracker },
251
- ])
252
- expect(peak).toBe(3)
253
- })
254
-
255
- test('runs items in parallel', async () => {
256
- const items: Decoratable[] = [{ id: 1 }, { id: 2 }, { id: 3 }]
257
- let active = 0
258
- let peak = 0
259
- const tracker = async () => {
260
- active++
261
- peak = Math.max(peak, active)
262
- await new Promise((r) => setTimeout(r, 5))
263
- active--
264
- return true
265
- }
266
- await decorateAllAsync(items, [{ field: 'mark', compute: tracker }])
267
- expect(peak).toBe(3)
268
- })
269
-
270
- test('runs children in parallel', async () => {
271
- const items: Decoratable[] = [
272
- { id: 1, children: [{ id: 2 }, { id: 3 }, { id: 4 }] },
273
- ]
274
- let active = 0
275
- let peak = 0
276
- const tracker = async () => {
277
- active++
278
- peak = Math.max(peak, active)
279
- await new Promise((r) => setTimeout(r, 5))
280
- active--
281
- return true
282
- }
283
- await decorateAllAsync(items, [{ field: 'mark', compute: tracker }])
284
- expect(peak).toBeGreaterThanOrEqual(3)
285
- })
286
-
287
- test('visits each item exactly once with multiple decorators', async () => {
288
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }, { id: 3 }] }]
289
- const seen: number[] = []
290
- const probe: FieldDecoratorAsync<Decoratable> = {
291
- field: 'probe',
292
- compute: async (item) => {
293
- seen.push(item.id as number)
294
- return true
295
- },
296
- }
297
- await decorateAllAsync(items, [probe, probe])
298
- expect(seen.sort()).toEqual([1, 1, 2, 2, 3, 3])
299
- })
300
-
301
- test('rejects when any decorator throws', async () => {
302
- const items: Decoratable[] = [{ id: 1 }]
303
- const ok: FieldDecoratorAsync<Decoratable> = {
304
- field: 'ok',
305
- compute: async () => true,
306
- }
307
- const bad: FieldDecoratorAsync<Decoratable> = {
308
- field: 'bad',
309
- compute: async () => {
310
- throw new Error('nope')
311
- },
312
- }
313
- await expect(decorateAllAsync(items, [ok, bad])).rejects.toThrow('nope')
314
- })
315
-
316
- test('passes visited item to compute', async () => {
317
- const item: Decoratable = { id: 7, label: 'x' }
318
- const compute = jest.fn(async () => 'ok')
319
- await decorateAllAsync(item, [{ field: 'mark', compute }])
320
- expect(compute).toHaveBeenCalledTimes(1)
321
- expect(compute).toHaveBeenCalledWith(item)
322
- })
323
-
324
- test('recurse:false decorator runs only at top level; recurse:true descends', async () => {
325
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }, { id: 3 }] }]
326
- const topOnlyHits: number[] = []
327
- const everyHits: number[] = []
328
- const topOnly: FieldDecoratorAsync<Decoratable> = {
329
- field: 'top',
330
- compute: async (item) => {
331
- topOnlyHits.push(item.id as number)
332
- return true
333
- },
334
- recurse: false,
335
- }
336
- const every: FieldDecoratorAsync<Decoratable> = {
337
- field: 'every',
338
- compute: async (item) => {
339
- everyHits.push(item.id as number)
340
- return true
341
- },
342
- }
343
- await decorateAllAsync(items, [topOnly, every])
344
- expect(topOnlyHits).toEqual([1])
345
- expect(everyHits.sort()).toEqual([1, 2, 3])
346
- const child = (items[0].children as Decoratable[])[0]
347
- expect(child.top).toBeUndefined()
348
- expect(child.every).toBe(true)
349
- })
350
-
351
- test('all async decorators recurse:false → children untouched', async () => {
352
- const items: Decoratable[] = [{ id: 1, children: [{ id: 2 }] }]
353
- const seen: number[] = []
354
- const dec: FieldDecoratorAsync<Decoratable> = {
355
- field: 'mark',
356
- compute: async (item) => {
357
- seen.push(item.id as number)
358
- return true
359
- },
360
- recurse: false,
361
- }
362
- await decorateAllAsync(items, [dec])
363
- expect(seen).toEqual([1])
364
- const child = (items[0].children as Decoratable[])[0]
365
- expect(child.mark).toBeUndefined()
366
- })
367
- })
368
- })