musora-content-services 2.152.1 → 2.154.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/.github/workflows/automated-testing.yml +24 -0
- package/CHANGELOG.md +29 -0
- package/codecov.yml +5 -0
- package/jest.config.js +39 -8
- package/package.json +5 -1
- package/src/contentTypeConfig.js +13 -14
- package/src/index.d.ts +8 -2
- package/src/index.js +8 -2
- package/src/infrastructure/http/interfaces/RequestOptions.ts +1 -1
- package/src/services/awards/internal/award-definitions.js +3 -3
- package/src/services/content-org/guided-courses.ts +2 -2
- package/src/services/contentProgress.js +35 -54
- package/src/services/dateUtils.js +9 -1
- package/src/services/forums/posts.ts +2 -2
- package/src/services/multi-user-accounts/multi-user-accounts.ts +43 -7
- package/src/services/reporting/reporting.ts +3 -4
- package/src/services/sanity.js +26 -58
- package/src/services/sync/adapters/lokijs.ts +7 -4
- package/src/services/sync/fetch.ts +2 -14
- package/src/services/sync/repositories/base.ts +4 -0
- package/src/services/sync/repositories/content-progress.ts +3 -3
- package/src/services/sync/store/index.ts +6 -1
- package/src/services/sync/strategies/base.ts +1 -1
- package/src/services/sync/telemetry/index.ts +1 -1
- package/src/services/urlBuilder.ts +1 -0
- package/src/services/user/streakCalculator.ts +1 -1
- package/test/SKIPPED_TESTS.md +151 -0
- package/test/initializeTests.js +2 -3
- package/test/{content.test.js → integration/content.test.js} +7 -23
- package/test/integration/contentProgress.test.js +73 -0
- package/test/{forum.test.js → integration/forum.test.js} +2 -4
- package/test/{sanityQueryService.test.js → integration/sanityQueryService.test.js} +143 -291
- package/test/{user → integration/user}/permissions.test.js +5 -4
- package/test/live/README.md +29 -0
- package/test/setupConsole.js +6 -0
- package/test/setupNetworkGuard.js +3 -0
- package/test/{HttpClient.test.js → unit/HttpClient.test.js} +5 -5
- package/test/{awards → unit/awards}/award-alacarte-observer.test.js +13 -12
- package/test/{awards → unit/awards}/award-auto-refresh.test.js +4 -3
- package/test/{awards → unit/awards}/award-calculations.test.js +3 -2
- package/test/{awards → unit/awards}/award-certificate-display.test.js +12 -11
- package/test/{awards → unit/awards}/award-collection-edge-cases.test.js +12 -11
- package/test/{awards → unit/awards}/award-collection-filtering.test.js +12 -11
- package/test/{awards → unit/awards}/award-completion-flow.test.js +15 -14
- package/test/{awards → unit/awards}/award-exclusion-handling.test.js +20 -19
- package/test/{awards → unit/awards}/award-multi-lesson.test.js +14 -13
- package/test/{awards → unit/awards}/award-observer-integration.test.js +14 -13
- package/test/{awards → unit/awards}/award-query-messages.test.js +30 -21
- package/test/{awards → unit/awards}/award-user-collection.test.js +11 -8
- package/test/{awards → unit/awards}/duplicate-prevention.test.js +12 -11
- package/test/unit/awards/helpers/index.js +3 -0
- package/test/{awards → unit/awards}/helpers/mock-setup.js +1 -1
- package/test/{awards → unit/awards}/helpers/progress-emitter.js +2 -2
- package/test/{awards → unit/awards}/message-generator.test.js +1 -1
- package/test/unit/contentLikes.test.js +62 -0
- package/test/unit/contentProgress.test.js +75 -0
- package/test/{dataContext.test.js → unit/dataContext.test.js} +2 -2
- package/test/unit/dateUtils.test.js +188 -0
- package/test/{imageSRCBuilder.test.js → unit/imageSRCBuilder.test.js} +2 -2
- package/test/{imageSRCVerify.test.js → unit/imageSRCVerify.test.js} +1 -1
- package/test/{lib → unit/lib}/filter.test.ts +10 -4
- package/test/{lib → unit/lib}/lastUpdated.test.js +6 -6
- package/test/{lib → unit/lib}/query.test.ts +1 -1
- package/test/{notifications.test.js → unit/notifications.test.js} +51 -39
- package/test/{progressRows.test.js → unit/progressRows.test.js} +57 -35
- package/test/unit/sanityQueryService.test.js +180 -0
- package/test/{streakMessage.test.js → unit/streakMessage.test.js} +18 -27
- package/test/unit/sync/adapters/idb-errors.test.ts +144 -0
- package/test/unit/sync/adapters/sqlite-errors.test.ts +173 -0
- package/test/unit/sync/helpers/TestModel.ts +44 -0
- package/test/unit/sync/helpers/index.ts +172 -0
- package/test/unit/sync/repositories/content-likes.test.ts +99 -0
- package/test/unit/sync/repositories/practices.test.ts +179 -0
- package/test/unit/sync/repositories/progress.test.ts +245 -0
- package/test/unit/sync/store/store-idb.test.ts +180 -0
- package/test/unit/sync/store/store.test.ts +274 -0
- package/test/unit/userActivity.test.js +99 -0
- package/tsconfig.json +15 -0
- package/test/awards/helpers/index.js +0 -3
- package/test/contentLikes.test.js +0 -95
- package/test/contentProgress.test.js +0 -279
- package/test/learningPaths.test.js +0 -70
- package/test/live/contentProgressLive.test.js +0 -110
- package/test/live/railcontentLive.test.js +0 -7
- package/test/sync/adapter.ts +0 -9
- package/test/sync/initialize-sync-manager.js +0 -88
- package/test/sync/models/award-database-integration.test.js +0 -519
- package/test/userActivity.test.js +0 -118
- /package/test/{awards → unit/awards}/helpers/completion-mock.js +0 -0
- /package/test/{lib → unit/lib}/__snapshots__/filter.test.ts.snap +0 -0
|
@@ -14,11 +14,16 @@ export interface User {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface InviteResponse {
|
|
17
|
-
email: string
|
|
18
17
|
id: number
|
|
19
18
|
created_at: string
|
|
20
19
|
expires_at: string
|
|
21
|
-
|
|
20
|
+
can_be_accepted: boolean
|
|
21
|
+
is_account_valid: boolean
|
|
22
|
+
is_invite_active: boolean
|
|
23
|
+
can_user_join: boolean
|
|
24
|
+
// These fields leak user information and are excluded entirely for the public endpoint
|
|
25
|
+
existing_user_details?: User
|
|
26
|
+
email?: string
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export interface UsersMultiAccountResponse {
|
|
@@ -27,7 +32,7 @@ export interface UsersMultiAccountResponse {
|
|
|
27
32
|
last_cancelled_multi_user_account: MultiUserAccountResponse
|
|
28
33
|
is_active_primary: boolean
|
|
29
34
|
is_active_sub: boolean
|
|
30
|
-
|
|
35
|
+
active_invites: InviteResponse[]
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface MultiUserAccountResponse {
|
|
@@ -35,13 +40,15 @@ export interface MultiUserAccountResponse {
|
|
|
35
40
|
product_name: string
|
|
36
41
|
is_active: boolean
|
|
37
42
|
primary_user: User
|
|
38
|
-
active_invited_emails: string[]
|
|
39
|
-
available_seats: number
|
|
40
|
-
available_invites: number
|
|
41
43
|
total_seats: number
|
|
42
|
-
active_subs: User[]
|
|
43
44
|
end_time: string
|
|
44
45
|
is_primary_account_holder: boolean
|
|
46
|
+
// The following fields are not included for public or subaccount users
|
|
47
|
+
active_invited_emails?: InviteResponse[]
|
|
48
|
+
available_seats?: number
|
|
49
|
+
available_invites?: InviteResponse[]
|
|
50
|
+
active_subs?: User[]
|
|
51
|
+
show_welcome?: boolean
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export interface CreateAccountParams {
|
|
@@ -53,6 +60,10 @@ export interface CreateInvitesParams {
|
|
|
53
60
|
emails: string[]
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
export interface UpdateMultiUserAccountParams {
|
|
64
|
+
show_welcome: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
|
|
57
68
|
/**
|
|
58
69
|
* Creates a new multi-user account with optional invites and seat count.
|
|
@@ -78,6 +89,19 @@ export async function fetchUsersMultiAccountDetails(userId: number): Promise<Use
|
|
|
78
89
|
return httpClient.get<UsersMultiAccountResponse>(`${baseUrl}/${userId}/details`)
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Fetch invite details
|
|
95
|
+
*
|
|
96
|
+
* @param {number} inviteId - The ID of the invite to check
|
|
97
|
+
* @returns {Promise<InviteResponse>} - A promise that resolves to the invite details.
|
|
98
|
+
* @throws {HttpError} - If the HTTP request fails.
|
|
99
|
+
*/
|
|
100
|
+
export async function fetchInvite(inviteId: number): Promise<InviteResponse> {
|
|
101
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
102
|
+
return httpClient.get<InviteResponse>(`${baseUrl}/invites/${inviteId}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
81
105
|
/**
|
|
82
106
|
* Creates invitations for an existing multi-user account.
|
|
83
107
|
*
|
|
@@ -125,3 +149,15 @@ export async function rescindInvite(inviteId: number): Promise<void> {
|
|
|
125
149
|
export async function removeUserFromActiveMultiUserAccount(userId: number): Promise<MultiUserAccountResponse|void> {
|
|
126
150
|
return DELETE(`${globalConfig.baseUrl}${baseUrl}/${userId}/remove`, {})
|
|
127
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Updates specified fields on a multi-user account. Authorized user must be the primary account owner
|
|
155
|
+
*
|
|
156
|
+
* @param {UpdateMultiUserAccountParams} params - The parameters for updating the account.
|
|
157
|
+
* @returns {Promise<MultiUserAccountResponse>} - Updated MultiUserAccountResponse if account owner
|
|
158
|
+
* @throws {HttpError} - If the request fails.
|
|
159
|
+
*/
|
|
160
|
+
export async function updateMultiUserAccount(params: UpdateMultiUserAccountParams): Promise<MultiUserAccountResponse> {
|
|
161
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
162
|
+
return httpClient.patch(`${globalConfig.baseUrl}${baseUrl}/update`, params)
|
|
163
|
+
}
|
|
@@ -11,9 +11,8 @@ import { HttpClient } from '../../infrastructure/http/HttpClient'
|
|
|
11
11
|
import { globalConfig } from '../config.js'
|
|
12
12
|
import { ReportResponse, ReportableType, IssueTypeMap, ReportIssueOption } from './types'
|
|
13
13
|
import { Brands } from '../../lib/brands'
|
|
14
|
-
import { generateContentUrl, generatePlaylistUrl, generateForumPostUrl, generateCommentUrl } from '../urlBuilder
|
|
15
|
-
import {fetchByRailContentId} from "
|
|
16
|
-
import {fetchByRailContentIds} from "../sanity";
|
|
14
|
+
import { generateContentUrl, generatePlaylistUrl, generateForumPostUrl, generateCommentUrl } from '../urlBuilder'
|
|
15
|
+
import {fetchByRailContentId, fetchByRailContentIds} from "../sanity";
|
|
17
16
|
import {addContextToContent} from "../contentAggregator";
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -122,7 +121,7 @@ export async function report<T extends ReportableType>(
|
|
|
122
121
|
id: params.id
|
|
123
122
|
})
|
|
124
123
|
} else if (params.type === 'forum_post') {
|
|
125
|
-
const { fetchPost } = await import('../forums/posts
|
|
124
|
+
const { fetchPost } = await import('../forums/posts')
|
|
126
125
|
const post = await fetchPost(params.id, params.brand)
|
|
127
126
|
|
|
128
127
|
if (post?.thread) {
|
package/src/services/sanity.js
CHANGED
|
@@ -33,8 +33,9 @@ import {
|
|
|
33
33
|
SONG_TYPES_WITH_CHILDREN,
|
|
34
34
|
liveFields,
|
|
35
35
|
postProcessBadge,
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
parentRecentTypes,
|
|
37
|
+
parentReferenceField,
|
|
38
|
+
grandParentReferenceField,
|
|
38
39
|
} from '../contentTypeConfig.js'
|
|
39
40
|
import { fetchSimilarItems } from './recommendations.js'
|
|
40
41
|
import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS, CONTENT_STATUSES } from '../contentMetaData.js'
|
|
@@ -952,7 +953,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
952
953
|
}
|
|
953
954
|
|
|
954
955
|
const parentQuery = addParent
|
|
955
|
-
? `"parent_content_data":
|
|
956
|
+
? `"parent_content_data": parent_content_reference[]->{
|
|
956
957
|
"id": railcontent_id,
|
|
957
958
|
title,
|
|
958
959
|
slug,
|
|
@@ -960,11 +961,12 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
960
961
|
"logo" : logo_image_url.asset->url,
|
|
961
962
|
"dark_mode_logo": dark_mode_logo_url.asset->url,
|
|
962
963
|
"light_mode_logo": light_mode_logo_url.asset->url,
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
964
|
+
...*[references(^._id) && _type == 'content-award'][0]{
|
|
965
|
+
"badge": badge.asset->url,
|
|
966
|
+
"badge_rear": badge_rear.asset->url,
|
|
967
|
+
"badge_logo": logo.asset->url,
|
|
968
|
+
}
|
|
969
|
+
},`
|
|
968
970
|
: ''
|
|
969
971
|
|
|
970
972
|
const fields = `${getFieldsForContentType()}
|
|
@@ -1106,19 +1108,6 @@ async function fetchRelatedByLicense(railcontentId, brand, onlyUseSongTypes, cou
|
|
|
1106
1108
|
* @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
|
|
1107
1109
|
*/
|
|
1108
1110
|
export async function fetchSiblingContent(railContentId, brand = null) {
|
|
1109
|
-
const filterGetParent = await new FilterBuilder(`references(^._id) && _type == ^.parent_type`, {
|
|
1110
|
-
pullFutureContent: true,
|
|
1111
|
-
showMembershipRestrictedContent: true, // Show parent even without permissions
|
|
1112
|
-
}).buildFilter()
|
|
1113
|
-
const filterForParentList = await new FilterBuilder(
|
|
1114
|
-
`references(^._id) && _type == ^.parent_type`,
|
|
1115
|
-
{
|
|
1116
|
-
pullFutureContent: true,
|
|
1117
|
-
isParentFilter: true,
|
|
1118
|
-
showMembershipRestrictedContent: true, // Show parent even without permissions
|
|
1119
|
-
}
|
|
1120
|
-
).buildFilter()
|
|
1121
|
-
|
|
1122
1111
|
const childrenFilter = await new FilterBuilder(``, {
|
|
1123
1112
|
isChildrenFilter: true,
|
|
1124
1113
|
showMembershipRestrictedContent: true, // Show all lessons in sidebar, need_access applied on individual page
|
|
@@ -1126,20 +1115,20 @@ export async function fetchSiblingContent(railContentId, brand = null) {
|
|
|
1126
1115
|
|
|
1127
1116
|
const brandString = brand ? ` && brand == "${brand}"` : ''
|
|
1128
1117
|
const queryFields = getFieldsForContentType()
|
|
1129
|
-
|
|
1118
|
+
const courseCollectionFields = await getFieldsForContentTypeWithFilteredChildren('course-collection')
|
|
1130
1119
|
const query = `*[railcontent_id == ${railContentId}${brandString}]{
|
|
1131
1120
|
_type,
|
|
1132
1121
|
parent_type,
|
|
1133
1122
|
railcontent_id,
|
|
1134
|
-
'parent_id': ${
|
|
1135
|
-
'grandparent_id'
|
|
1136
|
-
'
|
|
1137
|
-
'
|
|
1138
|
-
|
|
1123
|
+
'parent_id': ${parentReferenceField}->railcontent_id,
|
|
1124
|
+
'grandparent_id': ${grandParentReferenceField}->railcontent_id,
|
|
1125
|
+
'collection_data': ${grandParentReferenceField}->{${courseCollectionFields}},
|
|
1126
|
+
'for-calculations': ${parentReferenceField}->{
|
|
1127
|
+
'siblings-list': child[]->railcontent_id,
|
|
1128
|
+
'parents-list': ${parentReferenceField}->child[]->railcontent_id
|
|
1139
1129
|
},
|
|
1140
|
-
"related_lessons" :
|
|
1130
|
+
"related_lessons" : ${parentReferenceField}->child[${childrenFilter}]->{${queryFields}}
|
|
1141
1131
|
}`
|
|
1142
|
-
|
|
1143
1132
|
let result = await fetchSanity(query, false, { processNeedAccess: true })
|
|
1144
1133
|
|
|
1145
1134
|
//there's no way in sanity to retrieve the index of an array, so we must calculate after fetch
|
|
@@ -1152,10 +1141,6 @@ export async function fetchSiblingContent(railContentId, brand = null) {
|
|
|
1152
1141
|
|
|
1153
1142
|
delete result['for-calculations']
|
|
1154
1143
|
|
|
1155
|
-
if (result['grandparent_id']) {
|
|
1156
|
-
result['collection_data'] = await fetchCourseCollectionData(result['grandparent_id'])
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
1144
|
result = { ...result, parentCount, currentParentIndex, siblingCount, currentSiblingIndex }
|
|
1160
1145
|
return result
|
|
1161
1146
|
} else {
|
|
@@ -1199,7 +1184,7 @@ export async function fetchRelatedLessons(railContentId) {
|
|
|
1199
1184
|
}
|
|
1200
1185
|
|
|
1201
1186
|
export async function fetchLiveEvent(brand, forcedContentId = null) {
|
|
1202
|
-
const LIVE_EXTRA_MINUTES =
|
|
1187
|
+
const LIVE_EXTRA_MINUTES = 15
|
|
1203
1188
|
//calendarIDs taken from addevent.php
|
|
1204
1189
|
// TODO import instructor calendars to Sanity
|
|
1205
1190
|
let defaultCalendarID = ''
|
|
@@ -1320,16 +1305,12 @@ export async function fetchByReference(
|
|
|
1320
1305
|
* @returns {Promise<int|null>}
|
|
1321
1306
|
*/
|
|
1322
1307
|
export async function fetchTopLevelParentId(railcontentId) {
|
|
1323
|
-
const parentFilter = 'railcontent_id in [...(^.parent_content_data[].id)] && (!defined(parent_content_data) || count(parent_content_data) == 0)'
|
|
1324
|
-
const statusFilter = "&& status in ['scheduled', 'published', 'archived', 'unlisted']"
|
|
1325
|
-
|
|
1326
1308
|
const query = `*[railcontent_id == ${railcontentId}]{
|
|
1327
|
-
railcontent_id,
|
|
1328
|
-
'top_parent': *[${parentFilter} ${statusFilter}][0].railcontent_id
|
|
1309
|
+
'top_parent': coalesce(${grandParentReferenceField}->railcontent_id, ${parentReferenceField}->railcontent_id, railcontent_id),
|
|
1329
1310
|
}`
|
|
1330
1311
|
let response = await fetchSanity(query, false, { processNeedAccess: false })
|
|
1331
1312
|
if (!response) return null
|
|
1332
|
-
return response['top_parent'] ??
|
|
1313
|
+
return response['top_parent'] ?? railcontentId
|
|
1333
1314
|
}
|
|
1334
1315
|
|
|
1335
1316
|
export async function getHierarchy(contentId, collection) {
|
|
@@ -1403,31 +1384,18 @@ async function fetchALaCarteHierarchyData(railcontentId) {
|
|
|
1403
1384
|
const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
|
|
1404
1385
|
const query = `*[railcontent_id == ${topLevelId}]{
|
|
1405
1386
|
railcontent_id,
|
|
1406
|
-
'metadata': { brand, 'type': _type, 'parent_id': coalesce(
|
|
1387
|
+
'metadata': { brand, 'type': _type, 'parent_id': coalesce(${parentReferenceField}->railcontent_id, 0) },
|
|
1407
1388
|
'assignments': assignment[]{railcontent_id},
|
|
1408
1389
|
'children': child[${childrenFilter}]->{
|
|
1409
1390
|
railcontent_id,
|
|
1410
|
-
'metadata': {
|
|
1411
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1391
|
+
'metadata': { brand, 'type': _type, 'parent_id': coalesce(${parentReferenceField}->railcontent_id, 0) },
|
|
1412
1392
|
'assignments': assignment[]{railcontent_id},
|
|
1413
1393
|
'children': child[${childrenFilter}]->{
|
|
1414
1394
|
railcontent_id,
|
|
1415
|
-
'metadata': {
|
|
1416
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1395
|
+
'metadata': { brand, 'type': _type, 'parent_id': coalesce(${parentReferenceField}->railcontent_id, 0) },
|
|
1417
1396
|
'assignments': assignment[]{railcontent_id},
|
|
1418
|
-
'children': child[${childrenFilter}]->{
|
|
1419
|
-
railcontent_id,
|
|
1420
|
-
'metadata': {
|
|
1421
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1422
|
-
'assignments': assignment[]{railcontent_id},
|
|
1423
|
-
'children': child[${childrenFilter}]->{
|
|
1424
|
-
railcontent_id,
|
|
1425
|
-
'metadata': {
|
|
1426
|
-
brand, 'type': _type, 'parent_id': coalesce(parent_content_data[0].id, 0) },
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
1397
|
}
|
|
1430
|
-
}
|
|
1398
|
+
}
|
|
1431
1399
|
}`
|
|
1432
1400
|
return await fetchSanity(query, false, { processNeedAccess: false })
|
|
1433
1401
|
}
|
|
@@ -2000,7 +1968,7 @@ export async function fetchTabData(
|
|
|
2000
1968
|
? `&& !(railcontent_id in [${excludeIds.join(',')}])`
|
|
2001
1969
|
: ''
|
|
2002
1970
|
|
|
2003
|
-
const excludeCoursesInCourseCollectionsFilter = `&& !(_type == 'course' && defined(
|
|
1971
|
+
const excludeCoursesInCourseCollectionsFilter = `&& !(_type == 'course' && defined(parent_content_reference) && count(parent_content_reference[]) > 0)`
|
|
2004
1972
|
|
|
2005
1973
|
filter = `brand == "${brand}" && (defined(railcontent_id)) ${includedFieldsFilter} ${progressFilter} ${excludedIdsFilter} ${excludeCoursesInCourseCollectionsFilter}`
|
|
2006
1974
|
const childrenFilter = await new FilterBuilder(``, {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { SyncTelemetry } from '../telemetry'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import _LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
|
|
4
4
|
|
|
5
5
|
import { deleteDatabase, lokiFatalError } from '@nozbe/watermelondb/adapters/lokijs/worker/lokiExtensions'
|
|
6
6
|
|
|
7
|
+
// Handle CJS/ESM interop: in Node.js ESM the default import is the exports object
|
|
8
|
+
const LokiJSAdapter = (_LokiJSAdapter as any).default ?? _LokiJSAdapter
|
|
9
|
+
|
|
7
10
|
export type LokiExtensions = {
|
|
8
11
|
onPersistenceError?: (err: Error) => void
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
export default class LokiPersistenceErrorAwareAdapter extends LokiJSAdapter {
|
|
14
|
+
export default class LokiPersistenceErrorAwareAdapter extends (LokiJSAdapter as typeof _LokiJSAdapter) {
|
|
12
15
|
constructor(options: any, extensions: LokiExtensions = {}) {
|
|
13
16
|
super(options);
|
|
14
17
|
const that = this;
|
|
@@ -162,7 +165,7 @@ export function simulateIndexedDBQuotaExceeded() {
|
|
|
162
165
|
})
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
export function abortWritesToDatabase(adapter: LokiJSAdapter) {
|
|
168
|
+
export function abortWritesToDatabase(adapter: typeof LokiJSAdapter) {
|
|
166
169
|
// acts as handy helper to disable loki's save methods entirely
|
|
167
170
|
lokiFatalError(adapter._driver.loki)
|
|
168
171
|
return Promise.resolve()
|
|
@@ -174,7 +177,7 @@ export function abortWritesToDatabase(adapter: LokiJSAdapter) {
|
|
|
174
177
|
* Haven't encountered live issues related to this yet, but theoretically provides
|
|
175
178
|
* the cleanest slate for a user to recover from schema issues?
|
|
176
179
|
*/
|
|
177
|
-
export function destroyDatabase(dbName: string, adapter: LokiJSAdapter): Promise<void> {
|
|
180
|
+
export function destroyDatabase(dbName: string, adapter: typeof LokiJSAdapter): Promise<void> {
|
|
178
181
|
return new Promise(async (resolve, reject) => {
|
|
179
182
|
if (adapter._driver) {
|
|
180
183
|
try {
|
|
@@ -122,7 +122,7 @@ export interface SyncResponseBase {
|
|
|
122
122
|
|
|
123
123
|
export type PushPayload = {
|
|
124
124
|
entries: ({
|
|
125
|
-
record:
|
|
125
|
+
record: Record<string, unknown>
|
|
126
126
|
meta: {
|
|
127
127
|
ids: {
|
|
128
128
|
id: string
|
|
@@ -135,23 +135,11 @@ export type PushPayload = {
|
|
|
135
135
|
ids: {
|
|
136
136
|
id: string
|
|
137
137
|
}
|
|
138
|
-
deleted_at:
|
|
138
|
+
deleted_at: number
|
|
139
139
|
}
|
|
140
140
|
})[]
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
interface ServerPushPayload {
|
|
144
|
-
entries: {
|
|
145
|
-
record: BaseModel | null
|
|
146
|
-
meta: {
|
|
147
|
-
ids: {
|
|
148
|
-
id: string
|
|
149
|
-
},
|
|
150
|
-
deleted_at: EpochMs | null
|
|
151
|
-
}
|
|
152
|
-
}[]
|
|
153
|
-
}
|
|
154
|
-
|
|
155
143
|
export function makeFetchRequest(input: RequestInfo, init?: RequestInit) {
|
|
156
144
|
return (userId: number, context: SyncContext) => new Request(globalConfig.baseUrl + input, {
|
|
157
145
|
...init,
|
|
@@ -23,7 +23,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
23
23
|
Q.sortBy('updated_at', 'desc'),
|
|
24
24
|
|
|
25
25
|
...(limit ? [Q.take(limit)] : []),
|
|
26
|
-
])
|
|
26
|
+
].filter(Boolean) as Q.Clause[])
|
|
27
27
|
|
|
28
28
|
return opts.onlyIds
|
|
29
29
|
? results.data.map((r) => r.content_id)
|
|
@@ -44,7 +44,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
44
44
|
Q.sortBy('updated_at', 'desc'),
|
|
45
45
|
|
|
46
46
|
...(limit ? [Q.take(limit)] : []),
|
|
47
|
-
])
|
|
47
|
+
].filter(Boolean) as Q.Clause[])
|
|
48
48
|
|
|
49
49
|
return opts.onlyIds
|
|
50
50
|
? results.data.map((r) => r.content_id)
|
|
@@ -78,7 +78,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
78
78
|
|
|
79
79
|
Q.or(Q.where('state', STATE.STARTED), Q.where('state', STATE.COMPLETED)),
|
|
80
80
|
Q.sortBy('updated_at', 'desc'),
|
|
81
|
-
]
|
|
81
|
+
].filter(Boolean) as Q.Clause[]
|
|
82
82
|
|
|
83
83
|
if (opts.updatedAfter) {
|
|
84
84
|
clauses.push(Q.where('updated_at', Q.gte(opts.updatedAfter)))
|
|
@@ -892,7 +892,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
892
892
|
|
|
893
893
|
default:
|
|
894
894
|
this.telemetry.error(`[store:${this.model.table}] Unknown record status`, {
|
|
895
|
-
status: existing._raw._status,
|
|
895
|
+
extra: { status: existing._raw._status },
|
|
896
896
|
})
|
|
897
897
|
}
|
|
898
898
|
} else {
|
|
@@ -999,6 +999,11 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
999
999
|
}, 'sync.cleanup')
|
|
1000
1000
|
})
|
|
1001
1001
|
}, SyncStore.CLEANUP_INTERVAL)
|
|
1002
|
+
|
|
1003
|
+
// in tests in node env, prevents the timer from keeping the process alive
|
|
1004
|
+
if (typeof (this.cleanupTimer as any).unref === 'function') {
|
|
1005
|
+
(this.cleanupTimer as any).unref()
|
|
1006
|
+
}
|
|
1002
1007
|
}
|
|
1003
1008
|
|
|
1004
1009
|
private stopCleanupTimer() {
|
|
@@ -10,7 +10,7 @@ export type SentryLike = {
|
|
|
10
10
|
captureMessage: typeof InjectedSentry.captureMessage
|
|
11
11
|
addBreadcrumb: typeof InjectedSentry.addBreadcrumb
|
|
12
12
|
startSpan: typeof InjectedSentry.startSpan
|
|
13
|
-
logger:
|
|
13
|
+
logger: { debug: (...args: any[]) => void; info: (...args: any[]) => void }
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export type StartSpanOptions = Parameters<typeof InjectedSentry.startSpan>[0]
|
|
@@ -43,7 +43,7 @@ class StreakCalculator {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
private async fetchAllPractices(): Promise<PracticeData> {
|
|
46
|
-
const query = await db.practices.
|
|
46
|
+
const query = await db.practices.getAll()
|
|
47
47
|
|
|
48
48
|
return query.data.reduce((acc, practice) => {
|
|
49
49
|
acc[practice.date] = acc[practice.date] || []
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Skipped Tests Reference
|
|
2
|
+
|
|
3
|
+
This document tracks all skipped tests and why they are skipped. Tests are divided into two categories:
|
|
4
|
+
|
|
5
|
+
1. **Skipped for CI** — were passing but depend on live external services; skipped to enable clean CI runs
|
|
6
|
+
2. **Previously skipped — failing or unknown** — were already skipped before CI work; many confirmed failing
|
|
7
|
+
|
|
8
|
+
The goal is to eventually move all Category 1 tests into a dedicated integration/live test suite, and to triage Category 2 tests as either fixable or retired.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Category 1: Skipped for CI (were passing, have external dependencies)
|
|
13
|
+
|
|
14
|
+
### `test/sanityQueryService.test.js` — Sanity CMS
|
|
15
|
+
|
|
16
|
+
All tests in this file call real Sanity GROQ queries via `initializeTestService(true)`.
|
|
17
|
+
|
|
18
|
+
| Test | Dependency |
|
|
19
|
+
|---|---|
|
|
20
|
+
| fetchSongById | Sanity |
|
|
21
|
+
| fetchReturning | Sanity |
|
|
22
|
+
| fetchLeaving | Sanity |
|
|
23
|
+
| fetchComingSoon | Sanity |
|
|
24
|
+
| fetchSanity-WithPostProcess | Sanity |
|
|
25
|
+
| fetchSanityPostProcess | Sanity |
|
|
26
|
+
| fetchByRailContentIds | Sanity |
|
|
27
|
+
| fetchByRailContentIds_Order | Sanity |
|
|
28
|
+
| fetchUpcomingNewReleases | Sanity |
|
|
29
|
+
| fetchLessonContent | Sanity |
|
|
30
|
+
| fetchAllSongsInProgress | Sanity |
|
|
31
|
+
| fetchNewReleases | Sanity |
|
|
32
|
+
| fetchAllWorkouts | Sanity |
|
|
33
|
+
| fetchAllInstructorField | Sanity |
|
|
34
|
+
| fetchAllInstructors | Sanity |
|
|
35
|
+
| fetchAll-CustomFields | Sanity |
|
|
36
|
+
| fetchRelatedLessons | Sanity |
|
|
37
|
+
| fetchRelatedLessons-quick-tips | Sanity |
|
|
38
|
+
| fetchRelatedLessons-in-rhythm | Sanity |
|
|
39
|
+
| getSortOrder | Sanity (describe block requires live auth) |
|
|
40
|
+
| fetchAll-WithProgress | Sanity |
|
|
41
|
+
| fetchAllFilterOptions-WithProgress | Sanity |
|
|
42
|
+
| fetchAll-IncludedFields | Sanity |
|
|
43
|
+
| fetchAll-IncludedFields-rudiment-multiple-gear | Sanity |
|
|
44
|
+
| fetchByReference | Sanity |
|
|
45
|
+
| fetchScheduledReleases | Sanity |
|
|
46
|
+
| fetchAll-GroupBy-Artists | Sanity |
|
|
47
|
+
| fetchAll-GroupBy-Instructors | Sanity |
|
|
48
|
+
| fetchMetadata | Sanity |
|
|
49
|
+
| fetchMetadata-Coach-Lessons | Sanity |
|
|
50
|
+
| invalidContentType | Sanity (describe block requires live auth) |
|
|
51
|
+
| metaDataForLessons | Sanity |
|
|
52
|
+
| metaDataForSongs | Sanity |
|
|
53
|
+
| fetchAllFilterOptionsLessons | Sanity |
|
|
54
|
+
| fetchAllFilterOptionsSongs | Sanity |
|
|
55
|
+
| fetchLiveEvent | Sanity |
|
|
56
|
+
| fetchRelatedLessons-pack-bundle-lessons | Sanity |
|
|
57
|
+
| fetchRelatedLessons-course-parts | Sanity |
|
|
58
|
+
| fetchRelatedLessons-song-tutorial-children | Sanity |
|
|
59
|
+
| fetchMetadata (second) | Sanity |
|
|
60
|
+
|
|
61
|
+
### `test/content.test.js` — Sanity CMS + Railcontent API
|
|
62
|
+
|
|
63
|
+
| Test | Dependency |
|
|
64
|
+
|---|---|
|
|
65
|
+
| getTabResults-Singles | Sanity + Railcontent |
|
|
66
|
+
| getTabResults-Courses | Sanity + Railcontent |
|
|
67
|
+
| getTabResults-Type-Explore-All | Sanity + Railcontent |
|
|
68
|
+
|
|
69
|
+
### `test/user/permissions.test.js` — Railcontent API
|
|
70
|
+
|
|
71
|
+
| Test | Dependency |
|
|
72
|
+
|---|---|
|
|
73
|
+
| fetchUserPermissions | Railcontent `fetchUserPermissionsData` |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Category 2: Previously Skipped — Failing or Unknown State
|
|
78
|
+
|
|
79
|
+
These were already skipped before CI work. Status is noted where confirmed.
|
|
80
|
+
|
|
81
|
+
### `test/sanityQueryService.test.js`
|
|
82
|
+
|
|
83
|
+
| Test | Status | Failure Reason |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| fetchSongArtistCount | Unknown | — |
|
|
86
|
+
| fetchUpcomingEvents | Unknown | — |
|
|
87
|
+
| fetchLessonContent-PlayAlong-containts-array-of-videos | Unknown | — |
|
|
88
|
+
| fetchAllSortField | Unknown | — |
|
|
89
|
+
| fetchRelatedLessons-child | Unknown | — |
|
|
90
|
+
| fetchPackAll | Unknown | — |
|
|
91
|
+
| fetchAllPacks | Unknown | — |
|
|
92
|
+
| fetchAll-IncludedFields-multiple | Unknown | — |
|
|
93
|
+
| fetchAll-IncludedFields-playalong-multiple | Unknown | — |
|
|
94
|
+
| fetchAll-IncludedFields-coaches-multiple-focus | Unknown | — |
|
|
95
|
+
| fetchAll-IncludedFields-songs-multiple-instrumentless | Unknown | — |
|
|
96
|
+
| fetchAll-GroupBy-Genre | Unknown | — |
|
|
97
|
+
| fetchShowsData | Unknown | — |
|
|
98
|
+
| fetchShowsData-OddTimes | Unknown | — |
|
|
99
|
+
| fetchTopLevelParentId | Unknown | — |
|
|
100
|
+
| fetchHierarchy | Unknown | — |
|
|
101
|
+
| fetchTopLeveldrafts | Failing | Timeout (>5s) |
|
|
102
|
+
| fetchCommentData | Failing | `null.forEach` — Sanity returns null for content IDs |
|
|
103
|
+
| baseConstructor | Unknown | — |
|
|
104
|
+
| withOnlyFilterAvailableStatuses | Unknown | — |
|
|
105
|
+
| withContentStatusAndFutureScheduledContent | Unknown | — |
|
|
106
|
+
| withUserPermissions | Unknown | — |
|
|
107
|
+
| withUserPermissionsForPlusUser | Unknown | — |
|
|
108
|
+
| withPermissionBypass | Unknown | — |
|
|
109
|
+
| withPublishOnRestrictions | Unknown | — |
|
|
110
|
+
| fetchAllFilterOptions | Unknown | — |
|
|
111
|
+
| fetchAllFilterOptions-Rudiment | Unknown | — |
|
|
112
|
+
| fetchAllFilterOptions-PlayAlong | Unknown | — |
|
|
113
|
+
| fetchAllFilterOptions-Coaches | Unknown | — |
|
|
114
|
+
| fetchAllFilterOptions-filter-selected | Failing | `null.meta` — API returns null for filter combination |
|
|
115
|
+
| customBrandTypeExists | Unknown | — |
|
|
116
|
+
| withCommon | Unknown | — |
|
|
117
|
+
| fetchOtherSongVersions | Failing | 0 results — content is drafted/admin-only |
|
|
118
|
+
| fetchLessonsFeaturingThisContent | Failing | 0 results — content is drafted/admin-only |
|
|
119
|
+
| getRecommendedForYou | Failing | `SyncError: Intended user ID does not match` |
|
|
120
|
+
| getRecommendedForYou-SeeAll | Failing | `SyncError: Intended user ID does not match` |
|
|
121
|
+
|
|
122
|
+
### `test/content.test.js`
|
|
123
|
+
|
|
124
|
+
| Test | Status | Failure Reason |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| getTabResults-Filters | Failing | Timeout (>5s) |
|
|
127
|
+
| getTabResults-Type-Filter | Failing | `TypeError: null.entity` — Sanity returns null |
|
|
128
|
+
| getContentRows | Unknown |Sanity & pw-recommender |
|
|
129
|
+
| getNewAndUpcoming | Failing | Timeout (>5s) |
|
|
130
|
+
| getScheduleContentRows | Failing | Timeout (>5s) |
|
|
131
|
+
| getSpecificScheduleContentRow | Failing | Timeout (>5s) |
|
|
132
|
+
|
|
133
|
+
### `test/contentProgress.test.js`
|
|
134
|
+
|
|
135
|
+
| Test | Status | Failure Reason |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| get-Songs-Tutorials | Unknown | Live Sanity call |
|
|
138
|
+
| get-Songs-Transcriptions | Unknown | Live Sanity call |
|
|
139
|
+
| get-Songs-Play-Alongs | Unknown | Live Sanity call |
|
|
140
|
+
|
|
141
|
+
### `test/progressRows.test.js`
|
|
142
|
+
|
|
143
|
+
| Test | Status | Failure Reason |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| check progress rows logic | Failing | Stale mock data — not a live API issue; mock data no longer reflects current data shape |
|
|
146
|
+
|
|
147
|
+
### `test/learningPaths.test.js`
|
|
148
|
+
|
|
149
|
+
| Test | Status | Failure Reason |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| learningPathCompletion | Unknown | Uses `initializeTestService(true)` — live API |
|
package/test/initializeTests.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { globalConfig, initializeService } from '../src'
|
|
2
2
|
import { LocalStorageMock } from './localStorageMock'
|
|
3
|
-
import { initializeSyncManager } from './sync/initialize-sync-manager'
|
|
4
3
|
const railContentModule = require('../src/services/railcontent.js')
|
|
4
|
+
const awardDefsModule = require('../src/services/awards/internal/award-definitions.js')
|
|
5
5
|
let token = null
|
|
6
6
|
let userId = process.env.RAILCONTENT_USER_ID ?? null
|
|
7
7
|
|
|
@@ -51,11 +51,10 @@ export async function initializeTestService(useLive = false, isAdmin = false) {
|
|
|
51
51
|
localStorage: new LocalStorageMock(),
|
|
52
52
|
isMA: true,
|
|
53
53
|
}
|
|
54
|
+
jest.spyOn(awardDefsModule.awardDefinitions, 'initialize').mockResolvedValue()
|
|
54
55
|
initializeService(config)
|
|
55
56
|
// Mock user permissions
|
|
56
57
|
let permissionsMock = jest.spyOn(railContentModule, 'fetchUserPermissionsData')
|
|
57
58
|
let permissionsData = { permissions: [108, 91, 92], isAdmin: isAdmin }
|
|
58
59
|
permissionsMock.mockImplementation(() => permissionsData)
|
|
59
|
-
|
|
60
|
-
initializeSyncManager(userId)
|
|
61
60
|
}
|