musora-content-services 2.162.1 → 2.164.5

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.
@@ -0,0 +1,53 @@
1
+ ---
2
+ date: 2026-05-19
3
+ branch: refactor/content-progress-ts-part-2
4
+ pr: https://github.com/railroadmedia/musora-content-services/pull/978
5
+ status: open
6
+ tags: [[chore]]
7
+ ---
8
+
9
+ # Split progress module into typed sub-modules with Progress namespace
10
+
11
+ ## Context
12
+
13
+ `src/services/progress/index.ts` started as a flat file during the initial migration of `contentProgress.js` to TypeScript. As the migration continued it became clear the file would grow significantly, mixing query helpers, read operations, collection queries, and shared types. Naming also conflicted with the legacy JS exports still living in `contentProgress.js` during the transition.
14
+
15
+ ## Decision
16
+
17
+ Split `progress/index.ts` into focused sub-modules:
18
+
19
+ - `state.ts` — point queries for progress state, playback position, last interacted
20
+ - `collections.ts` — bulk queries (allStarted, allCompleted, allStartedOrCompleted)
21
+ - `types.ts` — shared interfaces (GetAllQueryOptions, QueryMetadata, StartedOrCompletedOptions)
22
+ - `internal/queries.ts` — private DB helpers (getById, getByIds, getByRecordIds)
23
+
24
+ `index.ts` becomes a 5-line file that spreads both modules into a `Progress` namespace object:
25
+
26
+ ```ts
27
+ export const Progress = { ...state, ...collections }
28
+ ```
29
+
30
+ **Naming convention: nouns for reads, verbs for writes.**
31
+ Query functions drop their `get`/`find`/`getAll` prefixes since the namespace provides context (`Progress.state`, `Progress.allStarted`). Write operations not yet migrated will keep verb names when they land (`Progress.recordWatchSession`, `Progress.markCompleted`).
32
+
33
+ `resumeTimeByIds` was renamed to `playbackPositionByIds` — `resumeTime` read as a verb phrase in camelCase and was semantically ambiguous.
34
+
35
+ `navigate-to.ts` updated to consume `Progress.*` instead of named imports from the old flat index.
36
+
37
+ ## Alternatives Considered
38
+
39
+ **Drop `get` prefix only (standalone names):** Rejected because `findIncompleteLesson` has a `find` prefix with semantic meaning (sync local search, not a DB fetch). Mixing bare nouns with `find*` names produces an inconsistent surface. The namespace approach sidesteps the verb question entirely.
40
+
41
+ **Nested read/write namespace (`Progress.get.*` / `Progress.set.*`):** Cleaner separation on paper but adds nesting for what is currently a read-only module. The simpler convention (nouns=reads, verbs=writes in flat namespace) was chosen to avoid premature structure.
42
+
43
+ ## Process Notes
44
+
45
+ - `getByIds` and `getById` in `internal/queries.ts` originally had `collection` as the second parameter. The user's linter moved it to last as an optional param — this required updating all call sites in `state.ts` and cascading arg-order fixes in navigate-to.ts and tests.
46
+ - LSP diagnostics repeatedly fired false positives after the param reorder, claiming `string` was not assignable to `CollectionParameter`. `npx tsc --noEmit` was consistently the source of truth — all those diagnostics were stale cache.
47
+ - `findIncompleteLesson` param order was changed by the linter to `(progressOnItems, contentType, currentContentId?)` — both `navigate-to.ts` and the test file required arg-order updates.
48
+
49
+ ## Consequences
50
+
51
+ - External callers import `Progress` and call `Progress.state(id)`, `Progress.allStarted()`, etc. — no ambiguity with `contentProgress.js` exports during the remaining migration
52
+ - Write operations remain in `contentProgress.js` until a follow-up PR migrates them into the `Progress` namespace with verb names
53
+ - The `internal/` directory convention signals private helpers not intended for direct import outside the progress module
@@ -17,11 +17,12 @@
17
17
  "Bash(echo \"exit=$?\")",
18
18
  "Bash(git checkout *)",
19
19
  "Skill(pr)",
