musora-content-services 2.165.5 → 2.166.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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +8 -1
- package/src/services/multi-user-accounts/multi-user-accounts.ts +3 -1
- package/src/services/progress/internal/bubble.ts +1 -1
- package/src/services/progress/internal/queries.ts +25 -7
- package/src/services/progress/state.ts +32 -47
- package/src/services/user/memberships.ts +4 -2
- package/.claude/settings.local.json +0 -52
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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.166.0](https://github.com/railroadmedia/musora-content-services/compare/v2.165.5...v2.166.0) (2026-07-02)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEHLPT-186:** subscription upgrade endpoints ([#1003](https://github.com/railroadmedia/musora-content-services/issues/1003)) ([9da55f2](https://github.com/railroadmedia/musora-content-services/commit/9da55f24708976190317a3b69381acbccce2f33c))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **BR-671:** fix Latin/World style search ([#1002](https://github.com/railroadmedia/musora-content-services/issues/1002)) ([25fdff8](https://github.com/railroadmedia/musora-content-services/commit/25fdff80c6279e8568ea057ac250f7e057717ccf))
|
|
16
|
+
|
|
5
17
|
### [2.165.5](https://github.com/railroadmedia/musora-content-services/compare/v2.165.4...v2.165.5) (2026-06-25)
|
|
6
18
|
|
|
7
19
|
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -876,7 +876,14 @@ function createTypeConditions(lessonTypes) {
|
|
|
876
876
|
* Filter handler registry - maps filter keys to their handler functions
|
|
877
877
|
*/
|
|
878
878
|
const filterHandlers = {
|
|
879
|
-
style: (value) =>
|
|
879
|
+
style: (value) => {
|
|
880
|
+
const hasMultipleStyles = value.includes('/')
|
|
881
|
+
const styles = [value]
|
|
882
|
+
if (hasMultipleStyles) {
|
|
883
|
+
styles.push(...value.split('/').map((s) => s.trim()))
|
|
884
|
+
}
|
|
885
|
+
return styles.map((style) => `"${style}" in genre[]->name`).join(' || ')
|
|
886
|
+
},
|
|
880
887
|
|
|
881
888
|
difficulty: (value) => {
|
|
882
889
|
return `difficulty_string == "${value}"`
|
|
@@ -20,7 +20,9 @@ export interface InviteResponse {
|
|
|
20
20
|
can_be_accepted: boolean
|
|
21
21
|
is_account_valid: boolean
|
|
22
22
|
is_invite_active: boolean
|
|
23
|
-
can_user_join: boolean
|
|
23
|
+
can_user_join: boolean // deprecated in favour of is_user_part_of_existing_mua which is more descriptive
|
|
24
|
+
is_user_active_subscriber: boolean
|
|
25
|
+
is_user_part_of_existing_mua: boolean
|
|
24
26
|
primary_user_name: string
|
|
25
27
|
product_name: string
|
|
26
28
|
// These fields leak user information and are excluded entirely for the public endpoint
|
|
@@ -85,7 +85,7 @@ export const bubbleProgress = async (
|
|
|
85
85
|
collection?: CollectionParameter
|
|
86
86
|
): Promise<Record<number, number>> => {
|
|
87
87
|
const ids = getAncestorAndSiblingIds(hierarchy, contentId)
|
|
88
|
-
const progresses = await getByIds(ids,
|
|
88
|
+
const progresses = await getByIds(ids, (p) => p.progress_percent, 0, collection)
|
|
89
89
|
return averageProgressesFor(hierarchy, contentId, progresses)
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { db } from '../../sync'
|
|
2
|
-
import {
|
|
2
|
+
import type { ModelSerialized } from '../../sync/serializers'
|
|
3
|
+
import ContentProgress, { CollectionParameter } from '../../sync/models/ContentProgress'
|
|
4
|
+
|
|
5
|
+
type Selector<V> = (p: ModelSerialized<ContentProgress>) => V
|
|
3
6
|
|
|
4
7
|
export const getByIds = async <V>(
|
|
5
8
|
contentIds: number[],
|
|
6
|
-
|
|
9
|
+
selector: Selector<V>,
|
|
7
10
|
defaultValue: V,
|
|
8
11
|
collection?: CollectionParameter
|
|
9
12
|
): Promise<Map<number, V>> => {
|
|
@@ -12,7 +15,7 @@ export const getByIds = async <V>(
|
|
|
12
15
|
const progress = new Map(contentIds.map((id) => [id, defaultValue]))
|
|
13
16
|
await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then((r) => {
|
|
14
17
|
r.data.forEach((p) => {
|
|
15
|
-
progress.set(p.content_id, p
|
|
18
|
+
progress.set(p.content_id, selector(p) ?? defaultValue)
|
|
16
19
|
})
|
|
17
20
|
})
|
|
18
21
|
return progress
|
|
@@ -20,24 +23,39 @@ export const getByIds = async <V>(
|
|
|
20
23
|
|
|
21
24
|
export const getById = async <V>(
|
|
22
25
|
contentId: number,
|
|
23
|
-
|
|
26
|
+
selector: Selector<V>,
|
|
24
27
|
defaultValue: V,
|
|
25
28
|
collection?: CollectionParameter
|
|
26
29
|
): Promise<V> => {
|
|
27
30
|
if (!contentId) return defaultValue
|
|
28
31
|
return db.contentProgress
|
|
29
32
|
.getOneProgressByContentId(contentId, collection)
|
|
30
|
-
.then((r) => r.data
|
|
33
|
+
.then((r) => (r.data ? selector(r.data) : defaultValue) ?? defaultValue)
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export const getByRecordIds = async <V>(ids: string[],
|
|
36
|
+
export const getByRecordIds = async <V>(ids: string[], selector: Selector<V>, defaultValue: V) => {
|
|
34
37
|
const progress = Object.fromEntries(ids.map((id) => [id, defaultValue]))
|
|
35
38
|
|
|
36
39
|
await db.contentProgress.getSomeProgressByRecordIds(ids).then((r) => {
|
|
37
40
|
r.data.forEach((p) => {
|
|
38
|
-
progress[p.id] = p
|
|
41
|
+
progress[p.id] = selector(p) ?? defaultValue
|
|
39
42
|
})
|
|
40
43
|
})
|
|
41
44
|
|
|
42
45
|
return progress
|
|
43
46
|
}
|
|
47
|
+
|
|
48
|
+
export const queryById =
|
|
49
|
+
<V>(selector: Selector<V>, defaultValue: V) =>
|
|
50
|
+
(contentId: number, collection?: CollectionParameter) =>
|
|
51
|
+
getById(contentId, selector, defaultValue, collection)
|
|
52
|
+
|
|
53
|
+
export const queryByIds =
|
|
54
|
+
<V>(selector: Selector<V>, defaultValue: V) =>
|
|
55
|
+
(contentIds: number[], collection?: CollectionParameter) =>
|
|
56
|
+
getByIds(contentIds, selector, defaultValue, collection)
|
|
57
|
+
|
|
58
|
+
export const queryByRecordIds =
|
|
59
|
+
<V>(selector: Selector<V>, defaultValue: V) =>
|
|
60
|
+
(ids: string[]) =>
|
|
61
|
+
getByRecordIds(ids, selector, defaultValue)
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { db } from '../sync'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { ModelSerialized } from '../sync/serializers'
|
|
3
|
+
import ContentProgress, { COLLECTION_TYPE, CollectionParameter } from '../sync/models/ContentProgress'
|
|
4
|
+
import { queryById, queryByIds, queryByRecordIds } from './internal/queries'
|
|
4
5
|
import type { ProgressSnapshot } from './types'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const buildSnapshotMap = <K extends string | number>(
|
|
8
|
+
ids: K[],
|
|
9
|
+
records: ModelSerialized<ContentProgress>[],
|
|
10
|
+
keyOf: (p: ModelSerialized<ContentProgress>) => K,
|
|
11
|
+
lastUpdateOf: (p: ModelSerialized<ContentProgress>) => number
|
|
12
|
+
): Map<K, ProgressSnapshot> => {
|
|
13
|
+
const overrides = new Map(
|
|
14
|
+
records.map((p) => [
|
|
15
|
+
keyOf(p),
|
|
16
|
+
{ last_update: lastUpdateOf(p), progress: p.progress_percent, status: p.state as string },
|
|
17
|
+
])
|
|
18
|
+
)
|
|
19
|
+
return new Map(
|
|
20
|
+
ids.map((id) => [id, overrides.get(id) ?? { last_update: 0, progress: 0, status: '' }])
|
|
21
|
+
)
|
|
22
|
+
}
|
|
13
23
|
|
|
14
|
-
export const
|
|
15
|
-
|
|
24
|
+
export const state = queryById((p) => p.state as string, '')
|
|
25
|
+
export const stateByIds = queryByIds((p) => p.state as string, '')
|
|
26
|
+
export const stateByRecordIds = queryByRecordIds((p) => p.state as string, '')
|
|
16
27
|
|
|
17
|
-
export const
|
|
18
|
-
|
|
28
|
+
export const playbackPositionByIds = queryByIds((p) => p.resume_time_seconds ?? 0, 0)
|
|
29
|
+
export const playbackPositionByRecordIds = queryByRecordIds((p) => p.resume_time_seconds ?? 0, 0)
|
|
19
30
|
|
|
20
31
|
export const lastInteractedOf = (
|
|
21
32
|
contentIds: number[],
|
|
@@ -53,43 +64,17 @@ export const incompleteLesson = (
|
|
|
53
64
|
export const snapshotByIds = async (
|
|
54
65
|
contentIds: number[],
|
|
55
66
|
collection?: CollectionParameter
|
|
56
|
-
): Promise<Map<number, ProgressSnapshot>> =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then((r) => {
|
|
62
|
-
r.data.forEach((p) => {
|
|
63
|
-
result.set(p.content_id, {
|
|
64
|
-
last_update: p.last_interacted_a_la_carte,
|
|
65
|
-
progress: p.progress_percent,
|
|
66
|
-
status: p.state,
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
return result
|
|
72
|
-
}
|
|
67
|
+
): Promise<Map<number, ProgressSnapshot>> =>
|
|
68
|
+
db.contentProgress
|
|
69
|
+
.getSomeProgressByContentIds(contentIds, collection)
|
|
70
|
+
.then((r) => buildSnapshotMap(contentIds, r.data, (p) => p.content_id, (p) => p.last_interacted_a_la_carte))
|
|
73
71
|
|
|
74
72
|
export const snapshotByRecordIds = async (
|
|
75
73
|
ids: string[]
|
|
76
|
-
): Promise<Record<string, ProgressSnapshot>> =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await db.contentProgress.getSomeProgressByRecordIds(ids).then((r) => {
|
|
82
|
-
r.data.forEach((p) => {
|
|
83
|
-
result[p.id] = {
|
|
84
|
-
last_update: p.updated_at,
|
|
85
|
-
progress: p.progress_percent,
|
|
86
|
-
status: p.state,
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
return result
|
|
92
|
-
}
|
|
74
|
+
): Promise<Record<string, ProgressSnapshot>> =>
|
|
75
|
+
db.contentProgress
|
|
76
|
+
.getSomeProgressByRecordIds(ids)
|
|
77
|
+
.then((r) => Object.fromEntries(buildSnapshotMap(ids, r.data, (p) => p.id, (p) => p.updated_at)))
|
|
93
78
|
|
|
94
79
|
export const methodAccessedIds = async (contentIds: number[]): Promise<number[]> =>
|
|
95
80
|
db.contentProgress
|
|
@@ -148,6 +148,7 @@ export async function fetchRechargeTokens(): Promise<RechargeTokens> {
|
|
|
148
148
|
* Upgrades the user's subscription or provides a prefilled add-to-cart URL.
|
|
149
149
|
*
|
|
150
150
|
* @param {boolean} featureFlag - MultiUserAccount feature Flag - default false
|
|
151
|
+
* @param {string} sku- product sku to upgrade to, required if feature flag is set
|
|
151
152
|
*
|
|
152
153
|
* @returns {Promise<UpgradeSubscriptionResponse>} A promise that resolves to an object containing either:
|
|
153
154
|
* - {string} action - The action performed (e.g., 'instant_upgrade').
|
|
@@ -163,10 +164,11 @@ export async function fetchRechargeTokens(): Promise<RechargeTokens> {
|
|
|
163
164
|
* .then(response => console.log(response))
|
|
164
165
|
* .catch(error => console.error(error));
|
|
165
166
|
*/
|
|
166
|
-
export async function upgradeSubscription(featureFlag = false): Promise<UpgradeSubscriptionResponse> {
|
|
167
|
+
export async function upgradeSubscription(featureFlag = false, sku): Promise<UpgradeSubscriptionResponse> {
|
|
167
168
|
let featureFlagValue = featureFlag ? 1 : 0
|
|
169
|
+
let skuValue = sku ? `&sku=${sku}` : ''
|
|
168
170
|
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
169
|
-
return httpClient.get<UpgradeSubscriptionResponse>(`${baseUrl}/v1/update-subscription?${multiUserAccountFeatureFlag}=${featureFlagValue}`)
|
|
171
|
+
return httpClient.get<UpgradeSubscriptionResponse>(`${baseUrl}/v1/update-subscription?${multiUserAccountFeatureFlag}=${featureFlagValue}${skuValue}`)
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
/**
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npx jest *)",
|
|
5
|
-
"Bash(npx tsc *)",
|
|
6
|
-
"Skill(counselors)",
|
|
7
|
-
"Bash(counselors ls *)",
|
|
8
|
-
"Bash(counselors groups *)",
|
|
9
|
-
"Bash(counselors run *)",
|
|
10
|
-
"Bash(npm test *)",
|
|
11
|
-
"Bash(gh pr *)",
|
|
12
|
-
"Bash(gh api *)",
|
|
13
|
-
"Bash(mkdir -p /tmp/pr-review-v2)",
|
|
14
|
-
"Read(//tmp/pr-review-v2/**)",
|
|
15
|
-
"Bash(cat /home/alesevero/railenvironment/applications/musora-content-services/AGENTS.md)",
|
|
16
|
-
"Bash(echo \"no AGENTS.md\")",
|
|
17
|
-
"Bash(echo \"exit=$?\")",
|
|
18
|
-
"Bash(git checkout *)",
|
|
19
|
-
"Skill(pr)",
|
|
20
|
-
"Skill(create-decision)",
|
|
21
|
-
"mcp__github__pull_request_read",
|
|
22
|
-
"mcp__github__push_files",
|
|
23
|
-
"Bash(git push *)",
|
|
24
|
-
"mcp__github__create_pull_request",
|
|
25
|
-
"mcp__github__search_pull_requests",
|
|
26
|
-
"Bash(git commit *)",
|
|
27
|
-
"Bash(git add *)"
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
"model": "sonnet",
|
|
31
|
-
"enabledMcpjsonServers": [
|
|
32
|
-
"atlassian",
|
|
33
|
-
"figma",
|
|
34
|
-
"google-workspace",
|
|
35
|
-
"snowflake",
|
|
36
|
-
"aws",
|
|
37
|
-
"hex",
|
|
38
|
-
"sanity",
|
|
39
|
-
"mysql",
|
|
40
|
-
"slack",
|
|
41
|
-
"langfuse",
|
|
42
|
-
"chrome-devtools",
|
|
43
|
-
"railway",
|
|
44
|
-
"github",
|
|
45
|
-
"asana"
|
|
46
|
-
],
|
|
47
|
-
"disabledMcpjsonServers": [
|
|
48
|
-
"nightwatch",
|
|
49
|
-
"1password"
|
|
50
|
-
],
|
|
51
|
-
"effortLevel": "medium"
|
|
52
|
-
}
|