@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.
Files changed (76) hide show
  1. package/dist/core/index.d.mts +7 -6
  2. package/dist/core/index.d.ts +7 -6
  3. package/dist/core/index.js +358 -87
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +358 -87
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-BiP3kWix.d.mts} +8 -1
  8. package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-DSdeyRT3.d.ts} +8 -1
  9. package/dist/impl/couch/index.d.mts +19 -7
  10. package/dist/impl/couch/index.d.ts +19 -7
  11. package/dist/impl/couch/index.js +375 -100
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +374 -99
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.mts +23 -8
  16. package/dist/impl/static/index.d.ts +23 -8
  17. package/dist/impl/static/index.js +289 -85
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +289 -85
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-CUNnL38E.d.mts → index-Bmll7Xse.d.mts} +1 -1
  22. package/dist/{index-CLL31bEy.d.ts → index-CD8BZz2k.d.ts} +1 -1
  23. package/dist/index.d.mts +123 -20
  24. package/dist/index.d.ts +123 -20
  25. package/dist/index.js +1133 -343
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +1137 -343
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/pouch/index.d.mts +1 -0
  30. package/dist/pouch/index.d.ts +1 -0
  31. package/dist/pouch/index.js +49 -0
  32. package/dist/pouch/index.js.map +1 -0
  33. package/dist/pouch/index.mjs +16 -0
  34. package/dist/pouch/index.mjs.map +1 -0
  35. package/dist/{types-BefDGkKa.d.ts → types-CewsN87z.d.ts} +1 -1
  36. package/dist/{types-DC-ckZug.d.mts → types-Dbp5DaRR.d.mts} +1 -1
  37. package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-6ettoclI.d.mts} +17 -2
  38. package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-6ettoclI.d.ts} +17 -2
  39. package/dist/{userDB-DusL7OXe.d.ts → userDB-C4yyAnpp.d.mts} +89 -56
  40. package/dist/{userDB-C33Hzjgn.d.mts → userDB-CD6s6ZCp.d.ts} +89 -56
  41. package/dist/util/packer/index.d.mts +3 -3
  42. package/dist/util/packer/index.d.ts +3 -3
  43. package/package.json +3 -3
  44. package/src/core/interfaces/contentSource.ts +3 -2
  45. package/src/core/interfaces/courseDB.ts +26 -3
  46. package/src/core/interfaces/dataLayerProvider.ts +9 -1
  47. package/src/core/interfaces/userDB.ts +80 -64
  48. package/src/core/navigators/elo.ts +10 -7
  49. package/src/core/navigators/hardcodedOrder.ts +64 -0
  50. package/src/core/navigators/index.ts +2 -1
  51. package/src/core/types/contentNavigationStrategy.ts +2 -1
  52. package/src/core/types/types-legacy.ts +7 -2
  53. package/src/impl/common/BaseUserDB.ts +60 -14
  54. package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
  55. package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
  56. package/src/impl/couch/adminDB.ts +2 -2
  57. package/src/impl/couch/auth.ts +13 -4
  58. package/src/impl/couch/classroomDB.ts +10 -12
  59. package/src/impl/couch/courseAPI.ts +2 -2
  60. package/src/impl/couch/courseDB.ts +204 -38
  61. package/src/impl/couch/courseLookupDB.ts +4 -3
  62. package/src/impl/couch/index.ts +36 -4
  63. package/src/impl/couch/pouchdb-setup.ts +3 -3
  64. package/src/impl/couch/updateQueue.ts +59 -36
  65. package/src/impl/static/StaticDataLayerProvider.ts +68 -17
  66. package/src/impl/static/courseDB.ts +64 -20
  67. package/src/impl/static/coursesDB.ts +10 -6
  68. package/src/pouch/index.ts +2 -0
  69. package/src/study/ItemQueue.ts +58 -0
  70. package/src/study/SessionController.ts +182 -111
  71. package/src/study/SpacedRepetition.ts +1 -1
  72. package/src/study/services/CardHydrationService.ts +153 -0
  73. package/src/study/services/EloService.ts +85 -0
  74. package/src/study/services/ResponseProcessor.ts +224 -0
  75. package/src/study/services/SrsService.ts +44 -0
  76. 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.11-9",
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.11-9",
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.10"
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(elo: number, limit?: number): Promise<string[]>;
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?: (id: string) => boolean
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
- * User data and authentication
14
+ * Read-only user data operations
15
15
  */
16
- export interface UserDBInterface extends DocumentUpdater {
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<string[]>;
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
- * Get user's classroom registrations
127
+ * Update user's ELO rating for a course
141
128
  */
142
- getUserClassrooms(): Promise<ClassroomRegistrationDoc>;
129
+ updateUserElo(courseId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
143
130
 
144
131
  /**
145
- * Get user's active classes
132
+ * Reset all user data (progress, registrations, etc.) while preserving authentication
146
133
  */
147
- getActiveClasses(): Promise<string[]>;
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
- * Update user's ELO rating for a course
142
+ * Create a new user account
151
143
  */
152
- updateUserElo(courseId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
144
+ createAccount(
145
+ username: string,
146
+ password: string
147
+ ): Promise<{
148
+ status: Status;
149
+ error: string;
150
+ }>;
153
151
 
154
152
  /**
155
- * Reset all user data (progress, registrations, etc.) while preserving authentication
153
+ * Log in as a user
156
154
  */
157
- resetUserData(): Promise<{ status: Status; error?: string }>;
155
+ login(
156
+ username: string,
157
+ password: string
158
+ ): Promise<{
159
+ ok: boolean;
160
+ name?: string;
161
+ roles?: string[];
162
+ }>;
158
163
 
159
- getCourseInterface(courseId: string): Promise<UsrCrsDataInterface>;
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({ limit: limit, elo: 'user' }, (c: string) => {
63
- if (activeCards.some((ac) => c.includes(ac))) {
64
- return false;
65
- } else {
66
- return true;
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', '.ts'];
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
- id: string;
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: Record<string, string> = {
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) => `${r.doc!.courseId}-${r.doc!.cardId}`);
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
- void this.applyDesignDocs();
624
- void this.deduplicateReviews();
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
- const initCardHistory: CardHistory<T> = {
740
- _id: cardHistoryID,
741
- cardID: record.cardID,
742
- courseID: record.courseID,
743
- records: [record],
744
- lapses: 0,
745
- streak: 0,
746
- bestInterval: 0,
747
- };
748
- const putResult = await this.writeDB.put<CardHistory<T>>(initCardHistory);
749
- return { ...initCardHistory, _rev: putResult.rev };
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 { pouchDBincludeCredentialsConfig } from './index';
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
- pouchDBincludeCredentialsConfig
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
- pouchDBincludeCredentialsConfig,
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
- pouchDBincludeCredentialsConfig
24
+ createPouchDBConfig()
25
25
  );
26
26
  }
27
27