musora-content-services 2.162.0 → 2.163.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/.agent/decisions/2026-05-19-content-progress-ts-part-2.md +53 -0
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +3 -1
- package/src/index.d.ts +10 -1
- package/src/index.js +10 -1
- package/src/lib/sanity/decorators/examples.ts +1 -1
- package/src/lib/sanity/decorators/navigate-to.ts +50 -25
- package/src/services/content-org/learning-paths.ts +1 -1
- package/src/services/contentProgress.js +1 -1
- package/src/services/forums/posts.ts +28 -1
- package/src/services/forums/types.ts +23 -0
- package/src/services/permissions/PermissionsV2Adapter.ts +4 -0
- package/src/services/progress/.indexignore +0 -0
- package/src/services/progress/collections.ts +33 -0
- package/src/services/progress/index.ts +6 -0
- package/src/services/progress/internal/queries.ts +43 -0
- package/src/services/progress/state.ts +50 -0
- package/src/services/progress/types.ts +16 -0
- package/src/services/sanity.js +29 -14
- package/src/services/whoLiked.ts +52 -0
- package/test/unit/lib/sanity/decorators/navigate-to.test.ts +124 -29
- package/test/unit/services/progress.test.ts +300 -0
- package/usage/sanity/decorators/examples.ts +237 -0
|
@@ -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
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.163.0](https://github.com/railroadmedia/musora-content-services/compare/v2.162.1...v2.163.0) (2026-06-04)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 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))
|
|
11
|
+
* **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))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* 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))
|
|
17
|
+
|
|
18
|
+
### [2.162.1](https://github.com/railroadmedia/musora-content-services/compare/v2.162.0...v2.162.1) (2026-06-04)
|
|
19
|
+
|
|
5
20
|
## [2.162.0](https://github.com/railroadmedia/musora-content-services/compare/v2.161.4...v2.162.0) (2026-06-03)
|
|
6
21
|
|
|
7
22
|
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -177,7 +177,8 @@ import {
|
|
|
177
177
|
likePost,
|
|
178
178
|
search,
|
|
179
179
|
unlikePost,
|
|
180
|
-
updatePost
|
|
180
|
+
updatePost,
|
|
181
|
+
whoLikedPost
|
|
181
182
|
} from './services/forums/posts.ts';
|
|
182
183
|
|
|
183
184
|
import {
|
|
@@ -494,6 +495,11 @@ import {
|
|
|
494
495
|
updateUserPractice
|
|
495
496
|
} from './services/userActivity.js';
|
|
496
497
|
|
|
498
|
+
import {
|
|
499
|
+
whoLikedComment,
|
|
500
|
+
whoLikedContent
|
|
501
|
+
} from './services/whoLiked.ts';
|
|
502
|
+
|
|
497
503
|
import {
|
|
498
504
|
default as EventsAPI
|
|
499
505
|
} from './services/eventsAPI';
|
|
@@ -847,6 +853,9 @@ declare module 'musora-content-services' {
|
|
|
847
853
|
userOnboardingForBrand,
|
|
848
854
|
verifyImageSRC,
|
|
849
855
|
verifyLocalDataContext,
|
|
856
|
+
whoLikedComment,
|
|
857
|
+
whoLikedContent,
|
|
858
|
+
whoLikedPost,
|
|
850
859
|
}
|
|
851
860
|
}
|
|
852
861
|
|
package/src/index.js
CHANGED
|
@@ -181,7 +181,8 @@ import {
|
|
|
181
181
|
likePost,
|
|
182
182
|
search,
|
|
183
183
|
unlikePost,
|
|
184
|
-
updatePost
|
|
184
|
+
updatePost,
|
|
185
|
+
whoLikedPost
|
|
185
186
|
} from './services/forums/posts.ts';
|
|
186
187
|
|
|
187
188
|
import {
|
|
@@ -498,6 +499,11 @@ import {
|
|
|
498
499
|
updateUserPractice
|
|
499
500
|
} from './services/userActivity.js';
|
|
500
501
|
|
|
502
|
+
import {
|
|
503
|
+
whoLikedComment,
|
|
504
|
+
whoLikedContent
|
|
505
|
+
} from './services/whoLiked.ts';
|
|
506
|
+
|
|
501
507
|
export {
|
|
502
508
|
PermissionsAdapter,
|
|
503
509
|
PermissionsV1Adapter,
|
|
@@ -846,6 +852,9 @@ export {
|
|
|
846
852
|
userOnboardingForBrand,
|
|
847
853
|
verifyImageSRC,
|
|
848
854
|
verifyLocalDataContext,
|
|
855
|
+
whoLikedComment,
|
|
856
|
+
whoLikedContent,
|
|
857
|
+
whoLikedPost,
|
|
849
858
|
};
|
|
850
859
|
|
|
851
860
|
export default EventsAPI
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
89
|
-
|
|
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
|
-
:
|
|
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 =
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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,
|
|
844
|
+
return saveContentProgress(parseInt(id), null, pct, undefined, { skipPush: true, accessedDirectly: false })
|
|
845
845
|
}))
|
|
846
846
|
}
|
|
847
847
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { HttpClient } from '../../infrastructure/http/HttpClient'
|
|
5
5
|
import { globalConfig } from '../config.js'
|
|
6
|
-
import { ForumPost } from './types'
|
|
6
|
+
import { ForumPost, WhoLikedParams, WhoLikedResponse } from './types'
|
|
7
7
|
import { PaginatedResponse } from '../api/types'
|
|
8
8
|
import { markThreadAsRead } from './threads'
|
|
9
9
|
|
|
@@ -128,6 +128,33 @@ export async function likePost(postId: number, brand: string): Promise<void> {
|
|
|
128
128
|
})
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Fetch the list of users who liked a forum post.
|
|
133
|
+
*
|
|
134
|
+
* @param {number} postId - The ID of the post.
|
|
135
|
+
* @param {string} brand - The brand context (e.g., "drumeo", "singeo").
|
|
136
|
+
* @param {WhoLikedParams} [params] - Optional pagination parameters.
|
|
137
|
+
* @returns {Promise<WhoLikedResponse>} - A promise that resolves to the paginated list of likers.
|
|
138
|
+
* @throws {HttpError} - If the request fails.
|
|
139
|
+
*/
|
|
140
|
+
export async function whoLikedPost(
|
|
141
|
+
postId: number,
|
|
142
|
+
brand: string,
|
|
143
|
+
params: WhoLikedParams = {}
|
|
144
|
+
): Promise<WhoLikedResponse> {
|
|
145
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
146
|
+
const queryObj: Record<string, string> = {
|
|
147
|
+
brand,
|
|
148
|
+
...Object.fromEntries(
|
|
149
|
+
Object.entries({ page: 1, limit: 20, ...params })
|
|
150
|
+
.filter(([_, v]) => v !== undefined && v !== null)
|
|
151
|
+
.map(([k, v]) => [k, String(v)])
|
|
152
|
+
),
|
|
153
|
+
}
|
|
154
|
+
const query = new URLSearchParams(queryObj).toString()
|
|
155
|
+
return httpClient.get<WhoLikedResponse>(`${baseUrl}/v1/posts/${postId}/likes?${query}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
131
158
|
/**
|
|
132
159
|
* Unlike a forum post.
|
|
133
160
|
*
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
export interface WhoLikedParams {
|
|
2
|
+
page?: number
|
|
3
|
+
limit?: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Liker {
|
|
7
|
+
user_id: number
|
|
8
|
+
display_name: string
|
|
9
|
+
profile_picture_url: string | null
|
|
10
|
+
access_level: string
|
|
11
|
+
liked_at: string
|
|
12
|
+
liked_at_diff: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WhoLikedResponse {
|
|
16
|
+
data: Liker[]
|
|
17
|
+
meta: {
|
|
18
|
+
total: number
|
|
19
|
+
current_page: number
|
|
20
|
+
last_page: number
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
export interface ForumUser {
|
|
2
25
|
id: number
|
|
3
26
|
display_name: string
|
|
@@ -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,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
|
+
}
|
package/src/services/sanity.js
CHANGED
|
@@ -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
|
-
|
|
1200
|
-
|
|
1201
|
-
)
|
|
1202
|
-
|
|
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
|
|
1216
|
-
&&
|
|
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
|
-
|
|
1221
|
-
|
|
1218
|
+
const events = await fetchSanity(
|
|
1219
|
+
`*[${filter}]{${fieldsString}} | order(live_event_start_time asc)`,
|
|
1220
|
+
true,
|
|
1221
|
+
{ processNeedAccess: false }
|
|
1222
|
+
)
|
|
1222
1223
|
|
|
1223
|
-
|
|
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
|
-
|
|
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) {
|