20
- "Skill(create-decision)"
20
+ "Skill(create-decision)",
21
+ "mcp__github__pull_request_read",
22
+ "mcp__github__push_files"
21
23
  ]
22
24
  },
23
25
  "model": "sonnet",
24
- "effortLevel": "medium",
25
26
  "enabledMcpjsonServers": [
26
27
  "atlassian",
27
28
  "figma",
@@ -37,5 +38,9 @@
37
38
  "railway",
38
39
  "github",
39
40
  "asana"
40
- ]
41
+ ],
42
+ "disabledMcpjsonServers": [
43
+ "nightwatch"
44
+ ],
45
+ "effortLevel": "medium"
41
46
  }
@@ -19,7 +19,7 @@ jobs:
19
19
  - name: Run unit tests
20
20
  run: npm test -- --coverage
21
21
  - name: Upload coverage to Codecov
22
- uses: codecov/codecov-action@v5
22
+ uses: codecov/codecov-action@v4
23
23
  with:
24
24
  token: ${{ secrets.CODECOV_TOKEN }}
25
25
  slug: railroadmedia/musora-content-services
@@ -39,7 +39,7 @@ jobs:
39
39
  - name: Run integration tests
40
40
  run: npm run test:integration
41
41
  - name: Upload coverage to Codecov
42
- uses: codecov/codecov-action@v5
42
+ uses: codecov/codecov-action@v4
43
43
  with:
44
44
  token: ${{ secrets.CODECOV_TOKEN }}
