musora-content-services 2.131.2 → 2.131.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +16 -10
- package/src/services/awards/award-query.js +13 -15
- package/src/services/progress-row/base.js +2 -2
- package/src/services/progress-row/rows/content-card.js +2 -2
- package/src/services/progress-row/rows/playlist-card.js +2 -2
- package/src/services/sanity.js +2 -2
- package/src/services/sync/errors/validators.ts +9 -1
- package/src/services/sync/models/ContentLike.ts +5 -0
- package/src/services/sync/models/ContentProgress.ts +60 -29
- package/src/services/sync/models/Practice.ts +64 -23
- package/src/services/sync/repositories/content-likes.ts +4 -8
- package/src/services/sync/repositories/content-progress.ts +5 -15
- package/src/services/sync/repositories/practices.ts +3 -10
- package/src/services/sync/store/index.ts +36 -54
- package/src/services/sync/telemetry/index.ts +1 -2
- package/src/services/sync/telemetry/sampling.ts +2 -2
- package/src/services/user/onboarding.ts +1 -1
- package/src/services/userActivity.js +3 -3
- package/.claude/settings.local.json +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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.131.4](https://github.com/railroadmedia/musora-content-services/compare/v2.131.3...v2.131.4) (2026-02-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* hide badge field if is_active false ([#773](https://github.com/railroadmedia/musora-content-services/issues/773)) ([d8d9159](https://github.com/railroadmedia/musora-content-services/commit/d8d9159c5270dac8ea1de92634266f9f412b3c7c))
|
|
11
|
+
* **onboarding:** update recommended content interface ([65f5e91](https://github.com/railroadmedia/musora-content-services/commit/65f5e91eea00762ea5ab195ce650eb1b3fa92e77))
|
|
12
|
+
* watermelon validation tweaks ([#770](https://github.com/railroadmedia/musora-content-services/issues/770)) ([ebd6e7f](https://github.com/railroadmedia/musora-content-services/commit/ebd6e7f7c059a23a6126b993b3084744d3d6e78a))
|
|
13
|
+
|
|
14
|
+
### [2.131.3](https://github.com/railroadmedia/musora-content-services/compare/v2.131.2...v2.131.3) (2026-02-04)
|
|
15
|
+
|
|
5
16
|
### [2.131.2](https://github.com/railroadmedia/musora-content-services/compare/v2.131.1...v2.131.2) (2026-02-04)
|
|
6
17
|
|
|
7
18
|
### [2.131.1](https://github.com/railroadmedia/musora-content-services/compare/v2.131.0...v2.131.1) (2026-02-04)
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -1018,24 +1018,30 @@ export const awardTemplate = {
|
|
|
1018
1018
|
* @param {string} brand - brand for if content brand is missing
|
|
1019
1019
|
* @returns {object|object[]} post-processed content
|
|
1020
1020
|
*/
|
|
1021
|
-
export function
|
|
1021
|
+
export function postProcessBadge(content, brand = null) {
|
|
1022
1022
|
if (!content) return content
|
|
1023
1023
|
|
|
1024
1024
|
// should be fine with this; children don't need awards.
|
|
1025
1025
|
// assumes if badge_logo exists, it needs a badge_template.
|
|
1026
1026
|
if (Array.isArray(content)) {
|
|
1027
|
-
content.
|
|
1028
|
-
|
|
1029
|
-
item['badge_template'] = awardTemplate[item['brand'] || brand].front
|
|
1030
|
-
item['badge_template_rear'] = awardTemplate[item['brand'] || brand].rear
|
|
1031
|
-
}
|
|
1027
|
+
content.map((item) => {
|
|
1028
|
+
return process(item)
|
|
1032
1029
|
})
|
|
1033
1030
|
} else {
|
|
1034
|
-
|
|
1035
|
-
content['badge_template'] = awardTemplate[content['brand'] || brand].front
|
|
1036
|
-
content['badge_template_rear'] = awardTemplate[content['brand'] || brand].rear
|
|
1037
|
-
}
|
|
1031
|
+
content = process(content)
|
|
1038
1032
|
}
|
|
1039
1033
|
|
|
1040
1034
|
return content
|
|
1035
|
+
|
|
1036
|
+
function process(item) {
|
|
1037
|
+
if (!item['is_active']) {
|
|
1038
|
+
item['badge'] = null
|
|
1039
|
+
item['badge_rear'] = null
|
|
1040
|
+
}
|
|
1041
|
+
if (item['badge_logo'] && !item['badge_template']) {
|
|
1042
|
+
item['badge_template'] = awardTemplate[item['brand'] || brand].front
|
|
1043
|
+
item['badge_template_rear'] = awardTemplate[item['brand'] || brand].rear
|
|
1044
|
+
}
|
|
1045
|
+
return item
|
|
1046
|
+
}
|
|
1041
1047
|
}
|
|
@@ -223,11 +223,7 @@ function defineAwards(data) {
|
|
|
223
223
|
return {
|
|
224
224
|
awardId: def._id,
|
|
225
225
|
awardTitle: def.name,
|
|
226
|
-
|
|
227
|
-
badge_rear: def.badge_rear,
|
|
228
|
-
badge_logo: def.logo,
|
|
229
|
-
badge_template: awardTemplate[def.brand].front,
|
|
230
|
-
badge_template_rear: awardTemplate[def.brand].rear,
|
|
226
|
+
...getBadgeFields(def),
|
|
231
227
|
award: def.award,
|
|
232
228
|
brand: def.brand,
|
|
233
229
|
instructorName: def.instructor_name,
|
|
@@ -329,11 +325,7 @@ export async function getCompletedAwards(brand = null, options = {}) {
|
|
|
329
325
|
awardId: progress.award_id,
|
|
330
326
|
awardTitle: definition.name,
|
|
331
327
|
awardType: definition.type,
|
|
332
|
-
|
|
333
|
-
badge_rear: definition.badge_rear,
|
|
334
|
-
badge_logo: definition.logo,
|
|
335
|
-
badge_template: awardTemplate[definition.brand].front,
|
|
336
|
-
badge_template_rear: awardTemplate[definition.brand].rear,
|
|
328
|
+
...getBadgeFields(definition),
|
|
337
329
|
award: definition.award,
|
|
338
330
|
brand: definition.brand,
|
|
339
331
|
hasCertificate: hasCertificate,
|
|
@@ -454,11 +446,7 @@ export async function getInProgressAwards(brand = null, options = {}) {
|
|
|
454
446
|
return {
|
|
455
447
|
awardId: progress.award_id,
|
|
456
448
|
awardTitle: definition.name,
|
|
457
|
-
|
|
458
|
-
badge_rear: definition.badge_rear,
|
|
459
|
-
badge_logo: definition.logo,
|
|
460
|
-
badge_template: awardTemplate[definition.brand].front,
|
|
461
|
-
badge_template_rear: awardTemplate[definition.brand].rear,
|
|
449
|
+
...getBadgeFields(definition),
|
|
462
450
|
award: definition.award,
|
|
463
451
|
brand: definition.brand,
|
|
464
452
|
instructorName: definition.instructor_name,
|
|
@@ -593,3 +581,13 @@ export async function resetAllAwards() {
|
|
|
593
581
|
return { deletedCount: 0 }
|
|
594
582
|
}
|
|
595
583
|
}
|
|
584
|
+
|
|
585
|
+
function getBadgeFields(def) {
|
|
586
|
+
return {
|
|
587
|
+
badge: def.is_active ? def.badge : null,
|
|
588
|
+
badge_rear: def.is_active ? def.badge_rear : null,
|
|
589
|
+
badge_logo: def.logo,
|
|
590
|
+
badge_template: awardTemplate[def.brand].front,
|
|
591
|
+
badge_template_rear: awardTemplate[def.brand].rear,
|
|
592
|
+
}
|
|
593
|
+
}
|
|
@@ -15,7 +15,7 @@ import { addContextToContent } from '../contentAggregator.js'
|
|
|
15
15
|
import { fetchPlaylist } from '../content-org/playlists.js'
|
|
16
16
|
import { TabResponseType } from '../../contentMetaData.js'
|
|
17
17
|
import { PUT } from '../../infrastructure/http/HttpClient.ts'
|
|
18
|
-
import {
|
|
18
|
+
import { postProcessBadge } from "../../contentTypeConfig.js";
|
|
19
19
|
|
|
20
20
|
export const USER_PIN_PROGRESS_KEY = 'user_pin_progress_row'
|
|
21
21
|
|
|
@@ -142,7 +142,7 @@ async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, meth
|
|
|
142
142
|
} else {
|
|
143
143
|
// we use fetchByRailContentIds so that we don't have the _type restriction in the query
|
|
144
144
|
let data = await fetchByRailContentIds([pinnedId], 'progress-tracker')
|
|
145
|
-
data =
|
|
145
|
+
data = postProcessBadge(data)
|
|
146
146
|
item = await processContentItem(
|
|
147
147
|
await addContextToContent(() => data[0] ?? null, {
|
|
148
148
|
addNextLesson: true,
|
|
@@ -5,7 +5,7 @@ import { getAllStartedOrCompleted, getProgressStateByIds } from '../../contentPr
|
|
|
5
5
|
import { addContextToContent } from '../../contentAggregator.js'
|
|
6
6
|
import { fetchByRailContentIds, fetchShows } from '../../sanity.js'
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
postProcessBadge,
|
|
9
9
|
awardTemplate,
|
|
10
10
|
collectionLessonTypes,
|
|
11
11
|
getFormattedType,
|
|
@@ -46,7 +46,7 @@ export async function getContentCardMap(brand, limit, playlistEngagedOnContent,
|
|
|
46
46
|
}
|
|
47
47
|
)
|
|
48
48
|
: []
|
|
49
|
-
contents =
|
|
49
|
+
contents = postProcessBadge(contents)
|
|
50
50
|
|
|
51
51
|
const contentCards = await Promise.all(generateContentPromises(contents))
|
|
52
52
|
return contentCards.reduce((contentMap, content) => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { fetchUserPlaylists } from '../../content-org/playlists.js'
|
|
5
5
|
import { addContextToContent } from '../../contentAggregator.js'
|
|
6
6
|
import { fetchByRailContentIds } from '../../sanity.js'
|
|
7
|
-
import {
|
|
7
|
+
import { postProcessBadge } from "../../../contentTypeConfig.js";
|
|
8
8
|
|
|
9
9
|
export async function getPlaylistCards(recentPlaylists){
|
|
10
10
|
return await Promise.all(
|
|
@@ -74,6 +74,6 @@ export async function getPlaylistEngagedOnContent(recentPlaylists){
|
|
|
74
74
|
addProgressTimestamp: true,
|
|
75
75
|
})
|
|
76
76
|
: []
|
|
77
|
-
contents =
|
|
77
|
+
contents = postProcessBadge(contents)
|
|
78
78
|
return contents
|
|
79
79
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
SONG_TYPES,
|
|
33
33
|
SONG_TYPES_WITH_CHILDREN,
|
|
34
34
|
liveFields,
|
|
35
|
-
|
|
35
|
+
postProcessBadge,
|
|
36
36
|
contentAwardField,
|
|
37
37
|
} from '../contentTypeConfig.js'
|
|
38
38
|
import { fetchSimilarItems } from './recommendations.js'
|
|
@@ -998,7 +998,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
1000
|
let contents = await fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true })
|
|
1001
|
-
contents =
|
|
1001
|
+
contents = postProcessBadge(contents)
|
|
1002
1002
|
|
|
1003
1003
|
return contents
|
|
1004
1004
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { SyncValidationError } from './index'
|
|
2
2
|
|
|
3
|
+
export function throwIfNotInteger(val: any) {
|
|
4
|
+
if (!Number.isSafeInteger(val)) throw new SyncValidationError('Sync value is not a number: ' + val, typeof val, 'number');
|
|
5
|
+
return val
|
|
6
|
+
}
|
|
3
7
|
|
|
4
8
|
export function throwIfNotNumber(val: any) {
|
|
5
9
|
// note: this will accept decimal values
|
|
6
|
-
if (
|
|
10
|
+
if (!Number.isFinite(val)) throw new SyncValidationError('Sync value is not a number: ' + val, typeof val, 'number');
|
|
7
11
|
return val
|
|
8
12
|
}
|
|
9
13
|
|
|
@@ -17,6 +21,10 @@ export function throwIfNotBoolean(val: any) {
|
|
|
17
21
|
return val
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
export function throwIfNotNullableInteger(val: any) {
|
|
25
|
+
return val === null ? val : throwIfNotInteger(val)
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
export function throwIfNotNullableNumber(val: any) {
|
|
21
29
|
return val === null ? val : throwIfNotNumber(val)
|
|
22
30
|
}
|
|
@@ -14,4 +14,9 @@ export default class ContentLike extends BaseModel<{
|
|
|
14
14
|
set content_id(value: number) {
|
|
15
15
|
this._setRaw('content_id', throwIfNotNumber(value))
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
static generateId(contentId: number) {
|
|
19
|
+
throwIfNotNumber(contentId)
|
|
20
|
+
return contentId.toString()
|
|
21
|
+
}
|
|
17
22
|
}
|
|
@@ -3,9 +3,11 @@ import { SYNC_TABLES } from '../schema'
|
|
|
3
3
|
import {
|
|
4
4
|
throwIfInvalidEnumValue,
|
|
5
5
|
throwIfNotNullableNumber,
|
|
6
|
+
throwIfNotNullableInteger,
|
|
6
7
|
throwIfNotNullableString,
|
|
7
|
-
throwIfNotNumber,
|
|
8
8
|
throwIfOutsideRange,
|
|
9
|
+
throwIfNotInteger,
|
|
10
|
+
throwIfNotNumber
|
|
9
11
|
} from '../errors/validators'
|
|
10
12
|
|
|
11
13
|
export enum COLLECTION_TYPE {
|
|
@@ -19,16 +21,44 @@ export enum STATE {
|
|
|
19
21
|
COMPLETED = 'completed'
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
export interface CollectionParameter {
|
|
25
|
+
type: COLLECTION_TYPE,
|
|
26
|
+
id: number,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const validators = {
|
|
30
|
+
// unsigned int
|
|
31
|
+
content_id: (contentId: number) => {
|
|
32
|
+
throwIfNotNullableInteger(contentId)
|
|
33
|
+
return throwIfOutsideRange(contentId, 0)
|
|
34
|
+
},
|
|
35
|
+
content_brand: (contentBrand: string | null) => {
|
|
36
|
+
return throwIfNotNullableString(contentBrand)
|
|
37
|
+
},
|
|
38
|
+
// tinyint unsigned - IMPORTANT: progress percent only moves forward and is clamped between 0 and 100
|
|
39
|
+
// also has implications for last-write-wins sync strategy
|
|
40
|
+
progress_percent: (value: number, currentPercent: number) => {
|
|
41
|
+
throwIfNotNumber(value)
|
|
42
|
+
throwIfOutsideRange(value, 0, 100)
|
|
43
|
+
return value === 0 ? 0 : Math.max(value, currentPercent)
|
|
44
|
+
},
|
|
45
|
+
// enum collection_type
|
|
46
|
+
collection_type: (collectionType: string) => {
|
|
47
|
+
return throwIfInvalidEnumValue(collectionType, COLLECTION_TYPE) as COLLECTION_TYPE
|
|
48
|
+
},
|
|
49
|
+
// unsigned mediumint 16777215
|
|
50
|
+
collection_id: (collectionId: number) => {
|
|
51
|
+
throwIfNotInteger(collectionId)
|
|
52
|
+
return throwIfOutsideRange(collectionId, 0, 16777215)
|
|
53
|
+
},
|
|
54
|
+
// smallint unsigned
|
|
55
|
+
resume_time_seconds: (value: number | null) => {
|
|
56
|
+
throwIfNotNullableNumber(value)
|
|
57
|
+
return value !== null ? throwIfOutsideRange(value, 0, 65535) : value
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default class ContentProgress extends BaseModel {
|
|
32
62
|
static table = SYNC_TABLES.CONTENT_PROGRESS
|
|
33
63
|
|
|
34
64
|
get content_id() {
|
|
@@ -57,40 +87,41 @@ export default class ContentProgress extends BaseModel<{
|
|
|
57
87
|
}
|
|
58
88
|
|
|
59
89
|
set content_id(value: number) {
|
|
60
|
-
|
|
61
|
-
throwIfNotNumber(value)
|
|
62
|
-
this._setRaw('content_id', throwIfOutsideRange(value, 0))
|
|
90
|
+
this._setRaw('content_id', validators.content_id(value))
|
|
63
91
|
}
|
|
64
92
|
set content_brand(value: string | null) {
|
|
65
|
-
this._setRaw('content_brand',
|
|
93
|
+
this._setRaw('content_brand', validators.content_brand(value))
|
|
66
94
|
}
|
|
67
|
-
// IMPORTANT: progress percent only moves forward and is clamped between 0 and 100
|
|
68
|
-
// also has implications for last-write-wins sync strategy
|
|
69
95
|
set progress_percent(value: number) {
|
|
70
|
-
|
|
71
|
-
throwIfNotNumber(value)
|
|
72
|
-
throwIfOutsideRange(value, 0, 100)
|
|
73
|
-
const percent = value === 0 ? 0 : Math.max(value, this.progress_percent)
|
|
96
|
+
const percent = validators.progress_percent(value, this.progress_percent)
|
|
74
97
|
|
|
75
98
|
this._setRaw('progress_percent', percent)
|
|
76
99
|
this._setRaw('state', percent === 100 ? STATE.COMPLETED : STATE.STARTED)
|
|
77
100
|
}
|
|
78
101
|
set collection_type(value: COLLECTION_TYPE) {
|
|
79
|
-
|
|
80
|
-
this._setRaw('collection_type', throwIfInvalidEnumValue(value, COLLECTION_TYPE))
|
|
102
|
+
this._setRaw('collection_type', validators.collection_type(value))
|
|
81
103
|
}
|
|
82
104
|
set collection_id(value: number) {
|
|
83
|
-
|
|
84
|
-
throwIfNotNumber(value)
|
|
85
|
-
this._setRaw('collection_id', throwIfOutsideRange(value, 0, 16777215))
|
|
105
|
+
this._setRaw('collection_id', validators.collection_id(value))
|
|
86
106
|
}
|
|
87
107
|
set resume_time_seconds(value: number | null) {
|
|
88
|
-
|
|
89
|
-
throwIfNotNullableNumber(value)
|
|
90
|
-
this._setRaw('resume_time_seconds', value !== null ? throwIfOutsideRange(value, 0, 65535) : value)
|
|
108
|
+
this._setRaw('resume_time_seconds', validators.resume_time_seconds(value))
|
|
91
109
|
}
|
|
92
110
|
set last_interacted_a_la_carte(value: number) {
|
|
93
111
|
this._setRaw('last_interacted_a_la_carte', value)
|
|
94
112
|
}
|
|
95
113
|
|
|
114
|
+
static generateId(
|
|
115
|
+
contentId: number,
|
|
116
|
+
collection: CollectionParameter | null
|
|
117
|
+
) {
|
|
118
|
+
validators.content_id(contentId)
|
|
119
|
+
|
|
120
|
+
if (collection !== null) {
|
|
121
|
+
validators.collection_type(collection.type)
|
|
122
|
+
validators.collection_id(collection.id)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return `${contentId}:${collection?.type || COLLECTION_TYPE.SELF}:${collection?.id || COLLECTION_ID_SELF}`
|
|
126
|
+
}
|
|
96
127
|
}
|
|
@@ -10,6 +10,50 @@ import {
|
|
|
10
10
|
throwIfOutsideRange,
|
|
11
11
|
} from '../errors/validators'
|
|
12
12
|
|
|
13
|
+
export const validators = {
|
|
14
|
+
// char(26)
|
|
15
|
+
manual_id: (value: string | null) => {
|
|
16
|
+
throwIfNotNullableString(value)
|
|
17
|
+
return value !== null ? throwIfMaxLengthExceeded(value, 26) : value
|
|
18
|
+
},
|
|
19
|
+
// int unsigned
|
|
20
|
+
content_id: (value: number | null) => {
|
|
21
|
+
throwIfNotNullableNumber(value)
|
|
22
|
+
return value !== null ? throwIfOutsideRange(value, 0) : value
|
|
23
|
+
},
|
|
24
|
+
date: (value: string) => {
|
|
25
|
+
return throwIfNotString(value)
|
|
26
|
+
},
|
|
27
|
+
// tinyint(1)
|
|
28
|
+
auto: (value: boolean) => {
|
|
29
|
+
return throwIfNotBoolean(value)
|
|
30
|
+
},
|
|
31
|
+
duration_seconds: (value: number) => {
|
|
32
|
+
throwIfNotNumber(value)
|
|
33
|
+
return throwIfOutsideRange(value, 0, 59999)
|
|
34
|
+
},
|
|
35
|
+
// varchar(64)
|
|
36
|
+
title: (value: string | null) => {
|
|
37
|
+
throwIfNotNullableString(value)
|
|
38
|
+
return value !== null ? throwIfMaxLengthExceeded(value, 64) : value
|
|
39
|
+
},
|
|
40
|
+
// varchar(255)
|
|
41
|
+
thumbnail_url: (value: string | null) => {
|
|
42
|
+
throwIfNotNullableString(value)
|
|
43
|
+
return value !== null ? throwIfMaxLengthExceeded(value, 255) : value
|
|
44
|
+
},
|
|
45
|
+
// tinyint unsigned
|
|
46
|
+
category_id: (value: number | null) => {
|
|
47
|
+
throwIfNotNullableNumber(value)
|
|
48
|
+
return value !== null ? throwIfOutsideRange(value, 0, 255) : value
|
|
49
|
+
},
|
|
50
|
+
// tinyint unsigned
|
|
51
|
+
instrument_id: (value: number | null) => {
|
|
52
|
+
throwIfNotNullableNumber(value)
|
|
53
|
+
return value !== null ? throwIfOutsideRange(value, 0, 255) : value
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
13
57
|
export default class Practice extends BaseModel<{
|
|
14
58
|
manual_id: string | null
|
|
15
59
|
content_id: number | null
|
|
@@ -52,44 +96,41 @@ export default class Practice extends BaseModel<{
|
|
|
52
96
|
}
|
|
53
97
|
|
|
54
98
|
set manual_id(value: string | null) {
|
|
55
|
-
|
|
56
|
-
throwIfNotNullableString(value)
|
|
57
|
-
this._setRaw('manual_id', value !== null ? throwIfMaxLengthExceeded(value, 26) : value)
|
|
99
|
+
this._setRaw('manual_id', validators.manual_id(value))
|
|
58
100
|
}
|
|
59
101
|
set content_id(value: number | null) {
|
|
60
|
-
|
|
61
|
-
throwIfNotNullableNumber(value)
|
|
62
|
-
this._setRaw('content_id', value !== null ? throwIfOutsideRange(value, 0) : value)
|
|
102
|
+
this._setRaw('content_id', validators.content_id(value))
|
|
63
103
|
}
|
|
64
104
|
set date(value: string) {
|
|
65
|
-
this._setRaw('date',
|
|
105
|
+
this._setRaw('date', validators.date(value))
|
|
66
106
|
}
|
|
67
107
|
set auto(value: boolean) {
|
|
68
|
-
|
|
69
|
-
this._setRaw('auto', throwIfNotBoolean(value))
|
|
108
|
+
this._setRaw('auto', validators.auto(value))
|
|
70
109
|
}
|
|
71
110
|
set duration_seconds(value: number) {
|
|
72
|
-
|
|
73
|
-
this._setRaw('duration_seconds', throwIfOutsideRange(value, 0, 59999))
|
|
111
|
+
this._setRaw('duration_seconds', validators.duration_seconds(value))
|
|
74
112
|
}
|
|
75
113
|
set title(value: string | null) {
|
|
76
|
-
|
|
77
|
-
throwIfNotNullableString(value)
|
|
78
|
-
this._setRaw('title', value !== null ? throwIfMaxLengthExceeded(value, 64) : value)
|
|
114
|
+
this._setRaw('title', validators.title(value))
|
|
79
115
|
}
|
|
80
116
|
set thumbnail_url(value: string | null) {
|
|
81
|
-
|
|
82
|
-
throwIfNotNullableString(value)
|
|
83
|
-
this._setRaw('thumbnail_url', value !== null ? throwIfMaxLengthExceeded(value, 255) : value)
|
|
117
|
+
this._setRaw('thumbnail_url', validators.thumbnail_url(value))
|
|
84
118
|
}
|
|
85
119
|
set category_id(value: number | null) {
|
|
86
|
-
|
|
87
|
-
throwIfNotNullableNumber(value)
|
|
88
|
-
this._setRaw('category_id', value !== null ? throwIfOutsideRange(value, 0, 255) : value)
|
|
120
|
+
this._setRaw('category_id', validators.category_id(value))
|
|
89
121
|
}
|
|
90
122
|
set instrument_id(value: number | null) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
123
|
+
this._setRaw('instrument_id', validators.instrument_id(value))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static generateAutoId(contentId: number, date: string) {
|
|
127
|
+
throwIfNotNumber(contentId)
|
|
128
|
+
throwIfNotString(date)
|
|
129
|
+
return ['auto', contentId.toString(), date].join(':')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static generateManualId(manualId: string) {
|
|
133
|
+
throwIfNotString(manualId)
|
|
134
|
+
return `manual:${manualId}`
|
|
94
135
|
}
|
|
95
136
|
}
|
|
@@ -3,24 +3,20 @@ import ContentLike from "../models/ContentLike";
|
|
|
3
3
|
|
|
4
4
|
export default class LikesRepository extends SyncRepository<ContentLike> {
|
|
5
5
|
async isLiked(contentId: number) {
|
|
6
|
-
return await this.existOne(
|
|
6
|
+
return await this.existOne(ContentLike.generateId(contentId))
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
async areLiked(contentIds: number[]) {
|
|
10
|
-
return await this.existSome(contentIds.map(
|
|
10
|
+
return await this.existSome(contentIds.map(ContentLike.generateId))
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
async like(contentId: number) {
|
|
14
|
-
return await this.upsertOne(
|
|
14
|
+
return await this.upsertOne(ContentLike.generateId(contentId), r => {
|
|
15
15
|
r.content_id = contentId;
|
|
16
16
|
})
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async unlike(contentId: number) {
|
|
20
|
-
return await this.deleteOne(
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private static generateId(contentId: number) {
|
|
24
|
-
return contentId.toString();
|
|
20
|
+
return await this.deleteOne(ContentLike.generateId(contentId))
|
|
25
21
|
}
|
|
26
22
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import SyncRepository, {Q} from './base'
|
|
2
|
-
import ContentProgress, {COLLECTION_ID_SELF, COLLECTION_TYPE, STATE} from '../models/ContentProgress'
|
|
2
|
+
import ContentProgress, {COLLECTION_ID_SELF, COLLECTION_TYPE, STATE, CollectionParameter} from '../models/ContentProgress'
|
|
3
3
|
import {EpochMs} from "../index";
|
|
4
4
|
|
|
5
5
|
interface ContentIdCollectionTuple {
|
|
@@ -7,10 +7,6 @@ interface ContentIdCollectionTuple {
|
|
|
7
7
|
collection: CollectionParameter | null,
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export interface CollectionParameter {
|
|
11
|
-
type: COLLECTION_TYPE,
|
|
12
|
-
id: number,
|
|
13
|
-
}
|
|
14
10
|
export default class ProgressRepository extends SyncRepository<ContentProgress> {
|
|
15
11
|
// null collection only
|
|
16
12
|
async startedIds(limit?: number) {
|
|
@@ -161,7 +157,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
161
157
|
}
|
|
162
158
|
|
|
163
159
|
recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number, {skipPush = false, fromLearningPath = false} = {}) {
|
|
164
|
-
const id =
|
|
160
|
+
const id = ContentProgress.generateId(contentId, collection)
|
|
165
161
|
|
|
166
162
|
if (collection?.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
167
163
|
fromLearningPath = true
|
|
@@ -222,7 +218,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
222
218
|
|
|
223
219
|
const data = Object.fromEntries(
|
|
224
220
|
Object.entries(contentProgresses).map(([contentId, progressPct]) => [
|
|
225
|
-
|
|
221
|
+
ContentProgress.generateId(+contentId, collection),
|
|
226
222
|
(r: ContentProgress) => {
|
|
227
223
|
r.content_id = +contentId
|
|
228
224
|
r.collection_type = collection?.type ?? COLLECTION_TYPE.SELF
|
|
@@ -242,11 +238,11 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
242
238
|
}
|
|
243
239
|
|
|
244
240
|
eraseProgress(contentId: number, collection: CollectionParameter | null, {skipPush = false} = {}) {
|
|
245
|
-
return this.deleteOne(
|
|
241
|
+
return this.deleteOne(ContentProgress.generateId(contentId, collection), { skipPush })
|
|
246
242
|
}
|
|
247
243
|
|
|
248
244
|
eraseProgressMany(contentIds: number[], collection: CollectionParameter | null, {skipPush = false} = {}) {
|
|
249
|
-
const ids = contentIds.map((id) =>
|
|
245
|
+
const ids = contentIds.map((id) => ContentProgress.generateId(id, collection))
|
|
250
246
|
return this.deleteSome(ids, { skipPush })
|
|
251
247
|
}
|
|
252
248
|
|
|
@@ -264,10 +260,4 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
264
260
|
)
|
|
265
261
|
)
|
|
266
262
|
|
|
267
|
-
private static generateId(
|
|
268
|
-
contentId: number,
|
|
269
|
-
collection: CollectionParameter | null
|
|
270
|
-
) {
|
|
271
|
-
return `${contentId}:${collection?.type || COLLECTION_TYPE.SELF}:${collection?.id || COLLECTION_ID_SELF}`
|
|
272
|
-
}
|
|
273
263
|
}
|
|
@@ -19,8 +19,8 @@ export default class PracticesRepository extends SyncRepository<Practice> {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
async trackAutoPractice(contentId: number, date: string, incrementalDurationSeconds: number, skipPush = true) {
|
|
22
|
-
return await this.upsertOne(
|
|
23
|
-
r._raw.id =
|
|
22
|
+
return await this.upsertOne(Practice.generateAutoId(contentId, date), r => {
|
|
23
|
+
r._raw.id = Practice.generateAutoId(contentId, date);
|
|
24
24
|
r.auto = true;
|
|
25
25
|
r.content_id = contentId;
|
|
26
26
|
r.date = date;
|
|
@@ -32,7 +32,7 @@ export default class PracticesRepository extends SyncRepository<Practice> {
|
|
|
32
32
|
async recordManualPractice(date: string, durationSeconds: number, details: Partial<Pick<Practice, 'title' | 'instrument_id' | 'category_id' | 'thumbnail_url'>> = {}) {
|
|
33
33
|
return await this.insertOne((r) => {
|
|
34
34
|
const manualId = r._raw.id; // yoink watermelon's autogenerated id
|
|
35
|
-
r._raw.id =
|
|
35
|
+
r._raw.id = Practice.generateManualId(manualId);
|
|
36
36
|
|
|
37
37
|
r.manual_id = manualId;
|
|
38
38
|
r.auto = false;
|
|
@@ -57,13 +57,6 @@ export default class PracticesRepository extends SyncRepository<Practice> {
|
|
|
57
57
|
})
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
private static generateAutoId(contentId: number, date: string) {
|
|
61
|
-
return ['auto', contentId.toString(), date].join(':');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private static generateManualId(manualId: string) {
|
|
65
|
-
return `manual:${manualId}`;
|
|
66
|
-
}
|
|
67
60
|
|
|
68
61
|
async getAutoPracticesOnDate(date: string) {
|
|
69
62
|
return await this.queryAll(
|
|
@@ -206,23 +206,25 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
206
206
|
return this.queryRecordId(...args)
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
async insertOne(builder: (record: TModel) => void,
|
|
209
|
+
async insertOne(builder: (record: TModel) => void, parentSpan?: Span) {
|
|
210
210
|
return await this.runScope.abortable(async () => {
|
|
211
|
-
const record = await this.paranoidWrite(
|
|
212
|
-
|
|
211
|
+
const record = await this.paranoidWrite(parentSpan, async (writer, span) => {
|
|
212
|
+
const r = await this.collection.create(rec => {
|
|
213
213
|
builder(rec)
|
|
214
214
|
})
|
|
215
|
+
span.setAttribute('records.ids', [r.id])
|
|
216
|
+
return r
|
|
215
217
|
})
|
|
216
218
|
this.emit('upserted', [record])
|
|
217
219
|
|
|
218
|
-
this.pushUnsyncedWithRetry(
|
|
220
|
+
this.pushUnsyncedWithRetry(parentSpan, { type: 'insertOne', recordId: record.id })
|
|
219
221
|
await this.ensurePersistence()
|
|
220
222
|
|
|
221
223
|
return this.modelSerializer.toPlainObject(record)
|
|
222
224
|
})
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
async updateOneId(id: RecordId, builder: (record: TModel) => void,
|
|
227
|
+
async updateOneId(id: RecordId, builder: (record: TModel) => void, parentSpan?: Span) {
|
|
226
228
|
return await this.runScope.abortable(async () => {
|
|
227
229
|
const found = await this.findRecord(id)
|
|
228
230
|
|
|
@@ -230,59 +232,44 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
230
232
|
throw new SyncError('Record not found', { id })
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
const record = await this.paranoidWrite(
|
|
235
|
+
const record = await this.paranoidWrite(parentSpan, async (_writer, span) => {
|
|
236
|
+
span.setAttribute('records.ids', [id])
|
|
234
237
|
return found.update(builder)
|
|
235
238
|
})
|
|
236
239
|
this.emit('upserted', [record])
|
|
237
240
|
|
|
238
|
-
this.pushUnsyncedWithRetry(
|
|
241
|
+
this.pushUnsyncedWithRetry(parentSpan, { type: 'updateOneId', recordIds: record.id })
|
|
239
242
|
await this.ensurePersistence()
|
|
240
243
|
|
|
241
244
|
return this.modelSerializer.toPlainObject(record)
|
|
242
245
|
})
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
async upsertSome(builders: Record<RecordId, (record: TModel) => void>,
|
|
248
|
+
async upsertSome(builders: Record<RecordId, (record: TModel) => void>, parentSpan?: Span, { skipPush = false } = {}) {
|
|
246
249
|
if (Object.keys(builders).length === 0) return []
|
|
247
250
|
|
|
248
251
|
return await this.runScope.abortable(async () => {
|
|
249
252
|
const ids = Object.keys(builders)
|
|
250
253
|
|
|
251
|
-
const records = await this.paranoidWrite(
|
|
252
|
-
|
|
253
|
-
const existingMap = existing.reduce((map, record) => map.set(record.id, record), new Map<RecordId, TModel>())
|
|
254
|
+
const records = await this.paranoidWrite(parentSpan, async (writer, span) => {
|
|
255
|
+
span.setAttribute('records.ids', ids)
|
|
254
256
|
|
|
257
|
+
const existing = await writer.callReader(() => {
|
|
258
|
+
return this.queryMaybeDeletedRecords(Q.where('id', Q.oneOf(ids)))
|
|
259
|
+
})
|
|
260
|
+
const existingMap = existing.reduce((map, record) => map.set(record.id, record), new Map<RecordId, TModel>())
|
|
255
261
|
const destroyedBuilds = []
|
|
256
|
-
const recreateBuilds: Array<{ id: RecordId; created_at: EpochMs; builder: (record: TModel) => void }> = []
|
|
257
262
|
|
|
258
263
|
existing.forEach(record => {
|
|
259
264
|
if (record._raw._status === 'deleted') {
|
|
260
265
|
destroyedBuilds.push(new this.model(this.collection, { id: record.id }).prepareDestroyPermanently())
|
|
261
|
-
} else if (record._raw._status === 'created' && builders[record.id]) {
|
|
262
|
-
// Workaround for WatermelonDB bug: prepareUpdate() doesn't commit field changes
|
|
263
|
-
// for records with _status='created'. Destroy and recreate to ensure updates persist.
|
|
264
|
-
destroyedBuilds.push(new this.model(this.collection, { id: record.id }).prepareDestroyPermanently())
|
|
265
|
-
recreateBuilds.push({
|
|
266
|
-
id: record.id,
|
|
267
|
-
created_at: record._raw.created_at,
|
|
268
|
-
builder: builders[record.id]
|
|
269
|
-
})
|
|
270
266
|
}
|
|
271
267
|
})
|
|
272
268
|
|
|
273
269
|
const newBuilds = Object.entries(builders).map(([id, builder]) => {
|
|
274
270
|
const existing = existingMap.get(id)
|
|
275
|
-
const recreate = recreateBuilds.find(r => r.id === id)
|
|
276
271
|
|
|
277
|
-
if (
|
|
278
|
-
return this.collection.prepareCreate(record => {
|
|
279
|
-
record._raw.id = id
|
|
280
|
-
record._raw.created_at = recreate.created_at as EpochMs
|
|
281
|
-
record._raw.updated_at = this.generateTimestamp()
|
|
282
|
-
record._raw._status = 'created'
|
|
283
|
-
builder(record)
|
|
284
|
-
})
|
|
285
|
-
} else if (existing && existing._raw._status !== 'deleted' && existing._raw._status !== 'created') {
|
|
272
|
+
if (existing && existing._raw._status !== 'deleted') {
|
|
286
273
|
return existing.prepareUpdate(builder)
|
|
287
274
|
} else if (!existing || existing._raw._status === 'deleted') {
|
|
288
275
|
return this.collection.prepareCreate(record => {
|
|
@@ -295,7 +282,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
295
282
|
})
|
|
296
283
|
}
|
|
297
284
|
return null
|
|
298
|
-
}).filter(
|
|
285
|
+
}).filter(build => build !== null)
|
|
299
286
|
|
|
300
287
|
await writer.batch(...destroyedBuilds)
|
|
301
288
|
await writer.batch(...newBuilds)
|
|
@@ -306,7 +293,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
306
293
|
this.emit('upserted', records)
|
|
307
294
|
|
|
308
295
|
if (!skipPush) {
|
|
309
|
-
this.pushUnsyncedWithRetry(
|
|
296
|
+
this.pushUnsyncedWithRetry(parentSpan, { type: 'upsertSome', recordIds: records.map(r => r.id).join(',') })
|
|
310
297
|
}
|
|
311
298
|
await this.ensurePersistence()
|
|
312
299
|
|
|
@@ -329,11 +316,13 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
329
316
|
return this.upsertSomeTentative({ [id]: builder }, span).then(r => r[0])
|
|
330
317
|
}
|
|
331
318
|
|
|
332
|
-
async deleteOne(id: RecordId,
|
|
319
|
+
async deleteOne(id: RecordId, parentSpan?: Span, { skipPush = false } = {}) {
|
|
333
320
|
return await this.runScope.abortable(async () => {
|
|
334
321
|
let record: TModel | null = null
|
|
335
322
|
|
|
336
|
-
await this.paranoidWrite(
|
|
323
|
+
await this.paranoidWrite(parentSpan, async (writer, span) => {
|
|
324
|
+
span.setAttribute('records.ids', [id])
|
|
325
|
+
|
|
337
326
|
const existing = await writer.callReader(() => this.queryMaybeDeletedRecords(Q.where('id', id))).then(
|
|
338
327
|
(records) => records[0] || null
|
|
339
328
|
)
|
|
@@ -355,7 +344,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
355
344
|
this.emit('deleted', [id])
|
|
356
345
|
|
|
357
346
|
if (!skipPush) {
|
|
358
|
-
this.pushUnsyncedWithRetry(
|
|
347
|
+
this.pushUnsyncedWithRetry(parentSpan, { type: 'deleteOne', recordId: id })
|
|
359
348
|
}
|
|
360
349
|
await this.ensurePersistence()
|
|
361
350
|
|
|
@@ -363,9 +352,10 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
363
352
|
})
|
|
364
353
|
}
|
|
365
354
|
|
|
366
|
-
async deleteSome(ids: RecordId[],
|
|
355
|
+
async deleteSome(ids: RecordId[], parentSpan?: Span, { skipPush = false } = {}) {
|
|
367
356
|
return this.runScope.abortable(async () => {
|
|
368
|
-
await this.paranoidWrite(
|
|
357
|
+
await this.paranoidWrite(parentSpan, async (writer, span) => {
|
|
358
|
+
span.setAttribute('records.ids', ids)
|
|
369
359
|
const existing = await this.queryRecords(Q.where('id', Q.oneOf(ids)))
|
|
370
360
|
|
|
371
361
|
await writer.batch(...existing.map(record => record.prepareMarkAsDeleted()))
|
|
@@ -374,7 +364,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
374
364
|
this.emit('deleted', ids)
|
|
375
365
|
|
|
376
366
|
if (!skipPush) {
|
|
377
|
-
this.pushUnsyncedWithRetry(
|
|
367
|
+
this.pushUnsyncedWithRetry(parentSpan, { type: 'deleteSome', recordIds: ids.join(',') })
|
|
378
368
|
}
|
|
379
369
|
await this.ensurePersistence()
|
|
380
370
|
|
|
@@ -386,9 +376,11 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
386
376
|
return this.restoreSome([id], span).then(r => r[0])
|
|
387
377
|
}
|
|
388
378
|
|
|
389
|
-
async restoreSome(ids: RecordId[],
|
|
379
|
+
async restoreSome(ids: RecordId[], parentSpan?: Span) {
|
|
390
380
|
return this.runScope.abortable(async () => {
|
|
391
|
-
const records = await this.paranoidWrite(
|
|
381
|
+
const records = await this.paranoidWrite(parentSpan, async (writer, span) => {
|
|
382
|
+
span.setAttribute('records.ids', ids)
|
|
383
|
+
|
|
392
384
|
const records = await writer.callReader(() => this.queryMaybeDeletedRecords(
|
|
393
385
|
Q.where('id', Q.oneOf(ids)),
|
|
394
386
|
Q.where('_status', 'deleted')
|
|
@@ -410,7 +402,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
410
402
|
|
|
411
403
|
this.emit('upserted', records)
|
|
412
404
|
|
|
413
|
-
this.pushUnsyncedWithRetry(
|
|
405
|
+
this.pushUnsyncedWithRetry(parentSpan, { type: 'restoreSome', recordIds: ids.join(',') })
|
|
414
406
|
await this.ensurePersistence()
|
|
415
407
|
|
|
416
408
|
return records.map((record) => this.modelSerializer.toPlainObject(record))
|
|
@@ -806,7 +798,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
806
798
|
*/
|
|
807
799
|
private paranoidWrite<T>(
|
|
808
800
|
parentSpan: Span | undefined,
|
|
809
|
-
work: (writer: WriterInterface) => Promise<T>
|
|
801
|
+
work: (writer: WriterInterface, span: Span) => Promise<T>
|
|
810
802
|
): Promise<T> {
|
|
811
803
|
const initialId = this.userScope.initialId
|
|
812
804
|
const currentId = this.userScope.getCurrentId()
|
|
@@ -820,17 +812,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
820
812
|
return this.telemetry.trace(
|
|
821
813
|
{ name: `write:${this.model.table}`, op: 'write', parentSpan, attributes: { ...this.context.session.toJSON() } },
|
|
822
814
|
(writeSpan) => {
|
|
823
|
-
return this.db.write(writer =>
|
|
824
|
-
this.telemetry.trace(
|
|
825
|
-
{
|
|
826
|
-
name: `write:generate:${this.model.table}`,
|
|
827
|
-
op: 'write:generate',
|
|
828
|
-
parentSpan: writeSpan,
|
|
829
|
-
attributes: { ...this.context.session.toJSON() },
|
|
830
|
-
},
|
|
831
|
-
() => work(writer)
|
|
832
|
-
)
|
|
833
|
-
)
|
|
815
|
+
return this.db.write(writer => work(writer, writeSpan))
|
|
834
816
|
}
|
|
835
817
|
)
|
|
836
818
|
}
|
|
@@ -93,8 +93,7 @@ export class SyncTelemetry {
|
|
|
93
93
|
op: `${SYNC_TELEMETRY_TRACE_PREFIX}${opts.op}`,
|
|
94
94
|
attributes: {
|
|
95
95
|
...opts.attributes,
|
|
96
|
-
'user.
|
|
97
|
-
'user.currentId': this.userScope.getCurrentId(),
|
|
96
|
+
'user.id': this.userScope.initialId
|
|
98
97
|
},
|
|
99
98
|
}
|
|
100
99
|
return this.Sentry.startSpan<T>(options, (span) => {
|
|
@@ -48,8 +48,8 @@ export const createSyncSentryTracesSampler = (sampleRate = 0.1) => {
|
|
|
48
48
|
return true
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (attributes?.
|
|
52
|
-
return userBucketedSampler(attributes.
|
|
51
|
+
if (attributes?.['user.id']) {
|
|
52
|
+
return userBucketedSampler(attributes['user.id'] as string, sampleRate)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return undefined
|
|
@@ -129,7 +129,7 @@ export interface OnboardingRecommendedContent {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export interface OnboardingRecommendationResponse {
|
|
132
|
-
|
|
132
|
+
recommended_content: OnboardingRecommendedContent
|
|
133
133
|
user_onboarding: Onboarding
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -8,7 +8,7 @@ import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
|
8
8
|
import { fetchByRailContentIds, fetchParentChildRelationshipsFor } from './sanity'
|
|
9
9
|
import { getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
|
|
10
10
|
import { globalConfig } from './config'
|
|
11
|
-
import {
|
|
11
|
+
import { postProcessBadge, getFormattedType } from '../contentTypeConfig'
|
|
12
12
|
import dayjs from 'dayjs'
|
|
13
13
|
import { addContextToContent } from './contentAggregator.js'
|
|
14
14
|
import { db, Q } from './sync'
|
|
@@ -505,7 +505,7 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
|
|
|
505
505
|
addNextLesson: true,
|
|
506
506
|
}
|
|
507
507
|
)
|
|
508
|
-
contents =
|
|
508
|
+
contents = postProcessBadge(contents)
|
|
509
509
|
|
|
510
510
|
contents = await mapContentsThatWereLastProgressedFromMethod(contents)
|
|
511
511
|
|
|
@@ -790,7 +790,7 @@ async function formatPracticeMeta(practices = []) {
|
|
|
790
790
|
addNextLesson: true,
|
|
791
791
|
}
|
|
792
792
|
)
|
|
793
|
-
contents =
|
|
793
|
+
contents = postProcessBadge(contents)
|
|
794
794
|
|
|
795
795
|
contents = await mapContentsThatWereLastProgressedFromMethod(contents)
|
|
796
796
|
|