@vue-skuilder/db 0.1.11-9 → 0.1.12
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/dist/core/index.d.mts +7 -6
- package/dist/core/index.d.ts +7 -6
- package/dist/core/index.js +358 -87
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +358 -87
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-BiP3kWix.d.mts} +8 -1
- package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-DSdeyRT3.d.ts} +8 -1
- package/dist/impl/couch/index.d.mts +19 -7
- package/dist/impl/couch/index.d.ts +19 -7
- package/dist/impl/couch/index.js +375 -100
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +374 -99
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +23 -8
- package/dist/impl/static/index.d.ts +23 -8
- package/dist/impl/static/index.js +289 -85
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +289 -85
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-CUNnL38E.d.mts → index-Bmll7Xse.d.mts} +1 -1
- package/dist/{index-CLL31bEy.d.ts → index-CD8BZz2k.d.ts} +1 -1
- package/dist/index.d.mts +123 -20
- package/dist/index.d.ts +123 -20
- package/dist/index.js +1133 -343
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1137 -343
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.d.mts +1 -0
- package/dist/pouch/index.d.ts +1 -0
- package/dist/pouch/index.js +49 -0
- package/dist/pouch/index.js.map +1 -0
- package/dist/pouch/index.mjs +16 -0
- package/dist/pouch/index.mjs.map +1 -0
- package/dist/{types-BefDGkKa.d.ts → types-CewsN87z.d.ts} +1 -1
- package/dist/{types-DC-ckZug.d.mts → types-Dbp5DaRR.d.mts} +1 -1
- package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-6ettoclI.d.mts} +17 -2
- package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-6ettoclI.d.ts} +17 -2
- package/dist/{userDB-DusL7OXe.d.ts → userDB-C4yyAnpp.d.mts} +89 -56
- package/dist/{userDB-C33Hzjgn.d.mts → userDB-CD6s6ZCp.d.ts} +89 -56
- package/dist/util/packer/index.d.mts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +3 -2
- package/src/core/interfaces/courseDB.ts +26 -3
- package/src/core/interfaces/dataLayerProvider.ts +9 -1
- package/src/core/interfaces/userDB.ts +80 -64
- package/src/core/navigators/elo.ts +10 -7
- package/src/core/navigators/hardcodedOrder.ts +64 -0
- package/src/core/navigators/index.ts +2 -1
- package/src/core/types/contentNavigationStrategy.ts +2 -1
- package/src/core/types/types-legacy.ts +7 -2
- package/src/impl/common/BaseUserDB.ts +60 -14
- package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
- package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
- package/src/impl/couch/adminDB.ts +2 -2
- package/src/impl/couch/auth.ts +13 -4
- package/src/impl/couch/classroomDB.ts +10 -12
- package/src/impl/couch/courseAPI.ts +2 -2
- package/src/impl/couch/courseDB.ts +204 -38
- package/src/impl/couch/courseLookupDB.ts +4 -3
- package/src/impl/couch/index.ts +36 -4
- package/src/impl/couch/pouchdb-setup.ts +3 -3
- package/src/impl/couch/updateQueue.ts +59 -36
- package/src/impl/static/StaticDataLayerProvider.ts +68 -17
- package/src/impl/static/courseDB.ts +64 -20
- package/src/impl/static/coursesDB.ts +10 -6
- package/src/pouch/index.ts +2 -0
- package/src/study/ItemQueue.ts +58 -0
- package/src/study/SessionController.ts +182 -111
- package/src/study/SpacedRepetition.ts +1 -1
- package/src/study/services/CardHydrationService.ts +153 -0
- package/src/study/services/EloService.ts +85 -0
- package/src/study/services/ResponseProcessor.ts +224 -0
- package/src/study/services/SrsService.ts +44 -0
- package/tsup.config.ts +1 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.12",
|
|
7
7
|
"description": "Database layer for vue-skuilder",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"module": "dist/index.mjs",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
48
|
-
"@vue-skuilder/common": "0.1.
|
|
48
|
+
"@vue-skuilder/common": "0.1.12",
|
|
49
49
|
"cross-fetch": "^4.1.0",
|
|
50
50
|
"moment": "^2.29.4",
|
|
51
51
|
"pouchdb": "^9.0.0",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"tsup": "^8.0.2",
|
|
58
58
|
"typescript": "~5.7.2"
|
|
59
59
|
},
|
|
60
|
-
"stableVersion": "0.1.
|
|
60
|
+
"stableVersion": "0.1.12"
|
|
61
61
|
}
|
|
@@ -31,11 +31,12 @@ export function isReview(item: StudySessionItem): item is StudySessionReviewItem
|
|
|
31
31
|
|
|
32
32
|
export interface StudySessionItem {
|
|
33
33
|
status: 'new' | 'review' | 'failed-new' | 'failed-review';
|
|
34
|
-
qualifiedID: `${string}-${string}` | `${string}-${string}-${number}`;
|
|
35
|
-
cardID: string;
|
|
36
34
|
contentSourceType: 'course' | 'classroom';
|
|
37
35
|
contentSourceID: string;
|
|
36
|
+
// qualifiedID: `${string}-${string}` | `${string}-${string}-${number}`;
|
|
37
|
+
cardID: string;
|
|
38
38
|
courseID: string;
|
|
39
|
+
elo?: number;
|
|
39
40
|
// reviewID?: string;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CourseConfig, CourseElo, DataShape, SkuilderCourseData } from '@vue-skuilder/common';
|
|
2
2
|
import { StudySessionNewItem, StudySessionItem } from './contentSource';
|
|
3
|
-
import { TagStub, Tag } from '../types/types-legacy';
|
|
3
|
+
import { TagStub, Tag, QualifiedCardID } from '../types/types-legacy';
|
|
4
4
|
import { DataLayerResult } from '../types/db';
|
|
5
5
|
import { NavigationStrategyManager } from './navigationStrategyManager';
|
|
6
6
|
|
|
@@ -53,7 +53,16 @@ export interface CourseDBInterface extends NavigationStrategyManager {
|
|
|
53
53
|
/**
|
|
54
54
|
* Get cards sorted by ELO rating
|
|
55
55
|
*/
|
|
56
|
-
getCardsByELO(
|
|
56
|
+
getCardsByELO(
|
|
57
|
+
elo: number,
|
|
58
|
+
limit?: number
|
|
59
|
+
): Promise<
|
|
60
|
+
{
|
|
61
|
+
courseID: string;
|
|
62
|
+
cardID: string;
|
|
63
|
+
elo?: number;
|
|
64
|
+
}[]
|
|
65
|
+
>;
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
68
|
* Get ELO data for specific cards
|
|
@@ -75,7 +84,7 @@ export interface CourseDBInterface extends NavigationStrategyManager {
|
|
|
75
84
|
*/
|
|
76
85
|
getCardsCenteredAtELO(
|
|
77
86
|
options: { limit: number; elo: 'user' | 'random' | number },
|
|
78
|
-
filter?: (
|
|
87
|
+
filter?: (card: QualifiedCardID) => boolean
|
|
79
88
|
): Promise<StudySessionItem[]>;
|
|
80
89
|
|
|
81
90
|
/**
|
|
@@ -136,4 +145,18 @@ export interface CourseDBInterface extends NavigationStrategyManager {
|
|
|
136
145
|
elo: CourseElo;
|
|
137
146
|
}[]
|
|
138
147
|
>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Search for cards by text content
|
|
151
|
+
* @param query Text to search for
|
|
152
|
+
* @returns Array of matching card data
|
|
153
|
+
*/
|
|
154
|
+
searchCards(query: string): Promise<any[]>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find documents using PouchDB query syntax
|
|
158
|
+
* @param request PouchDB find request
|
|
159
|
+
* @returns Query response
|
|
160
|
+
*/
|
|
161
|
+
find(request: PouchDB.Find.FindRequest<any>): Promise<PouchDB.Find.FindResponse<any>>;
|
|
139
162
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// db/src/core/interfaces.ts
|
|
2
2
|
|
|
3
|
-
import { UserDBInterface } from './userDB';
|
|
3
|
+
import { UserDBInterface, UserDBReader } from './userDB';
|
|
4
4
|
import { CourseDBInterface, CoursesDBInterface } from './courseDB';
|
|
5
5
|
import { ClassroomDBInterface } from './classroomDB';
|
|
6
6
|
import { AdminDBInterface } from './adminDB';
|
|
@@ -14,6 +14,14 @@ export interface DataLayerProvider {
|
|
|
14
14
|
*/
|
|
15
15
|
getUserDB(): UserDBInterface;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Create a UserDBReader for a specific user (admin access required)
|
|
19
|
+
* Uses session authentication to verify requesting user is admin
|
|
20
|
+
* @param targetUsername - The username to create a reader for
|
|
21
|
+
* @throws Error if requesting user is not 'admin'
|
|
22
|
+
*/
|
|
23
|
+
createUserReaderForUser(targetUsername: string): Promise<UserDBReader>;
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* Get a course database interface
|
|
19
27
|
*/
|
|
@@ -6,46 +6,16 @@ import {
|
|
|
6
6
|
} from '@db/core/types/user';
|
|
7
7
|
import { CourseElo, Status } from '@vue-skuilder/common';
|
|
8
8
|
import { Moment } from 'moment';
|
|
9
|
-
import { CardHistory, CardRecord } from '../types/types-legacy';
|
|
9
|
+
import { CardHistory, CardRecord, QualifiedCardID } from '../types/types-legacy';
|
|
10
10
|
import { UserConfig } from '../types/user';
|
|
11
11
|
import { DocumentUpdater } from '@db/study';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Read-only user data operations
|
|
15
15
|
*/
|
|
16
|
-
export interface
|
|
17
|
-
|
|
18
|
-
* Create a new user account
|
|
19
|
-
*/
|
|
20
|
-
createAccount(
|
|
21
|
-
username: string,
|
|
22
|
-
password: string
|
|
23
|
-
): Promise<{
|
|
24
|
-
status: Status;
|
|
25
|
-
error: string;
|
|
26
|
-
}>;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Log in as a user
|
|
30
|
-
*/
|
|
31
|
-
login(
|
|
32
|
-
username: string,
|
|
33
|
-
password: string
|
|
34
|
-
): Promise<{
|
|
35
|
-
ok: boolean;
|
|
36
|
-
name?: string;
|
|
37
|
-
roles?: string[];
|
|
38
|
-
}>;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Log out the current user
|
|
42
|
-
*/
|
|
43
|
-
logout(): Promise<{
|
|
44
|
-
ok: boolean;
|
|
45
|
-
}>;
|
|
46
|
-
|
|
16
|
+
export interface UserDBReader {
|
|
17
|
+
get<T>(id: string): Promise<T & PouchDB.Core.RevisionIdMeta>;
|
|
47
18
|
getUsername(): string;
|
|
48
|
-
|
|
49
19
|
isLoggedIn(): boolean;
|
|
50
20
|
|
|
51
21
|
/**
|
|
@@ -53,16 +23,6 @@ export interface UserDBInterface extends DocumentUpdater {
|
|
|
53
23
|
*/
|
|
54
24
|
getConfig(): Promise<UserConfig>;
|
|
55
25
|
|
|
56
|
-
/**
|
|
57
|
-
* Update user configuration
|
|
58
|
-
*/
|
|
59
|
-
setConfig(config: Partial<UserConfig>): Promise<void>;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Record a user's interaction with a card
|
|
63
|
-
*/
|
|
64
|
-
putCardRecord<T extends CardRecord>(record: T): Promise<CardHistory<CardRecord>>;
|
|
65
|
-
|
|
66
26
|
/**
|
|
67
27
|
* Get cards that the user has seen
|
|
68
28
|
*/
|
|
@@ -71,17 +31,7 @@ export interface UserDBInterface extends DocumentUpdater {
|
|
|
71
31
|
/**
|
|
72
32
|
* Get cards that are actively scheduled for review
|
|
73
33
|
*/
|
|
74
|
-
getActiveCards(): Promise<
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Register user for a course
|
|
78
|
-
*/
|
|
79
|
-
registerForCourse(courseId: string, previewMode?: boolean): Promise<PouchDB.Core.Response>;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Drop a course registration
|
|
83
|
-
*/
|
|
84
|
-
dropCourse(courseId: string, dropStatus?: string): Promise<PouchDB.Core.Response>;
|
|
34
|
+
getActiveCards(): Promise<QualifiedCardID[]>;
|
|
85
35
|
|
|
86
36
|
/**
|
|
87
37
|
* Get user's course registrations
|
|
@@ -106,6 +56,43 @@ export interface UserDBInterface extends DocumentUpdater {
|
|
|
106
56
|
|
|
107
57
|
getActivityRecords(): Promise<ActivityRecord[]>;
|
|
108
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Get user's classroom registrations
|
|
61
|
+
*/
|
|
62
|
+
getUserClassrooms(): Promise<ClassroomRegistrationDoc>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get user's active classes
|
|
66
|
+
*/
|
|
67
|
+
getActiveClasses(): Promise<string[]>;
|
|
68
|
+
|
|
69
|
+
getCourseInterface(courseId: string): Promise<UsrCrsDataInterface>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* User data mutation operations
|
|
74
|
+
*/
|
|
75
|
+
export interface UserDBWriter extends DocumentUpdater {
|
|
76
|
+
/**
|
|
77
|
+
* Update user configuration
|
|
78
|
+
*/
|
|
79
|
+
setConfig(config: Partial<UserConfig>): Promise<void>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Record a user's interaction with a card
|
|
83
|
+
*/
|
|
84
|
+
putCardRecord<T extends CardRecord>(record: T): Promise<CardHistory<CardRecord>>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Register user for a course
|
|
88
|
+
*/
|
|
89
|
+
registerForCourse(courseId: string, previewMode?: boolean): Promise<PouchDB.Core.Response>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Drop a course registration
|
|
93
|
+
*/
|
|
94
|
+
dropCourse(courseId: string, dropStatus?: string): Promise<PouchDB.Core.Response>;
|
|
95
|
+
|
|
109
96
|
/**
|
|
110
97
|
* Schedule a card for review
|
|
111
98
|
*/
|
|
@@ -137,28 +124,57 @@ export interface UserDBInterface extends DocumentUpdater {
|
|
|
137
124
|
dropFromClassroom(classId: string): Promise<PouchDB.Core.Response>;
|
|
138
125
|
|
|
139
126
|
/**
|
|
140
|
-
*
|
|
127
|
+
* Update user's ELO rating for a course
|
|
141
128
|
*/
|
|
142
|
-
|
|
129
|
+
updateUserElo(courseId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
|
|
143
130
|
|
|
144
131
|
/**
|
|
145
|
-
*
|
|
132
|
+
* Reset all user data (progress, registrations, etc.) while preserving authentication
|
|
146
133
|
*/
|
|
147
|
-
|
|
134
|
+
resetUserData(): Promise<{ status: Status; error?: string }>;
|
|
135
|
+
}
|
|
148
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Authentication and account management operations
|
|
139
|
+
*/
|
|
140
|
+
export interface UserDBAuthenticator {
|
|
149
141
|
/**
|
|
150
|
-
*
|
|
142
|
+
* Create a new user account
|
|
151
143
|
*/
|
|
152
|
-
|
|
144
|
+
createAccount(
|
|
145
|
+
username: string,
|
|
146
|
+
password: string
|
|
147
|
+
): Promise<{
|
|
148
|
+
status: Status;
|
|
149
|
+
error: string;
|
|
150
|
+
}>;
|
|
153
151
|
|
|
154
152
|
/**
|
|
155
|
-
*
|
|
153
|
+
* Log in as a user
|
|
156
154
|
*/
|
|
157
|
-
|
|
155
|
+
login(
|
|
156
|
+
username: string,
|
|
157
|
+
password: string
|
|
158
|
+
): Promise<{
|
|
159
|
+
ok: boolean;
|
|
160
|
+
name?: string;
|
|
161
|
+
roles?: string[];
|
|
162
|
+
}>;
|
|
158
163
|
|
|
159
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Log out the current user
|
|
166
|
+
*/
|
|
167
|
+
logout(): Promise<{
|
|
168
|
+
ok: boolean;
|
|
169
|
+
}>;
|
|
160
170
|
}
|
|
161
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Complete user database interface - combines all user operations
|
|
174
|
+
* This maintains backward compatibility with existing code
|
|
175
|
+
*/
|
|
176
|
+
export interface UserDBInterface extends UserDBReader, UserDBWriter, UserDBAuthenticator {}
|
|
177
|
+
|
|
162
178
|
export interface UserCourseSettings {
|
|
163
179
|
[setting: string]: string | number | boolean;
|
|
164
180
|
}
|
|
@@ -3,7 +3,7 @@ import { CourseDBInterface } from '../interfaces/courseDB';
|
|
|
3
3
|
import { UserDBInterface } from '../interfaces/userDB';
|
|
4
4
|
import { ContentNavigator } from './index';
|
|
5
5
|
import { CourseElo } from '@vue-skuilder/common';
|
|
6
|
-
import { StudySessionReviewItem, StudySessionNewItem } from '..';
|
|
6
|
+
import { StudySessionReviewItem, StudySessionNewItem, QualifiedCardID } from '..';
|
|
7
7
|
|
|
8
8
|
export default class ELONavigator extends ContentNavigator {
|
|
9
9
|
user: UserDBInterface;
|
|
@@ -59,13 +59,16 @@ export default class ELONavigator extends ContentNavigator {
|
|
|
59
59
|
async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
|
|
60
60
|
const activeCards = await this.user.getActiveCards();
|
|
61
61
|
return (
|
|
62
|
-
await this.course.getCardsCenteredAtELO(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
await this.course.getCardsCenteredAtELO(
|
|
63
|
+
{ limit: limit, elo: 'user' },
|
|
64
|
+
(c: QualifiedCardID) => {
|
|
65
|
+
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
66
|
+
return false;
|
|
67
|
+
} else {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
67
70
|
}
|
|
68
|
-
|
|
71
|
+
)
|
|
69
72
|
).map((c) => {
|
|
70
73
|
return {
|
|
71
74
|
...c,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CourseDBInterface, QualifiedCardID, StudySessionNewItem, StudySessionReviewItem, UserDBInterface } from '..';
|
|
2
|
+
import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
|
|
3
|
+
import { ScheduledCard } from '../types/user';
|
|
4
|
+
import { ContentNavigator } from './index';
|
|
5
|
+
import { logger } from '../../util/logger';
|
|
6
|
+
|
|
7
|
+
export default class HardcodedOrderNavigator extends ContentNavigator {
|
|
8
|
+
private orderedCardIds: string[] = [];
|
|
9
|
+
private user: UserDBInterface;
|
|
10
|
+
private course: CourseDBInterface;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
user: UserDBInterface,
|
|
14
|
+
course: CourseDBInterface,
|
|
15
|
+
strategyData: ContentNavigationStrategyData
|
|
16
|
+
) {
|
|
17
|
+
super();
|
|
18
|
+
this.user = user;
|
|
19
|
+
this.course = course;
|
|
20
|
+
|
|
21
|
+
if (strategyData.serializedData) {
|
|
22
|
+
try {
|
|
23
|
+
this.orderedCardIds = JSON.parse(strategyData.serializedData);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
logger.error('Failed to parse serializedData for HardcodedOrderNavigator', e);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
31
|
+
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
32
|
+
return reviews.map((r) => {
|
|
33
|
+
return {
|
|
34
|
+
...r,
|
|
35
|
+
contentSourceType: 'course',
|
|
36
|
+
contentSourceID: this.course.getCourseID(),
|
|
37
|
+
cardID: r.cardId,
|
|
38
|
+
courseID: r.courseId,
|
|
39
|
+
reviewID: r._id,
|
|
40
|
+
status: 'review',
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
|
|
46
|
+
const activeCardIds = (await this.user.getActiveCards()).map((c: QualifiedCardID) => c.cardID);
|
|
47
|
+
|
|
48
|
+
const newCardIds = this.orderedCardIds.filter(
|
|
49
|
+
(cardId) => !activeCardIds.includes(cardId)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const cardsToReturn = newCardIds.slice(0, limit);
|
|
53
|
+
|
|
54
|
+
return cardsToReturn.map((cardId) => {
|
|
55
|
+
return {
|
|
56
|
+
cardID: cardId,
|
|
57
|
+
courseID: this.course.getCourseID(),
|
|
58
|
+
contentSourceType: 'course',
|
|
59
|
+
contentSourceID: this.course.getCourseID(),
|
|
60
|
+
status: 'new',
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -11,6 +11,7 @@ import { logger } from '../../util/logger';
|
|
|
11
11
|
|
|
12
12
|
export enum Navigators {
|
|
13
13
|
ELO = 'elo',
|
|
14
|
+
HARDCODED = 'hardcodedOrder',
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -32,7 +33,7 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
32
33
|
let NavigatorImpl;
|
|
33
34
|
|
|
34
35
|
// Try different extension variations
|
|
35
|
-
const variations = ['', '.js', '
|
|
36
|
+
const variations = ['.ts', '.js', ''];
|
|
36
37
|
|
|
37
38
|
for (const ext of variations) {
|
|
38
39
|
try {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { DocType, SkuilderCourseData } from './types-legacy';
|
|
2
|
+
import type { DocTypePrefixes } from './types-legacy';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
*
|
|
5
6
|
*/
|
|
6
7
|
export interface ContentNavigationStrategyData extends SkuilderCourseData {
|
|
7
|
-
|
|
8
|
+
_id: `${typeof DocTypePrefixes[DocType.NAVIGATION_STRATEGY]}-${string}`;
|
|
8
9
|
docType: DocType.NAVIGATION_STRATEGY;
|
|
9
10
|
name: string;
|
|
10
11
|
description: string;
|
|
@@ -21,6 +21,11 @@ export enum DocType {
|
|
|
21
21
|
NAVIGATION_STRATEGY = 'NAVIGATION_STRATEGY',
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface QualifiedCardID {
|
|
25
|
+
courseID: string;
|
|
26
|
+
cardID: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
/**
|
|
25
30
|
* Interface for all data on course content and pedagogy stored
|
|
26
31
|
* in the c/pouch database.
|
|
@@ -86,7 +91,7 @@ export interface QuestionData extends SkuilderCourseData {
|
|
|
86
91
|
dataShapeList: PouchDB.Core.DocumentId[];
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
export const DocTypePrefixes
|
|
94
|
+
export const DocTypePrefixes = {
|
|
90
95
|
[DocType.CARD]: 'c',
|
|
91
96
|
[DocType.DISPLAYABLE_DATA]: 'dd',
|
|
92
97
|
[DocType.TAG]: 'TAG',
|
|
@@ -98,7 +103,7 @@ export const DocTypePrefixes: Record<string, string> = {
|
|
|
98
103
|
[DocType.VIEW]: 'VIEW',
|
|
99
104
|
[DocType.PEDAGOGY]: 'PEDAGOGY',
|
|
100
105
|
[DocType.NAVIGATION_STRATEGY]: 'NAVIGATION_STRATEGY',
|
|
101
|
-
};
|
|
106
|
+
} as const;
|
|
102
107
|
|
|
103
108
|
export interface CardHistory<T extends CardRecord> {
|
|
104
109
|
_id: PouchDB.Core.DocumentId;
|
|
@@ -217,6 +217,10 @@ Currently logged-in as ${this._username}.`
|
|
|
217
217
|
return ret;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
public async get<T>(id: string): Promise<T & PouchDB.Core.RevisionIdMeta> {
|
|
221
|
+
return this.localDB.get<T>(id);
|
|
222
|
+
}
|
|
223
|
+
|
|
220
224
|
public update<T extends PouchDB.Core.Document<object>>(id: string, update: Update<T>) {
|
|
221
225
|
return this.updateQueue.update(id, update);
|
|
222
226
|
}
|
|
@@ -273,7 +277,12 @@ Currently logged-in as ${this._username}.`
|
|
|
273
277
|
include_docs: true,
|
|
274
278
|
});
|
|
275
279
|
|
|
276
|
-
return reviews.rows.map((r) =>
|
|
280
|
+
return reviews.rows.map((r) => {
|
|
281
|
+
return {
|
|
282
|
+
courseID: r.doc!.courseId,
|
|
283
|
+
cardID: r.doc!.cardId,
|
|
284
|
+
};
|
|
285
|
+
});
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
public async getActivityRecords(): Promise<ActivityRecord[]> {
|
|
@@ -620,8 +629,18 @@ Currently logged-in as ${this._username}.`
|
|
|
620
629
|
this.setDBandQ();
|
|
621
630
|
|
|
622
631
|
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
623
|
-
|
|
624
|
-
|
|
632
|
+
this.applyDesignDocs().catch((error) => {
|
|
633
|
+
log(`Error in applyDesignDocs background task: ${error}`);
|
|
634
|
+
if (error && typeof error === 'object') {
|
|
635
|
+
log(`Full error details in applyDesignDocs: ${JSON.stringify(error)}`);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
this.deduplicateReviews().catch((error) => {
|
|
639
|
+
log(`Error in deduplicateReviews background task: ${error}`);
|
|
640
|
+
if (error && typeof error === 'object') {
|
|
641
|
+
log(`Full error details in background task: ${JSON.stringify(error)}`);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
625
644
|
BaseUser._initialized = true;
|
|
626
645
|
}
|
|
627
646
|
|
|
@@ -641,12 +660,18 @@ Currently logged-in as ${this._username}.`
|
|
|
641
660
|
];
|
|
642
661
|
|
|
643
662
|
private async applyDesignDocs() {
|
|
663
|
+
log(`Starting applyDesignDocs for user: ${this._username}`);
|
|
664
|
+
log(`Remote DB name: ${this.remoteDB.name || 'unknown'}`);
|
|
665
|
+
|
|
644
666
|
if (this._username === 'admin') {
|
|
645
667
|
// Skip admin user
|
|
668
|
+
log('Skipping design docs for admin user');
|
|
646
669
|
return;
|
|
647
670
|
}
|
|
648
671
|
|
|
672
|
+
log(`Applying ${BaseUser.designDocs.length} design docs`);
|
|
649
673
|
for (const doc of BaseUser.designDocs) {
|
|
674
|
+
log(`Applying design doc: ${doc._id}`);
|
|
650
675
|
try {
|
|
651
676
|
// Try to get existing doc
|
|
652
677
|
try {
|
|
@@ -736,17 +761,21 @@ Currently logged-in as ${this._username}.`
|
|
|
736
761
|
} catch (e) {
|
|
737
762
|
const reason = e as PouchError;
|
|
738
763
|
if (reason.status === 404) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
764
|
+
try {
|
|
765
|
+
const initCardHistory: CardHistory<T> = {
|
|
766
|
+
_id: cardHistoryID,
|
|
767
|
+
cardID: record.cardID,
|
|
768
|
+
courseID: record.courseID,
|
|
769
|
+
records: [record],
|
|
770
|
+
lapses: 0,
|
|
771
|
+
streak: 0,
|
|
772
|
+
bestInterval: 0,
|
|
773
|
+
};
|
|
774
|
+
const putResult = await this.writeDB.put<CardHistory<T>>(initCardHistory);
|
|
775
|
+
return { ...initCardHistory, _rev: putResult.rev };
|
|
776
|
+
} catch (creationError) {
|
|
777
|
+
throw new Error(`Failed to create CardHistory for ${cardHistoryID}. Reason: ${creationError}`);
|
|
778
|
+
}
|
|
750
779
|
} else {
|
|
751
780
|
throw new Error(`putCardRecord failed because of:
|
|
752
781
|
name:${reason.name}
|
|
@@ -759,6 +788,8 @@ Currently logged-in as ${this._username}.`
|
|
|
759
788
|
private async deduplicateReviews() {
|
|
760
789
|
try {
|
|
761
790
|
log('Starting deduplication of scheduled reviews...');
|
|
791
|
+
log(`Remote DB name: ${this.remoteDB.name || 'unknown'}`);
|
|
792
|
+
log(`Write DB name: ${this.writeDB.name || 'unknown'}`);
|
|
762
793
|
/**
|
|
763
794
|
* Maps the qualified-id of a scheduled review card to
|
|
764
795
|
* the docId of the same scheduled review.
|
|
@@ -770,6 +801,9 @@ Currently logged-in as ${this._username}.`
|
|
|
770
801
|
const reviewsMap: { [index: string]: string } = {};
|
|
771
802
|
const duplicateDocIds: string[] = [];
|
|
772
803
|
|
|
804
|
+
log(
|
|
805
|
+
`Attempting to query remoteDB for reviewCards/reviewCards. Database: ${this.remoteDB.name || 'unknown'}`
|
|
806
|
+
);
|
|
773
807
|
const scheduledReviews = await this.remoteDB.query<{
|
|
774
808
|
id: string;
|
|
775
809
|
value: string;
|
|
@@ -817,6 +851,18 @@ Currently logged-in as ${this._username}.`
|
|
|
817
851
|
}
|
|
818
852
|
} catch (error) {
|
|
819
853
|
log(`Error during review deduplication: ${error}`);
|
|
854
|
+
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
|
|
855
|
+
log(
|
|
856
|
+
`Database not found (404) during review deduplication. Database: ${this.remoteDB.name || 'unknown'}`
|
|
857
|
+
);
|
|
858
|
+
log(
|
|
859
|
+
`This might indicate the user database doesn't exist or the reviewCards view isn't available`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
// Log full error details for debugging
|
|
863
|
+
if (error && typeof error === 'object') {
|
|
864
|
+
log(`Full error details: ${JSON.stringify(error)}`);
|
|
865
|
+
}
|
|
820
866
|
}
|
|
821
867
|
}
|
|
822
868
|
|
|
@@ -8,7 +8,7 @@ import type { SyncStrategy } from '../common/SyncStrategy';
|
|
|
8
8
|
import type { AccountCreationResult, AuthenticationResult } from '../common/types';
|
|
9
9
|
import { getLocalUserDB, hexEncode, updateGuestAccountExpirationDate } from '../common';
|
|
10
10
|
import pouch from './pouchdb-setup';
|
|
11
|
-
import {
|
|
11
|
+
import { createPouchDBConfig } from './index';
|
|
12
12
|
import { getLoggedInUsername } from './auth';
|
|
13
13
|
|
|
14
14
|
const log = (s: any) => {
|
|
@@ -207,7 +207,7 @@ export class CouchDBSyncStrategy implements SyncStrategy {
|
|
|
207
207
|
// see: https://github.com/pouchdb-community/pouchdb-authentication/issues/239
|
|
208
208
|
const ret = new pouch(
|
|
209
209
|
ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
|
|
210
|
-
|
|
210
|
+
createPouchDBConfig()
|
|
211
211
|
);
|
|
212
212
|
|
|
213
213
|
if (guestAccount) {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CourseDBInterface,
|
|
8
8
|
DataLayerProvider,
|
|
9
9
|
UserDBInterface,
|
|
10
|
+
UserDBReader,
|
|
10
11
|
} from '../../core/interfaces';
|
|
11
12
|
import { logger } from '../../util/logger';
|
|
12
13
|
import { initializeDataDirectory } from '../../util/dataDirectory';
|
|
@@ -107,6 +108,26 @@ export class CouchDataLayerProvider implements DataLayerProvider {
|
|
|
107
108
|
return new AdminDB();
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
async createUserReaderForUser(targetUsername: string): Promise<UserDBReader> {
|
|
112
|
+
// Security check: only admin can access other users' data
|
|
113
|
+
const requestingUsername = await getLoggedInUsername();
|
|
114
|
+
if (requestingUsername !== 'admin') {
|
|
115
|
+
throw new Error('Unauthorized: Only admin users can access other users\' data');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
logger.info(`Admin user '${requestingUsername}' requesting UserDBReader for '${targetUsername}'`);
|
|
119
|
+
|
|
120
|
+
// Create a new sync strategy for the target user
|
|
121
|
+
const syncStrategy = new CouchDBSyncStrategy();
|
|
122
|
+
|
|
123
|
+
// Create a BaseUser instance for the target user
|
|
124
|
+
// Note: This creates a read-capable user instance without affecting the current session
|
|
125
|
+
const targetUserDB = await BaseUser.instance(syncStrategy, targetUsername);
|
|
126
|
+
|
|
127
|
+
// Return as UserDBReader (which BaseUser implements since UserDBInterface extends UserDBReader)
|
|
128
|
+
return targetUserDB as UserDBReader;
|
|
129
|
+
}
|
|
130
|
+
|
|
110
131
|
isReadOnly(): boolean {
|
|
111
132
|
return false;
|
|
112
133
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pouch from './pouchdb-setup';
|
|
2
2
|
import { ENV } from '@db/factory';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
createPouchDBConfig,
|
|
5
5
|
getStartAndEndKeys,
|
|
6
6
|
getCredentialledCourseConfig,
|
|
7
7
|
updateCredentialledCourseConfig,
|
|
@@ -21,7 +21,7 @@ export class AdminDB implements AdminDBInterface {
|
|
|
21
21
|
// if the user is not an admin
|
|
22
22
|
this.usersDB = new pouch(
|
|
23
23
|
ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + '_users',
|
|
24
|
-
|
|
24
|
+
createPouchDBConfig()
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|