45
45
  slug: railroadmedia/musora-content-services
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
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.164.5](https://github.com/railroadmedia/musora-content-services/compare/v2.164.4...v2.164.5) (2026-06-10)
6
+
7
+ ### [2.164.4](https://github.com/railroadmedia/musora-content-services/compare/v2.164.3...v2.164.4) (2026-06-10)
8
+
9
+ ### [2.164.3](https://github.com/railroadmedia/musora-content-services/compare/v2.164.2...v2.164.3) (2026-06-10)
10
+
11
+ ### [2.164.2](https://github.com/railroadmedia/musora-content-services/compare/v2.164.1...v2.164.2) (2026-06-10)
12
+
13
+ ### [2.164.1](https://github.com/railroadmedia/musora-content-services/compare/v2.164.0...v2.164.1) (2026-06-10)
14
+
15
+ ## [2.164.0](https://github.com/railroadmedia/musora-content-services/compare/v2.160.5...v2.164.0) (2026-06-10)
16
+
17
+
18
+ ### Features
19
+
20
+ * add helper for FE/MA method progress card (free method) ([#987](https://github.com/railroadmedia/musora-content-services/issues/987)) ([ce8e491](https://github.com/railroadmedia/musora-content-services/commit/ce8e49128d54ec4c3b36261eb0e550b152c3041a))
21
+ * add need access logic branch for LPs ([#993](https://github.com/railroadmedia/musora-content-services/issues/993)) ([1d935aa](https://github.com/railroadmedia/musora-content-services/commit/1d935aaf147b45cd01bc1d438424f5322991aa69))
22
+ * **BEHLTP-106:** membership data upgrade ([#973](https://github.com/railroadmedia/musora-content-services/issues/973)) ([e1e0611](https://github.com/railroadmedia/musora-content-services/commit/e1e0611c13bdf4be920eb28cca1f15f912930d24))
23
+ * **TP-1195:** optimize live event loading ([#971](https://github.com/railroadmedia/musora-content-services/issues/971)) ([8daa616](https://github.com/railroadmedia/musora-content-services/commit/8daa6160c5bc1ee005093719beef1dd9e83ac3c8))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * dont set resume-time to null ([#992](https://github.com/railroadmedia/musora-content-services/issues/992)) ([d7f0f3e](https://github.com/railroadmedia/musora-content-services/commit/d7f0f3e6aab53dcb332a9c149dd0a33f95bc1d75))
29
+ * **MU2-1487:** fetchAll to by pass permissions filter ([#991](https://github.com/railroadmedia/musora-content-services/issues/991)) ([39f38ca](https://github.com/railroadmedia/musora-content-services/commit/39f38cad21b97591eae113c32212b1fdc76be035))
30
+ * Replace PUT with PATCH ([a74a007](https://github.com/railroadmedia/musora-content-services/commit/a74a007efdc19fed98a882db5bf15741d95b89d0))
31
+
32
+ ## [2.163.0](https://github.com/railroadmedia/musora-content-services/compare/v2.162.1...v2.163.0) (2026-06-04)
33
+
34
+
35
+ ### Features
36
+
37
+ * add need access logic branch for LPs ([#993](https://github.com/railroadmedia/musora-content-services/issues/993)) ([1d935aa](https://github.com/railroadmedia/musora-content-services/commit/1d935aaf147b45cd01bc1d438424f5322991aa69))
38
+ * **TP-1195:** optimize live event loading ([#971](https://github.com/railroadmedia/musora-content-services/issues/971)) ([8daa616](https://github.com/railroadmedia/musora-content-services/commit/8daa6160c5bc1ee005093719beef1dd9e83ac3c8))
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * dont set resume-time to null ([#992](https://github.com/railroadmedia/musora-content-services/issues/992)) ([d7f0f3e](https://github.com/railroadmedia/musora-content-services/commit/d7f0f3e6aab53dcb332a9c149dd0a33f95bc1d75))
44
+
5
45
  ### [2.162.1](https://github.com/railroadmedia/musora-content-services/compare/v2.162.0...v2.162.1) (2026-06-04)
6
46
 
7
47
  ## [2.162.0](https://github.com/railroadmedia/musora-content-services/compare/v2.161.4...v2.162.0) (2026-06-03)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.162.1",
3
+ "version": "2.164.5",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -695,7 +695,9 @@ export let contentTypeConfig = {
695
695
  }`,
696
696
  ],
697
697
  'new-and-scheduled': {
698
- fields: ['show_in_new_feed', isLiveField()],
698
+ fields: [
699
+ 'show_in_new_feed',
700
+ ],
699
701
  includeChildFields: true,
700
702
  },
701
703
  }
package/src/index.d.ts CHANGED
@@ -460,6 +460,11 @@ import {
460
460
  fetchCustomerPayments
461
461
  } from './services/user/payments.ts';
462
462
 
463
+ import {
464
+ fetchPlayerSettings,
465
+ updatePlayerSettings
466
+ } from './services/user/playerSettings.ts';
467
+
463
468
  import {
464
469
  deleteProfilePicture,
465
470
  otherStats,
@@ -624,6 +629,7 @@ declare module 'musora-content-services' {
624
629
  fetchPackData,
625
630
  fetchParentChildRelationshipsFor,
626
631
  fetchPlayAlongsCount,
632
+ fetchPlayerSettings,
627
633
  fetchPlaylist,
628
634
  fetchPlaylistItems,
629
635
  fetchPost,
@@ -841,6 +847,7 @@ declare module 'musora-content-services' {
841
847
  updateMultiUserAccount,
842
848
  updateNotificationSetting,
843
849
  updateOnboarding,
850
+ updatePlayerSettings,
844
851
  updatePlaylist,
845
852
  updatePost,
846
853
  updatePracticeNotes,
package/src/index.js CHANGED
@@ -464,6 +464,11 @@ import {
464
464
  fetchCustomerPayments
465
465
  } from './services/user/payments.ts';
466
466
 
467
+ import {
468
+ fetchPlayerSettings,
469
+ updatePlayerSettings
470
+ } from './services/user/playerSettings.ts';
471
+
467
472
  import {
468
473
  deleteProfilePicture,
469
474
  otherStats,
@@ -623,6 +628,7 @@ export {
623
628
  fetchPackData,
624
629
  fetchParentChildRelationshipsFor,
625
630
  fetchPlayAlongsCount,
631
+ fetchPlayerSettings,
626
632
  fetchPlaylist,
627
633
  fetchPlaylistItems,
628
634
  fetchPost,
@@ -840,6 +846,7 @@ export {
840
846
  updateMultiUserAccount,
841
847
  updateNotificationSetting,
842
848
  updateOnboarding,
849
+ updatePlayerSettings,
843
850
  updatePlaylist,
844
851
  updatePost,
845
852
  updatePracticeNotes,
@@ -212,7 +212,7 @@ export async function navigateToParallelWithProgress() {
212
212
 
213
213
  const items = navigateRows as ContentWithNavAndProgress[]
214
214
  const decorators: FieldDecoratorAsync<ContentWithNavAndProgress>[] = [
215
- navigateToDecorator as FieldDecoratorAsync<ContentWithNavAndProgress>,
215
+ navigateToDecorator() as FieldDecoratorAsync<ContentWithNavAndProgress>,
216
216
  {
217
217
  field: 'progress_percent',
218
218
  compute: (item) => fetchProgress(item.id),
@@ -1,15 +1,10 @@
1
1
  import { decorateAllAsync, type Decoratable, type FieldDecoratorAsync } from './base'
2
- import {
3
- findIncompleteLesson,
4
- getLastInteractedOf,
5
- getProgressState,
6
- getProgressStateByIds,
7
- } from '../../../services/contentProgress.js'
8
2
  import {
9
3
  COLLECTION_TYPE,
10
4
  CollectionParameter,
11
5
  STATE,
12
6
  } from '../../../services/sync/models/ContentProgress'
7
+ import { Progress } from '@/services/progress'
13
8
 
14
9
  export const NAVIGATE_TO_FIELD = 'navigateTo' as const
15
10
 
@@ -18,7 +13,7 @@ const NAVIGABLE_TYPES = [
18
13
  'guided-course',
19
14
  'course-collection',
20
15
  'song-tutorial',
21
- 'learning-path-v2',
16
+ COLLECTION_TYPE.LEARNING_PATH,
22
17
  'skill-pack',
23
18
  ] as const
24
19
 
@@ -68,38 +63,66 @@ function buildNavigateTo(
68
63
  }
69
64
  }
70
65
 
71
- async function computeNavigateTo(content: NavigateToDecoratable): Promise<NavigateTo | null> {
66
+ interface NavigateContext {
67
+ states: Map<number, string>
68
+ }
69
+
70
+ async function prefetchStates(items: NavigateToDecoratable[]): Promise<NavigateContext> {
71
+ const ids = new Set<number>()
72
+ for (const item of items) {
73
+ if (!item || !NAVIGABLE_TYPES.includes(item.type as (typeof NAVIGABLE_TYPES)[number])) continue
74
+ ids.add(item.id)
75
+ for (const child of item.children ?? []) {
76
+ ids.add(child.id)
77
+ if (TWO_DEPTH_TYPES.includes(item.type)) {
78
+ for (const grandchild of child.children ?? []) {
79
+ ids.add(grandchild.id)
80
+ }
81
+ }
82
+ }
83
+ }
84
+ if (ids.size === 0) return { states: new Map() }
85
+ const states = await Progress.stateByIds(Array.from(ids))
86
+ return { states }
87
+ }
88
+
89
+ async function computeNavigateTo(
90
+ content: NavigateToDecoratable,
91
+ ctx?: NavigateContext
92
+ ): Promise<NavigateTo | null> {
72
93
  if (!NAVIGABLE_TYPES.includes(content.type as (typeof NAVIGABLE_TYPES)[number])) return null
73
94
 
74
95
  const children = content.children
75
96
  if (!children || children.length === 0) return null
76
97
 
77
- const contentState = await getProgressState(content.id)
98
+ const contentState = ctx?.states.get(content.id) ?? (await Progress.state(content.id))
78
99
  if (contentState !== STATE.STARTED) {
79
100
  const firstChild = children[0]
80
101
  const childNav = TWO_DEPTH_TYPES.includes(content.type)
81
- ? await computeNavigateTo(firstChild)
102
+ ? await computeNavigateTo(firstChild, ctx)
82
103
  : null
83
104
  return buildNavigateTo(firstChild, childNav)
84
105
  }
85
106
 
86
107
  const childrenIds = children.map((c) => c.id)
87
108
  const childrenById = new Map(children.map((c) => [c.id, c]))
88
- const childrenStates = (await getProgressStateByIds(childrenIds)) as Map<number, STATE>
89
- const lastInteractedId = (await getLastInteractedOf(childrenIds)) as number
109
+ const childrenStates = ctx
110
+ ? new Map(childrenIds.map((id) => [id, ctx.states.get(id) ?? '']))
111
+ : await Progress.stateByIds(childrenIds)
112
+ const lastInteractedId = await Progress.lastInteractedOf(childrenIds)
90
113
 
91
114
  if (COURSE_FLOW_TYPES.includes(content.type)) {
92
115
  const lastInteractedStatus = childrenStates.get(lastInteractedId)
93
116
  const targetId =
94
117
  lastInteractedStatus === STATE.STARTED
95
118
  ? lastInteractedId
96
- : findIncompleteLesson(childrenStates, lastInteractedId, content.type)
119
+ : Progress.incompleteLesson(childrenStates, content.type, lastInteractedId)
97
120
  const target = childrenById.get(targetId)
98
121
  return target ? buildNavigateTo(target) : null
99
122
  }
100
123
 
101
124
  if (GUIDED_FLOW_TYPES.includes(content.type)) {
102
- const targetId = findIncompleteLesson(childrenStates, lastInteractedId, content.type)
125
+ const targetId = Progress.incompleteLesson(childrenStates, content.type, lastInteractedId)
103
126
  const target = childrenById.get(targetId)
104
127
  return target ? buildNavigateTo(target) : null
105
128
  }
@@ -107,21 +130,21 @@ async function computeNavigateTo(content: NavigateToDecoratable): Promise<Naviga
107
130
  if (TWO_DEPTH_TYPES.includes(content.type)) {
108
131
  const lastChild = childrenById.get(lastInteractedId)
109
132
  if (!lastChild) return null
110
- const childNav = await computeNavigateTo(lastChild)
133
+ const childNav = await computeNavigateTo(lastChild, ctx)
111
134
  return buildNavigateTo(lastChild, childNav)
112
135
  }
113
136
 
114
137
  return null
115
138
  }
116
139
 
117
- export const navigateToDecorator: FieldDecoratorAsync<
118
- NavigateToDecoratable,
119
- typeof NAVIGATE_TO_FIELD,
120
- NavigateTo | null
121
- > = {
122
- field: NAVIGATE_TO_FIELD,
123
- compute: computeNavigateTo,
124
- recurse: false,
140
+ export function navigateToDecorator(
141
+ ctx?: NavigateContext
142
+ ): FieldDecoratorAsync<NavigateToDecoratable, typeof NAVIGATE_TO_FIELD, NavigateTo | null> {
143
+ return {
144
+ field: NAVIGATE_TO_FIELD,
145
+ compute: (item) => computeNavigateTo(item, ctx),
146
+ recurse: false,
147
+ }
125
148
  }
126
149
 
127
150
  export function decorateNavigateTo<T extends NavigateToDecoratable>(
@@ -130,10 +153,12 @@ export function decorateNavigateTo<T extends NavigateToDecoratable>(
130
153
  export function decorateNavigateTo<T extends NavigateToDecoratable>(
131
154
  items: T
132
155
  ): Promise<WithNavigateTo<T>>
133
- export function decorateNavigateTo<T extends NavigateToDecoratable>(
156
+ export async function decorateNavigateTo<T extends NavigateToDecoratable>(
134
157
  items: T | T[]
135
158
  ): Promise<WithNavigateTo<T> | WithNavigateTo<T>[]> {
136
- return decorateAllAsync(items as NavigateToDecoratable, [navigateToDecorator]) as Promise<
159
+ const list = Array.isArray(items) ? items : [items]
160
+ const ctx = await prefetchStates(list)
161
+ return decorateAllAsync(items as NavigateToDecoratable, [navigateToDecorator(ctx)]) as Promise<
137
162
  WithNavigateTo<T> | WithNavigateTo<T>[]
138
163
  >
139
164
  }
@@ -222,7 +222,7 @@ export async function resetAllLearningPaths() {
222
222
  * @param {number} learningPathId - The learning path ID
223
223
  * @returns {Promise<Object>} Learning path with enriched lesson data
224
224
  */
225
- export async function getEnrichedLearningPath(learningPathId) {
225
+ export async function getEnrichedLearningPath(learningPathId: number) {
226
226
  let response = (await addContextToLearningPaths(
227
227
  fetchByRailContentId,
228
228
  learningPathId,
@@ -841,7 +841,7 @@ export function filterOutLearningPathsForDuplication(progresses, collection) {
841
841
 
842
842
  export async function duplicateProgressForIds(entries) {
843
843
  return Promise.all(Object.entries(entries).map(([id, pct]) => {
844
- return saveContentProgress(parseInt(id), null, pct, null, { skipPush: true, accessedDirectly: false })
844
+ return saveContentProgress(parseInt(id), null, pct, undefined, { skipPush: true, accessedDirectly: false })
845
845
  }))
846
846
  }
847
847
 
@@ -19,6 +19,7 @@ import {
19
19
  } from './permissions'
20
20
  import {arrayToRawRepresentation, arrayToStringRepresentation} from '../../filterBuilder.js'
21
21
  import {basicMembershipTier, plusMembershipTier} from "../../contentTypeConfig";
22
+ import { COLLECTION_TYPE } from '../sync/models/ContentProgress'
22
23
 
23
24
  /**
24
25
  * V2 Permissions Adapter for the new permissions system.
@@ -55,6 +56,9 @@ export class PermissionsV2Adapter extends PermissionsAdapter {
55
56
 
56
57
  // Content with no permissions is accessible to all
57
58
  if (contentPermissions.size === 0) {
59
+ if (content?.type === COLLECTION_TYPE.LEARNING_PATH && content?.children?.length) {
60
+ return content.children.every(child => child.need_access)
61
+ }
58
62
  return false
59
63
  }
60
64
 
File without changes
@@ -0,0 +1,33 @@
1
+ import { db } from '../sync'
2
+ import { ProgressQueryOptions, StartedOrCompletedOptions } from './types'
3
+
4
+ const SIXTY_DAYS_IN_SECONDS = 60 * 24 * 60 * 60
5
+
6
+ const defaultQueryOptions: ProgressQueryOptions = {
7
+ onlyIds: true,
8
+ include: {
9
+ aLaCarte: true,
10
+ learningPaths: false,
11
+ },
12
+ }
13
+
14
+ export const allStarted = async (limit: number | null = null, options?: ProgressQueryOptions) =>
15
+ db.contentProgress.started(limit, options ?? defaultQueryOptions)
16
+
17
+ export const allCompleted = async (limit: number | null = null, options?: ProgressQueryOptions) =>
18
+ db.contentProgress.completed(limit, options ?? defaultQueryOptions)
19
+
20
+ export const allCompletedByIds = async (contentIds: number[]) =>
21
+ db.contentProgress.completedByContentIds(contentIds)
22
+
23
+ export const allStartedOrCompleted = async (
24
+ limit?: number,
25
+ options: StartedOrCompletedOptions = {}
26
+ ) =>
27
+ db.contentProgress
28
+ .startedOrCompleted({
29
+ ...options,
30
+ limit,
31
+ updatedAfter: Math.floor(Date.now() / 1000) - SIXTY_DAYS_IN_SECONDS,
32
+ })
33
+ .then((r) => r.data)
@@ -0,0 +1,6 @@
1
+ import * as state from './state'
2
+ import * as collections from './collections'
3
+
4
+ export const Progress = { ...state, ...collections }
5
+
6
+ export type { ProgressContentFilter, ProgressQueryOptions, StartedOrCompletedOptions } from './types'
@@ -0,0 +1,43 @@
1
+ import { db } from '../../sync'
2
+ import { CollectionParameter } from '../../sync/models/ContentProgress'
3
+
4
+ export const getByIds = async <V>(
5
+ contentIds: number[],
6
+ dataKey: string,
7
+ defaultValue: V,
8
+ collection?: CollectionParameter
9
+ ): Promise<Map<number, V>> => {
10
+ if (contentIds.length === 0) return new Map()
11
+
12
+ const progress = new Map(contentIds.map((id) => [id, defaultValue]))
13
+ await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then((r) => {
14
+ r.data.forEach((p) => {
15
+ progress.set(p.content_id, p[dataKey] ?? defaultValue)
16
+ })
17
+ })
18
+ return progress
19
+ }
20
+
21
+ export const getById = async <V>(
22
+ contentId: number,
23
+ dataKey: string,
24
+ defaultValue: V,
25
+ collection?: CollectionParameter
26
+ ): Promise<V> => {
27
+ if (!contentId) return defaultValue
28
+ return db.contentProgress
29
+ .getOneProgressByContentId(contentId, collection)
30
+ .then((r) => r.data?.[dataKey] ?? defaultValue)
31
+ }
32
+
33
+ export const getByRecordIds = async <V>(ids: string[], dataKey: string, defaultValue: V) => {
34
+ const progress = Object.fromEntries(ids.map((id) => [id, defaultValue]))
35
+
36
+ await db.contentProgress.getSomeProgressByRecordIds(ids).then((r) => {
37
+ r.data.forEach((p) => {
38
+ progress[p.id] = p[dataKey] ?? defaultValue
39
+ })
40
+ })
41
+
42
+ return progress
43
+ }
@@ -0,0 +1,50 @@
1
+ import { db } from '../sync'
2
+ import { COLLECTION_TYPE, CollectionParameter } from '../sync/models/ContentProgress'
3
+ import { getById, getByIds, getByRecordIds } from './internal/queries'
4
+
5
+ export const state = async (contentId: number, collection?: CollectionParameter) =>
6
+ getById(contentId, 'state', '', collection)
7
+
8
+ export const stateByIds = async (contentIds: number[], collection?: CollectionParameter) =>
9
+ getByIds(contentIds, 'state', '', collection)
10
+
11
+ export const stateByRecordIds = async (ids: string[]) => getByRecordIds(ids, 'state', '')
12
+
13
+ export const playbackPositionByIds = async (contentIds: number[], collection?: CollectionParameter) =>
14
+ getByIds(contentIds, 'resume_time_seconds', 0, collection)
15
+
16
+ export const playbackPositionByRecordIds = async (ids: string[]) =>
17
+ getByRecordIds(ids, 'resume_time_seconds', 0)
18
+
19
+ export const lastInteractedOf = (
20
+ contentIds: number[],
21
+ collection?: CollectionParameter
22
+ ): Promise<number | undefined> =>
23
+ db.contentProgress
24
+ .mostRecentlyUpdatedId(contentIds, collection)
25
+ .then((r) => (r.data ? parseInt(r.data, 10) : undefined))
26
+
27
+ export const incompleteLesson = (
28
+ progressOnItems: Map<number, string>,
29
+ contentType: string,
30
+ currentContentId?: number
31
+ ): number | null | undefined => {
32
+ const ids = Array.from(progressOnItems.keys())
33
+ const getProgress = (id: number) => progressOnItems.get(id)
34
+
35
+ if (contentType === 'guided-course' || contentType === COLLECTION_TYPE.LEARNING_PATH) {
36
+ return ids.find((id) => getProgress(id) !== 'completed') || ids.at(0)
37
+ }
38
+
39
+ const currentIndex = ids.indexOf(Number(currentContentId))
40
+ const startIndex = currentIndex === -1 ? 0 : currentIndex + 1
41
+
42
+ for (let i = startIndex; i < ids.length; i++) {
43
+ const id = ids[i]
44
+ if (getProgress(id) !== 'completed') {
45
+ return id
46
+ }
47
+ }
48
+
49
+ return ids[0]
50
+ }
@@ -0,0 +1,16 @@
1
+ export interface ProgressContentFilter {
2
+ aLaCarte?: boolean
3
+ learningPaths?: boolean
4
+ }
5
+
6
+ export interface ProgressQueryOptions {
7
+ onlyIds?: boolean
8
+ include?: ProgressContentFilter
9
+ }
10
+
11
+ export interface StartedOrCompletedOptions {
12
+ brand?: string
13
+ contentTypes?: string[]
14
+ parentId?: number
15
+ include?: ProgressContentFilter
16
+ }
@@ -1193,13 +1193,11 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1193
1193
  default:
1194
1194
  break
1195
1195
  }
1196
- let startDateTemp = new Date()
1197
- let endDateTemp = new Date()
1198
1196
 
1199
- startDateTemp = new Date(
1200
- startDateTemp.setMinutes(startDateTemp.getMinutes() + LIVE_EXTRA_MINUTES)
1201
- )
1202
- endDateTemp = new Date(endDateTemp.setMinutes(endDateTemp.getMinutes() - LIVE_EXTRA_MINUTES))
1197
+ const now = new Date()
1198
+
1199
+ const startOfYesterday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 1)).toISOString()
1200
+ const endOfTomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 2)).toISOString()
1203
1201
 
1204
1202
  const liveEventFields = getLiveFields().concat(
1205
1203
  `'event_coach_calendar_id': coalesce(calendar_id, '${defaultCalendarID}')`
@@ -1212,15 +1210,25 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1212
1210
  : `status == 'scheduled'
1213
1211
  && (brand == '${brand}' || live_global_event == true)
1214
1212
  && defined(live_event_start_time)
1215
- && live_event_start_time <= '${getSanityDate(startDateTemp, false)}'
1216
- && live_event_end_time >= '${getSanityDate(endDateTemp, false)}'`
1213
+ && live_event_start_time >= '${startOfYesterday}'
1214
+ && live_event_start_time < '${endOfTomorrow}'`
1217
1215
 
1218
1216
  const filter = await new FilterBuilder(baseFilter, { bypassPermissions: true }).buildFilter()
1219
1217
 
1220
- // This query finds the first scheduled event (sorted by start_time) that ends after now()
1221
- const query = `*[${filter}]{${fieldsString}} | order(live_event_start_time)[0...1]`
1218
+ const events = await fetchSanity(
1219
+ `*[${filter}]{${fieldsString}} | order(live_event_start_time asc)`,
1220
+ true,
1221
+ { processNeedAccess: false }
1222
+ )
1222
1223
 
1223
- return await fetchSanity(query, false, { processNeedAccess: true })
1224
+ const clientNow = new Date()
1225
+ const windowStart = new Date(clientNow.getTime() - LIVE_EXTRA_MINUTES * 60000).toISOString()
1226
+ const windowEnd = new Date(clientNow.getTime() + LIVE_EXTRA_MINUTES * 60000).toISOString()
1227
+
1228
+ return events?.find(event =>
1229
+ event.live_event_end_time >= windowStart &&
1230
+ event.live_event_start_time <= windowEnd
1231
+ ) ?? null
1224
1232
  }
1225
1233
 
1226
1234
  /**
@@ -1667,8 +1675,8 @@ function contentResultsDecorator(results, fieldName, callback) {
1667
1675
  processChildren(contentItem)
1668
1676
  })
1669
1677
  } else {
1670
- result[fieldName] = callback(result)
1671
1678
  processChildren(result)
1679
+ result[fieldName] = callback(result)
1672
1680
  }
1673
1681
  })
1674
1682
  } else if (results.entity && Array.isArray(results.entity)) {
@@ -1705,8 +1713,8 @@ function contentResultsDecorator(results, fieldName, callback) {
1705
1713
  }
1706
1714
  })
1707
1715
  } else {
1716
+ processChildren(results)
1708
1717
  results[fieldName] = callback(results)
1709
- processChildren(results) // this on was always true
1710
1718
  }
1711
1719
 
1712
1720
  return results
@@ -2224,7 +2232,14 @@ export async function fetchScheduledAndNewReleases(
2224
2232
  return []
2225
2233
  }
2226
2234
 
2227
- return reorderScheduledAndNewReleases(r, limit)
2235
+ const reordered = reorderScheduledAndNewReleases(r, limit)
2236
+ const computedNow = new Date()
2237
+ return reordered.map(item => ({
2238
+ ...item,
2239
+ isLive: item.live_event_start_time && item.live_event_end_time
2240
+ ? new Date(item.live_event_start_time) <= computedNow && new Date(item.live_event_end_time) >= computedNow
2241
+ : false,
2242
+ }))
2228
2243
  }
2229
2244
 
2230
2245
  function reorderScheduledAndNewReleases(r, limit) {