musora-content-services 2.184.1 → 2.185.1
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-10-01-agi-remove-limit-from-queries.md +60 -0
- package/.claude/settings.local.json +25 -0
- package/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +16 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +9 -0
- package/src/services/content/artist.ts +4 -3
- package/src/services/content/genre.ts +4 -3
- package/src/services/content/instructor.ts +5 -4
- package/src/services/contentAggregator.js +15 -13
- package/src/services/my-path/dev/map-types.ts +1 -1
- package/src/services/my-path/learning-paths.ts +6 -3
- package/src/services/my-path/placement-quiz.ts +1 -0
- package/src/services/my-path/preroll.ts +35 -0
- package/src/services/sanity.js +4 -0
- package/src/services/user/account.ts +23 -0
- package/src/version-info.js +1 -1
- package/.idea/codeStyles/Project.xml +0 -61
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/modules.xml +0 -8
- package/.idea/musora-content-services.iml +0 -8
- package/.idea/php.xml +0 -19
- package/.idea/prettier.xml +0 -6
- package/.idea/vcs.xml +0 -7
- package/.yarn/install-state.gz +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: 2026-10-01
|
|
3
|
+
branch: chore/AGI-remove-limit-from-queries
|
|
4
|
+
pr: https://github.com/railroadmedia/musora-content-services/pull/1067
|
|
5
|
+
status: open
|
|
6
|
+
tags: [[chore]]
|
|
7
|
+
related: [[2026-07-20-query-builder-tostring]]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Optional limit in fetchArtists, fetchGenres and fetchInstructors
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
`fetchArtists`, `fetchGenres` and `fetchInstructors` destructured their options with a hard-coded `limit = 20`. Callers that wanted the complete list of artists, genres or instructors had no way to express that — the only workaround was to pass an arbitrarily large number. The requirement was to make `limit` genuinely optional, with omission meaning "return everything".
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
Removed the `limit = 20` default from all three services and made the `.slice()` call conditional:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const { sort = 'lower(name)', offset = 0, limit } = options
|
|
20
|
+
|
|
21
|
+
const data = query()
|
|
22
|
+
.and(type)
|
|
23
|
+
.order(getSortOrder(sort, brand))
|
|
24
|
+
.select(...)
|
|
25
|
+
.postFilter(postFilter)
|
|
26
|
+
|
|
27
|
+
if (limit) data.slice(offset, limit)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Simply dropping the default was not sufficient and would have shipped a silent breaking change. `QueryBuilder.slice(offset, limit)` in `src/lib/sanity/query.ts` computes its expression as:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const sliceExpr = !limit ? `[${offset}]` : `[${offset}...${offset + limit}]`
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A missing `limit` therefore produces `[offset]` — a single-index GROQ slice, the same branch `first()` depends on — not an unbounded one. The response shape would have flipped from `{ data: Artist[] }` to `{ data: Artist }`, and TypeScript would not have caught it because the declared return type stays `Artists`.
|
|
37
|
+
|
|
38
|
+
Skipping the `.slice()` call instead leaves `state.slice` at its monoid identity (`''`), so `build()` emits no slice expression and GROQ returns the full array.
|
|
39
|
+
|
|
40
|
+
The guard is placed after the builder chain rather than inside it. This is safe for two reasons, both already covered by `test/unit/lib/query.test.ts`: every builder method returns the same closure instance and mutates shared state (`returns same builder instance for chaining`, `builds valid query after multiple modifications`), and `build()` renders state in a fixed order regardless of call order, so the slice still lands last in the emitted query.
|
|
41
|
+
|
|
42
|
+
## Alternatives Considered
|
|
43
|
+
**Fixing `slice()` itself** to treat a nullish `limit` as "no slice" is the more correct fix — `!limit` is the wrong test, since `limit: 0` also falls through to the single-index branch. Rejected for now because `first()` is implemented as `this.slice()` and relies on that exact branch, making it a wider change than this ticket warranted.
|
|
44
|
+
|
|
45
|
+
**Honouring `offset` without a `limit`** via a sentinel ceiling (`data.slice(offset, MAX_RESULTS)`). Rejected: GROQ has no open-ended slice syntax (`[5...]` is invalid), so any such value would be arbitrary, and no current caller paginates without a limit.
|
|
46
|
+
|
|
47
|
+
**Adding a `when(condition, apply)` combinator** to `QueryBuilder` to keep the chain fluent. Rejected as new public API for a single call site. An audit of the other conditional clauses in these queries showed none would adopt it: `searchMatch`, `includedFields` and `progressIds` already return `Filters.empty` when their input is absent, and the `and`/`or` monoids drop empty strings, so conditionality is handled at the filter layer rather than the builder layer.
|
|
48
|
+
|
|
49
|
+
## Process Notes
|
|
50
|
+
The `f.brand(brand)` helper is the one filter that does not self-nullify, which is why four sites spell out `brand ? f.brand(brand) : f.empty` (`counts.ts:26`, `:33`, `:58` and `filter.ts:481`). Making it return `Filters.empty` on a falsy brand would remove those ternaries and match the surrounding idiom, but it was deliberately left alone: an absent brand filter does not return nothing, it returns every brand's content. The required `brand: string` signature currently blocks that mistake at compile time, and the explicit ternary keeps "all brands" a conscious decision at the call site.
|
|
51
|
+
|
|
52
|
+
The post-chain guard only works because builders are interpolated lazily. `data` is a `QueryBuilder`, not a string; the `q` template literal coerces it via the `toString()` added in [[2026-07-20-query-builder-tostring]], which runs below the guard. Had `build()` been called eagerly at the end of the chain, the conditional slice would have been dropped.
|
|
53
|
+
|
|
54
|
+
## Consequences
|
|
55
|
+
- Omitting `limit` now returns the complete list instead of the first 20 rows.
|
|
56
|
+
- `limit: 0` previously produced a single-object response through the faulty `!limit` branch; it now returns the full list.
|
|
57
|
+
- An `offset` passed without a `limit` is ignored rather than approximated.
|
|
58
|
+
- No caller changes required. Both existing consumers pass an explicit limit: `MusoraApp/src_v2/components/screens/CollectionList.tsx` uses `limit: 50`, and `musora-platform-frontend/src/modules/agi/services/entity.service.ts` defaults `fetchAll` to 40.
|
|
59
|
+
- Unbounded queries are now expressible against Sanity, so a caller omitting `limit` on a large brand will fetch every matching document. Worth watching if a new consumer adopts it on a hot path.
|
|
60
|
+
- Left unaddressed: the `Artists`, `Genres` and `Instructors` interfaces declare `total: number`, but the query only projects `"data"`. `total` is always `undefined`, so the pagination meta in `entity.service.ts` never populates.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"enabledMcpjsonServers": [
|
|
3
|
+
"1password",
|
|
4
|
+
"atlassian",
|
|
5
|
+
"figma",
|
|
6
|
+
"google-workspace",
|
|
7
|
+
"snowflake",
|
|
8
|
+
"aws",
|
|
9
|
+
"hex",
|
|
10
|
+
"sanity",
|
|
11
|
+
"mysql",
|
|
12
|
+
"mysql-staging",
|
|
13
|
+
"mysql-local",
|
|
14
|
+
"slack",
|
|
15
|
+
"langfuse",
|
|
16
|
+
"chrome-devtools",
|
|
17
|
+
"mobile",
|
|
18
|
+
"railway",
|
|
19
|
+
"github",
|
|
20
|
+
"asana",
|
|
21
|
+
"nightwatch",
|
|
22
|
+
"sentry",
|
|
23
|
+
"customerio"
|
|
24
|
+
]
|
|
25
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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.185.1](https://github.com/railroadmedia/musora-content-services/compare/v2.185.0...v2.185.1) (2026-10-06)
|
|
6
|
+
|
|
7
|
+
## [2.185.0](https://github.com/railroadmedia/musora-content-services/compare/v2.184.1...v2.185.0) (2026-10-01)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **BEHLTP-380:** preroll LP intros ([#1066](https://github.com/railroadmedia/musora-content-services/issues/1066)) ([4706a1f](https://github.com/railroadmedia/musora-content-services/commit/4706a1f19d15ddfd409a85d2d8ebb1f70e134a99))
|
|
13
|
+
|
|
5
14
|
### [2.184.1](https://github.com/railroadmedia/musora-content-services/compare/v2.184.0...v2.184.1) (2026-09-28)
|
|
6
15
|
|
|
7
16
|
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -549,6 +549,7 @@ export let contentTypeConfig = {
|
|
|
549
549
|
'learning-path-v2': {
|
|
550
550
|
fields: [
|
|
551
551
|
`"intro_video": intro_video->{ ${getIntroVideoFields('learning-path-v2').join(', ')} }`,
|
|
552
|
+
`"preroll": intro_video->{ ${getPrerollFields().join(', ')} }`,
|
|
552
553
|
'total_skills',
|
|
553
554
|
`"resource": ${resourcesField}`,
|
|
554
555
|
],
|
|
@@ -693,6 +694,7 @@ export let contentTypeConfig = {
|
|
|
693
694
|
"thumbnail": thumbnail.asset->url,
|
|
694
695
|
length_in_seconds,
|
|
695
696
|
intro_video,
|
|
697
|
+
"preroll": intro_video,
|
|
696
698
|
child[]->{
|
|
697
699
|
${DEFAULT_FIELDS.join(',')}
|
|
698
700
|
}
|
|
@@ -728,6 +730,20 @@ export function getIntroVideoFields(type) {
|
|
|
728
730
|
return fields
|
|
729
731
|
}
|
|
730
732
|
|
|
733
|
+
export function getPrerollFields() {
|
|
734
|
+
return [
|
|
735
|
+
`"id": railcontent_id`,
|
|
736
|
+
'title',
|
|
737
|
+
'brand',
|
|
738
|
+
`"instructor": ${instructorField}`,
|
|
739
|
+
`"type": _type`,
|
|
740
|
+
`"description": ${descriptionField}`,
|
|
741
|
+
`"thumbnail": thumbnail.asset->url`,
|
|
742
|
+
'length_in_seconds',
|
|
743
|
+
'video',
|
|
744
|
+
]
|
|
745
|
+
}
|
|
746
|
+
|
|
731
747
|
export const plusMembershipPermissions = 92
|
|
732
748
|
|
|
733
749
|
/**
|
package/src/index.d.ts
CHANGED
|
@@ -251,6 +251,11 @@ import {
|
|
|
251
251
|
storeQuizAnswers
|
|
252
252
|
} from './services/my-path/placement-quiz.ts';
|
|
253
253
|
|
|
254
|
+
import {
|
|
255
|
+
completeMyPathIntroVideo,
|
|
256
|
+
completePreroll
|
|
257
|
+
} from './services/my-path/preroll.ts';
|
|
258
|
+
|
|
254
259
|
import {
|
|
255
260
|
deleteAllNotifications,
|
|
256
261
|
deleteNotification,
|
|
@@ -429,6 +434,7 @@ import {
|
|
|
429
434
|
confirmEmailChange,
|
|
430
435
|
createPendingAccount,
|
|
431
436
|
deleteAccount,
|
|
437
|
+
maskEmail,
|
|
432
438
|
numberOfActiveUsers,
|
|
433
439
|
requestEmailChange,
|
|
434
440
|
resetPassword,
|
|
@@ -584,6 +590,8 @@ declare module 'musora-content-services' {
|
|
|
584
590
|
closeComment,
|
|
585
591
|
completeLearningPathIntroVideo,
|
|
586
592
|
completeMethodIntroVideo,
|
|
593
|
+
completeMyPathIntroVideo,
|
|
594
|
+
completePreroll,
|
|
587
595
|
confirmEmailChange,
|
|
588
596
|
contentStatusCompleted,
|
|
589
597
|
contentStatusCompletedMany,
|
|
@@ -844,6 +852,7 @@ declare module 'musora-content-services' {
|
|
|
844
852
|
markNotificationAsRead,
|
|
845
853
|
markNotificationAsUnread,
|
|
846
854
|
markThreadAsRead,
|
|
855
|
+
maskEmail,
|
|
847
856
|
numberOfActiveUsers,
|
|
848
857
|
onProgressSaved,
|
|
849
858
|
openComment,
|
package/src/index.js
CHANGED
|
@@ -255,6 +255,11 @@ import {
|
|
|
255
255
|
storeQuizAnswers
|
|
256
256
|
} from './services/my-path/placement-quiz.ts';
|
|
257
257
|
|
|
258
|
+
import {
|
|
259
|
+
completeMyPathIntroVideo,
|
|
260
|
+
completePreroll
|
|
261
|
+
} from './services/my-path/preroll.ts';
|
|
262
|
+
|
|
258
263
|
import {
|
|
259
264
|
deleteAllNotifications,
|
|
260
265
|
deleteNotification,
|
|
@@ -433,6 +438,7 @@ import {
|
|
|
433
438
|
confirmEmailChange,
|
|
434
439
|
createPendingAccount,
|
|
435
440
|
deleteAccount,
|
|
441
|
+
maskEmail,
|
|
436
442
|
numberOfActiveUsers,
|
|
437
443
|
requestEmailChange,
|
|
438
444
|
resetPassword,
|
|
@@ -583,6 +589,8 @@ export {
|
|
|
583
589
|
closeComment,
|
|
584
590
|
completeLearningPathIntroVideo,
|
|
585
591
|
completeMethodIntroVideo,
|
|
592
|
+
completeMyPathIntroVideo,
|
|
593
|
+
completePreroll,
|
|
586
594
|
confirmEmailChange,
|
|
587
595
|
contentStatusCompleted,
|
|
588
596
|
contentStatusCompletedMany,
|
|
@@ -843,6 +851,7 @@ export {
|
|
|
843
851
|
markNotificationAsRead,
|
|
844
852
|
markNotificationAsUnread,
|
|
845
853
|
markThreadAsRead,
|
|
854
|
+
maskEmail,
|
|
846
855
|
numberOfActiveUsers,
|
|
847
856
|
onProgressSaved,
|
|
848
857
|
openComment,
|
|
@@ -37,12 +37,11 @@ export async function fetchArtists(
|
|
|
37
37
|
): Promise<Artists> {
|
|
38
38
|
const type = f.type('artist')
|
|
39
39
|
const postFilter = `lesson_count > 0`
|
|
40
|
-
const { sort = 'lower(name)', offset = 0, limit
|
|
40
|
+
const { sort = 'lower(name)', offset = 0, limit } = options
|
|
41
41
|
|
|
42
42
|
const data = query()
|
|
43
43
|
.and(type)
|
|
44
44
|
.order(getSortOrder(sort, brand))
|
|
45
|
-
.slice(offset, limit)
|
|
46
45
|
.select(
|
|
47
46
|
'name',
|
|
48
47
|
`"slug": slug.current`,
|
|
@@ -51,6 +50,8 @@ export async function fetchArtists(
|
|
|
51
50
|
)
|
|
52
51
|
.postFilter(postFilter)
|
|
53
52
|
|
|
53
|
+
if (limit) data.slice(offset, limit)
|
|
54
|
+
|
|
54
55
|
const q = `{
|
|
55
56
|
"data": ${data},
|
|
56
57
|
}`
|
|
@@ -140,7 +141,7 @@ export async function fetchArtistLessons(
|
|
|
140
141
|
f.notDeprecated(),
|
|
141
142
|
f.referencesIDWithFilter(f.combine(f.type('artist'), f.slug(slug))),
|
|
142
143
|
f.brand(brand),
|
|
143
|
-
f.permissions({showMembershipRestrictedContent: true}),
|
|
144
|
+
f.permissions({ showMembershipRestrictedContent: true }),
|
|
144
145
|
f.searchMatch('title', searchTerm),
|
|
145
146
|
f.includedFields(includedFields),
|
|
146
147
|
f.progressIds(progressIds)
|
|
@@ -37,12 +37,11 @@ export async function fetchGenres(
|
|
|
37
37
|
): Promise<Genres> {
|
|
38
38
|
const type = f.type('genre')
|
|
39
39
|
const postFilter = `lesson_count > 0`
|
|
40
|
-
const { sort = 'lower(name)', offset = 0, limit
|
|
40
|
+
const { sort = 'lower(name)', offset = 0, limit } = options
|
|
41
41
|
|
|
42
42
|
const data = query()
|
|
43
43
|
.and(type)
|
|
44
44
|
.order(getSortOrder(sort, brand))
|
|
45
|
-
.slice(offset, limit)
|
|
46
45
|
.select(
|
|
47
46
|
'name',
|
|
48
47
|
`"slug": slug.current`,
|
|
@@ -51,6 +50,8 @@ export async function fetchGenres(
|
|
|
51
50
|
)
|
|
52
51
|
.postFilter(postFilter)
|
|
53
52
|
|
|
53
|
+
if (limit) data.slice(offset, limit)
|
|
54
|
+
|
|
54
55
|
const q = `{
|
|
55
56
|
"data": ${data},
|
|
56
57
|
}`
|
|
@@ -139,7 +140,7 @@ export async function fetchGenreLessons(
|
|
|
139
140
|
f.notDeprecated(),
|
|
140
141
|
f.referencesIDWithFilter(f.combine(f.type('genre'), f.slug(slug))),
|
|
141
142
|
f.brand(brand),
|
|
142
|
-
f.permissions({showMembershipRestrictedContent: true}),
|
|
143
|
+
f.permissions({ showMembershipRestrictedContent: true }),
|
|
143
144
|
f.searchMatch('title', searchTerm),
|
|
144
145
|
f.includedFields(includedFields),
|
|
145
146
|
f.progressIds(progressIds)
|
|
@@ -38,12 +38,11 @@ export async function fetchInstructors(
|
|
|
38
38
|
): Promise<Instructors> {
|
|
39
39
|
const type = f.type('instructor')
|
|
40
40
|
const postFilter = `lesson_count > 0`
|
|
41
|
-
const { sort = 'lower(name)', offset = 0, limit
|
|
41
|
+
const { sort = 'lower(name)', offset = 0, limit } = options
|
|
42
42
|
|
|
43
43
|
const data = query()
|
|
44
44
|
.and(type)
|
|
45
45
|
.order(getSortOrder(sort, brand))
|
|
46
|
-
.slice(offset, limit)
|
|
47
46
|
.select(
|
|
48
47
|
'name',
|
|
49
48
|
`"slug": slug.current`,
|
|
@@ -52,6 +51,8 @@ export async function fetchInstructors(
|
|
|
52
51
|
)
|
|
53
52
|
.postFilter(postFilter)
|
|
54
53
|
|
|
54
|
+
if (limit) data.slice(offset, limit)
|
|
55
|
+
|
|
55
56
|
const q = `{
|
|
56
57
|
"data": ${data},
|
|
57
58
|
}`
|
|
@@ -129,7 +130,7 @@ export async function fetchInstructorLessons(
|
|
|
129
130
|
offset = 0,
|
|
130
131
|
limit = 20,
|
|
131
132
|
includedFields = [],
|
|
132
|
-
contentType = null
|
|
133
|
+
contentType = null,
|
|
133
134
|
}: InstructorLessonsOptions = {}
|
|
134
135
|
): Promise<InstructorLessons> {
|
|
135
136
|
sort = getSortOrder(sort, brand)
|
|
@@ -140,7 +141,7 @@ export async function fetchInstructorLessons(
|
|
|
140
141
|
f.notDeprecated(),
|
|
141
142
|
f.referencesIDWithFilter(f.combine(f.type('instructor'), f.slug(slug))),
|
|
142
143
|
f.brand(brand),
|
|
143
|
-
f.permissions({showMembershipRestrictedContent: true}),
|
|
144
|
+
f.permissions({ showMembershipRestrictedContent: true }),
|
|
144
145
|
f.searchMatch('title', searchTerm),
|
|
145
146
|
f.includedFields(includedFields)
|
|
146
147
|
)
|
|
@@ -127,12 +127,12 @@ export async function addContextToContent(dataPromise, ...dataArgs) {
|
|
|
127
127
|
* Key behaviors:
|
|
128
128
|
* 1. Enriches all learning paths in a method structure
|
|
129
129
|
* 2. Auto-sets collection for learning-path-v2 items when no collection specified
|
|
130
|
-
* 3. Enriches
|
|
130
|
+
* 3. Enriches LP preroll when dataField_includePreroll is true
|
|
131
131
|
*
|
|
132
132
|
* @param dataPromise - promise or method that provides sanity data
|
|
133
133
|
* @param dataArgs - Arguments to pass to the dataPromise
|
|
134
134
|
* @param options - Same as addContextToContent, plus:
|
|
135
|
-
* @param options.
|
|
135
|
+
* @param options.dataField_includePreroll - If true, adds progress to preroll (and intro_video for FF) field where it exists
|
|
136
136
|
*
|
|
137
137
|
* @returns {Promise<Object | false>} - Enriched data or false if no data found
|
|
138
138
|
*
|
|
@@ -140,7 +140,7 @@ export async function addContextToContent(dataPromise, ...dataArgs) {
|
|
|
140
140
|
* // Enrich method structure with all learning paths
|
|
141
141
|
* const method = await addContextToMethodContent(fetchMethodV2Structure, brand, {
|
|
142
142
|
* dataField: 'learningPaths',
|
|
143
|
-
*
|
|
143
|
+
* dataField_includePreroll: true,
|
|
144
144
|
* addProgressStatus: true,
|
|
145
145
|
* addProgressPercentage: true,
|
|
146
146
|
* })
|
|
@@ -151,7 +151,7 @@ export async function addContextToContent(dataPromise, ...dataArgs) {
|
|
|
151
151
|
* collection: { id: lpId, type: 'learning-path-v2' },
|
|
152
152
|
* dataField: 'children',
|
|
153
153
|
* dataField_includeParent: true,
|
|
154
|
-
*
|
|
154
|
+
* dataField_includePreroll: true,
|
|
155
155
|
* addProgressStatus: true,
|
|
156
156
|
* })
|
|
157
157
|
*/
|
|
@@ -163,7 +163,7 @@ export async function addContextToLearningPaths(dataPromise, ...dataArgs) {
|
|
|
163
163
|
const {
|
|
164
164
|
dataField = null,
|
|
165
165
|
dataField_includeParent = false,
|
|
166
|
-
|
|
166
|
+
dataField_includePreroll = false,
|
|
167
167
|
addProgressPercentage = false,
|
|
168
168
|
addProgressStatus = false,
|
|
169
169
|
addProgressTimestamp = false,
|
|
@@ -175,8 +175,9 @@ export async function addContextToLearningPaths(dataPromise, ...dataArgs) {
|
|
|
175
175
|
} = options
|
|
176
176
|
|
|
177
177
|
let dataFields = dataField ? [dataField] : []
|
|
178
|
-
if (
|
|
179
|
-
dataFields.push('
|
|
178
|
+
if (dataField_includePreroll) {
|
|
179
|
+
dataFields.push('preroll')
|
|
180
|
+
dataFields.push('intro_video') // todo: remove intro_video things when My Path feature flag is removed.
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
|
|
@@ -187,7 +188,7 @@ export async function addContextToLearningPaths(dataPromise, ...dataArgs) {
|
|
|
187
188
|
if (!data) return false
|
|
188
189
|
|
|
189
190
|
let items, recordIds
|
|
190
|
-
[data, items, recordIds] = addRecordIdsToData(data, dataField, isDataAnArray, dataField_includeParent,
|
|
191
|
+
[data, items, recordIds] = addRecordIdsToData(data, dataField, isDataAnArray, dataField_includeParent, dataField_includePreroll) ?? []
|
|
191
192
|
if (items.length === 0) return data
|
|
192
193
|
|
|
193
194
|
const ids = recordIds.map(item => extractFromRecordId(item).contentId)
|
|
@@ -304,7 +305,7 @@ function extractItemsFromData(data, dataField, isParentArray, includeParent) {
|
|
|
304
305
|
return items
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
function addRecordIdsToData(data, dataField, isDataAnArray, includeParent,
|
|
308
|
+
function addRecordIdsToData(data, dataField, isDataAnArray, includeParent, includePreroll) {
|
|
308
309
|
let items = []
|
|
309
310
|
let recordIds = []
|
|
310
311
|
|
|
@@ -317,10 +318,11 @@ function addRecordIdsToData(data, dataField, isDataAnArray, includeParent, inclu
|
|
|
317
318
|
items.push(content)
|
|
318
319
|
recordIds.push(content.record_id)
|
|
319
320
|
}
|
|
320
|
-
if (
|
|
321
|
-
content.
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
if (includePreroll && content.preroll?.id) {
|
|
322
|
+
content.preroll.record_id = generateRecordId(content.preroll.id, null)
|
|
323
|
+
content.intro_video.record_id = generateRecordId(content.intro_video.id, null) // for My Path feature flag, maintaining Method functionality.
|
|
324
|
+
items.push(content.preroll)
|
|
325
|
+
recordIds.push(content.preroll.record_id)
|
|
324
326
|
}
|
|
325
327
|
if (dataField) {
|
|
326
328
|
for (const child of content[dataField] ?? []) {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
contentStatusReset,
|
|
19
19
|
getAllCompletedByIds,
|
|
20
20
|
getIdsWhereLastAccessedFromMethod,
|
|
21
|
-
getProgressState,
|
|
21
|
+
getProgressState,
|
|
22
22
|
resetStatus,
|
|
23
23
|
} from '../contentProgress.js'
|
|
24
24
|
import { COLLECTION_ID_SELF, COLLECTION_TYPE, CollectionParameter, STATE } from '../sync/models/ContentProgress'
|
|
@@ -254,7 +254,7 @@ export async function getEnrichedLearningPath(learningPathId: number) {
|
|
|
254
254
|
{
|
|
255
255
|
dataField: 'children',
|
|
256
256
|
dataField_includeParent: true,
|
|
257
|
-
|
|
257
|
+
dataField_includePreroll: true,
|
|
258
258
|
addProgressStatus: true,
|
|
259
259
|
addProgressPercentage: true,
|
|
260
260
|
addProgressTimestamp: true,
|
|
@@ -286,7 +286,7 @@ export async function getEnrichedLearningPaths(learningPathIds: number[]) {
|
|
|
286
286
|
{
|
|
287
287
|
dataField: 'children',
|
|
288
288
|
dataField_includeParent: true,
|
|
289
|
-
|
|
289
|
+
dataField_includePreroll: true,
|
|
290
290
|
addProgressStatus: true,
|
|
291
291
|
addProgressPercentage: true,
|
|
292
292
|
addProgressTimestamp: true,
|
|
@@ -484,6 +484,7 @@ export async function fetchLearningPathLessons(
|
|
|
484
484
|
*
|
|
485
485
|
* @param {number[]} contentIds The array of content IDs within the learning path
|
|
486
486
|
* @returns {Promise<number[]>} Array with completed content IDs
|
|
487
|
+
* @deprecated Only use for Method behaviour. Learning path progress import is removed in My Path. Kept for backwards compatibility for Method.
|
|
487
488
|
*/
|
|
488
489
|
export async function fetchLearningPathProgressCheckLessons(
|
|
489
490
|
contentIds: number[],
|
|
@@ -505,6 +506,7 @@ interface completeMethodIntroVideo {
|
|
|
505
506
|
* @returns {Promise<Array>} response - The response object.
|
|
506
507
|
* @returns {Promise<Object|null>} response.intro_video_response - The intro video completion response or null if already completed.
|
|
507
508
|
* @returns {Promise<Object>} response.active_path_response - The set active learning path response.
|
|
509
|
+
* @deprecated Only use for Method behaviour. For My Path, call `completeMyPathIntroVideo` instead.
|
|
508
510
|
*/
|
|
509
511
|
export async function completeMethodIntroVideo(
|
|
510
512
|
introVideoId: number | null,
|
|
@@ -563,6 +565,7 @@ interface completeLearningPathIntroVideo {
|
|
|
563
565
|
* @returns {Promise<void>} response.learning_path_reset_response - The reset learning path response.
|
|
564
566
|
* @returns {Promise<Object[]>} response.lesson_import_response - The responses for completing each content_id within the learning path.
|
|
565
567
|
* @returns {Promise<Object|null>} response.update_dailies_response - The updated daily session if it was changed.
|
|
568
|
+
* @deprecated Only use for Method behaviour. For My Path, use `completePreroll` from `my-path/preroll.ts`.
|
|
566
569
|
*/
|
|
567
570
|
export async function completeLearningPathIntroVideo(
|
|
568
571
|
introVideoId: number,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module MyPathPreroll
|
|
3
|
+
*/
|
|
4
|
+
import { contentStatusCompleted, getProgressState } from '../contentProgress.js'
|
|
5
|
+
import { SyncWriteDTO } from '../sync'
|
|
6
|
+
import { ContentProgress } from '../sync/models'
|
|
7
|
+
import { STATE } from '../sync/models/ContentProgress'
|
|
8
|
+
import { fetchQuizAnswers } from './placement-quiz'
|
|
9
|
+
|
|
10
|
+
export async function completePreroll(
|
|
11
|
+
prerollId: number
|
|
12
|
+
): Promise<SyncWriteDTO<ContentProgress, any> | null> {
|
|
13
|
+
return await completeIfNotCompleted(prerollId)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface completeMyPathIntroVideo {
|
|
17
|
+
placement_quiz: any
|
|
18
|
+
recommended_content: any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function completeMyPathIntroVideo(
|
|
22
|
+
introVideoId: number,
|
|
23
|
+
brand: string,
|
|
24
|
+
): Promise<any> {
|
|
25
|
+
completeIfNotCompleted(introVideoId) // no need to wait for result
|
|
26
|
+
|
|
27
|
+
return await fetchQuizAnswers(brand)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function completeIfNotCompleted(
|
|
31
|
+
contentId: number,
|
|
32
|
+
): Promise<SyncWriteDTO<ContentProgress, any> | null> {
|
|
33
|
+
const status = await getProgressState(contentId)
|
|
34
|
+
return status !== STATE.COMPLETED ? await contentStatusCompleted(contentId) : null
|
|
35
|
+
}
|
package/src/services/sanity.js
CHANGED
|
@@ -2326,6 +2326,7 @@ export async function fetchMethodV2IntroVideo(brand) {
|
|
|
2326
2326
|
* Fetch the structure (just ids) of the Method for a given brand.
|
|
2327
2327
|
* @param brand
|
|
2328
2328
|
* @returns {Promise<*|null>}
|
|
2329
|
+
* @deprecated Kept for backwards compatibility for Method.
|
|
2329
2330
|
*/
|
|
2330
2331
|
export async function fetchMethodV2Structure(brand) {
|
|
2331
2332
|
const _type = 'method-v2'
|
|
@@ -2338,6 +2339,7 @@ export async function fetchMethodV2Structure(brand) {
|
|
|
2338
2339
|
status,
|
|
2339
2340
|
published_on,
|
|
2340
2341
|
'intro_video_id': intro_video->railcontent_id,
|
|
2342
|
+
'preroll_id': intro_video->railcontent_id,
|
|
2341
2343
|
'children': child[]->railcontent_id
|
|
2342
2344
|
}
|
|
2343
2345
|
}`
|
|
@@ -2356,6 +2358,7 @@ export async function devFetchAllLearningPathsAndIntroVideoIdsForDelete() {
|
|
|
2356
2358
|
* Fetch the structure (just ids) of the Method of a given learning path or learning path lesson.
|
|
2357
2359
|
* @param contentId
|
|
2358
2360
|
* @returns {Promise<*|null>}
|
|
2361
|
+
* @deprecated Kept for backwards compatibility for Method.
|
|
2359
2362
|
*/
|
|
2360
2363
|
export async function fetchMethodV2StructureFromId(contentId) {
|
|
2361
2364
|
const _type = 'method-v2'
|
|
@@ -2366,6 +2369,7 @@ export async function fetchMethodV2StructureFromId(contentId) {
|
|
|
2366
2369
|
'learning_paths': child[]->{
|
|
2367
2370
|
'id': railcontent_id,
|
|
2368
2371
|
'intro_video_id': intro_video->railcontent_id,
|
|
2372
|
+
'preroll_id': intro_video->railcontent_id,
|
|
2369
2373
|
'children': child[]->railcontent_id
|
|
2370
2374
|
}
|
|
2371
2375
|
}`
|
|
@@ -9,6 +9,8 @@ import { Onboarding } from './onboarding'
|
|
|
9
9
|
import { OAuthProvider } from './session'
|
|
10
10
|
import { AuthResponse } from './types'
|
|
11
11
|
|
|
12
|
+
const LOCAL_PART_MASK = '***'
|
|
13
|
+
|
|
12
14
|
export interface AccountStatus {
|
|
13
15
|
requires_setup: boolean
|
|
14
16
|
last_login_provider?: OAuthProvider
|
|
@@ -256,3 +258,24 @@ export async function toggleStudentView(useStudentView: boolean): Promise<UserRe
|
|
|
256
258
|
const httpClient = new HttpClient(globalConfig.baseUrl, globalConfig.sessionConfig.token)
|
|
257
259
|
return httpClient.patch<UserResource>(apiUrl, { use_student_view: useStudentView })
|
|
258
260
|
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Masks the local part of an email, keeping the domain intact.
|
|
264
|
+
* Shows the first character of the local part unless it's 3 characters or shorter,
|
|
265
|
+
* in which case the whole local part is masked.
|
|
266
|
+
* @example
|
|
267
|
+
* maskEmail('abcd@gmail.com') // 'a***@gmail.com'
|
|
268
|
+
* maskEmail('ab@abc.com') // '***@abc.com'
|
|
269
|
+
*/
|
|
270
|
+
export function maskEmail(email: string): string {
|
|
271
|
+
const atIndex = email.indexOf('@')
|
|
272
|
+
if (atIndex === -1) {
|
|
273
|
+
return email
|
|
274
|
+
}
|
|
275
|
+
const localPart = email.slice(0, atIndex)
|
|
276
|
+
const domain = email.slice(atIndex)
|
|
277
|
+
|
|
278
|
+
return localPart.length <= 3
|
|
279
|
+
? `${LOCAL_PART_MASK}${domain}`
|
|
280
|
+
: `${localPart[0]}${LOCAL_PART_MASK}${domain}`
|
|
281
|
+
}
|
package/src/version-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCS_VERSION = '2.
|
|
1
|
+
export const MCS_VERSION = '2.185.1'
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<component name="ProjectCodeStyleConfiguration">
|
|
2
|
-
<code_scheme name="Project" version="173">
|
|
3
|
-
<HTMLCodeStyleSettings>
|
|
4
|
-
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
|
5
|
-
</HTMLCodeStyleSettings>
|
|
6
|
-
<JSCodeStyleSettings version="0">
|
|
7
|
-
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
|
8
|
-
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
|
9
|
-
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
|
10
|
-
<option name="USE_DOUBLE_QUOTES" value="false" />
|
|
11
|
-
<option name="FORCE_QUOTE_STYlE" value="true" />
|
|
12
|
-
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
|
13
|
-
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
|
14
|
-
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
|
15
|
-
</JSCodeStyleSettings>
|
|
16
|
-
<TypeScriptCodeStyleSettings version="0">
|
|
17
|
-
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
|
18
|
-
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
|
19
|
-
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
|
20
|
-
<option name="USE_DOUBLE_QUOTES" value="false" />
|
|
21
|
-
<option name="FORCE_QUOTE_STYlE" value="true" />
|
|
22
|
-
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
|
23
|
-
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
|
24
|
-
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
|
25
|
-
</TypeScriptCodeStyleSettings>
|
|
26
|
-
<VueCodeStyleSettings>
|
|
27
|
-
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
|
28
|
-
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
|
29
|
-
</VueCodeStyleSettings>
|
|
30
|
-
<codeStyleSettings language="HTML">
|
|
31
|
-
<option name="SOFT_MARGINS" value="100" />
|
|
32
|
-
<indentOptions>
|
|
33
|
-
<option name="INDENT_SIZE" value="2" />
|
|
34
|
-
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
|
35
|
-
<option name="TAB_SIZE" value="2" />
|
|
36
|
-
</indentOptions>
|
|
37
|
-
</codeStyleSettings>
|
|
38
|
-
<codeStyleSettings language="JavaScript">
|
|
39
|
-
<option name="SOFT_MARGINS" value="100" />
|
|
40
|
-
<indentOptions>
|
|
41
|
-
<option name="INDENT_SIZE" value="2" />
|
|
42
|
-
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
|
43
|
-
<option name="TAB_SIZE" value="2" />
|
|
44
|
-
</indentOptions>
|
|
45
|
-
</codeStyleSettings>
|
|
46
|
-
<codeStyleSettings language="TypeScript">
|
|
47
|
-
<option name="SOFT_MARGINS" value="100" />
|
|
48
|
-
<indentOptions>
|
|
49
|
-
<option name="INDENT_SIZE" value="2" />
|
|
50
|
-
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
|
51
|
-
<option name="TAB_SIZE" value="2" />
|
|
52
|
-
</indentOptions>
|
|
53
|
-
</codeStyleSettings>
|
|
54
|
-
<codeStyleSettings language="Vue">
|
|
55
|
-
<option name="SOFT_MARGINS" value="100" />
|
|
56
|
-
<indentOptions>
|
|
57
|
-
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
|
58
|
-
</indentOptions>
|
|
59
|
-
</codeStyleSettings>
|
|
60
|
-
</code_scheme>
|
|
61
|
-
</component>
|
package/.idea/modules.xml
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="ProjectModuleManager">
|
|
4
|
-
<modules>
|
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/musora-content-services.iml" filepath="$PROJECT_DIR$/.idea/musora-content-services.iml" />
|
|
6
|
-
</modules>
|
|
7
|
-
</component>
|
|
8
|
-
</project>
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<module type="WEB_MODULE" version="4">
|
|
3
|
-
<component name="NewModuleRootManager">
|
|
4
|
-
<content url="file://$MODULE_DIR$" />
|
|
5
|
-
<orderEntry type="inheritedJdk" />
|
|
6
|
-
<orderEntry type="sourceFolder" forTests="false" />
|
|
7
|
-
</component>
|
|
8
|
-
</module>
|
package/.idea/php.xml
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="MessDetectorOptionsConfiguration">
|
|
4
|
-
<option name="transferred" value="true" />
|
|
5
|
-
</component>
|
|
6
|
-
<component name="PHPCSFixerOptionsConfiguration">
|
|
7
|
-
<option name="transferred" value="true" />
|
|
8
|
-
</component>
|
|
9
|
-
<component name="PHPCodeSnifferOptionsConfiguration">
|
|
10
|
-
<option name="highlightLevel" value="WARNING" />
|
|
11
|
-
<option name="transferred" value="true" />
|
|
12
|
-
</component>
|
|
13
|
-
<component name="PhpStanOptionsConfiguration">
|
|
14
|
-
<option name="transferred" value="true" />
|
|
15
|
-
</component>
|
|
16
|
-
<component name="PsalmOptionsConfiguration">
|
|
17
|
-
<option name="transferred" value="true" />
|
|
18
|
-
</component>
|
|
19
|
-
</project>
|
package/.idea/prettier.xml
DELETED
package/.idea/vcs.xml
DELETED
package/.yarn/install-state.gz
DELETED
|
Binary file
|