@vue-skuilder/db 0.1.11-7 → 0.1.11

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 (68) hide show
  1. package/dist/core/index.d.mts +5 -5
  2. package/dist/core/index.d.ts +5 -5
  3. package/dist/core/index.js +212 -50
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +212 -50
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{dataLayerProvider-DqtNroSh.d.ts → dataLayerProvider-VieuAAkV.d.mts} +8 -1
  8. package/dist/{dataLayerProvider-BInqI_RF.d.mts → dataLayerProvider-juuqUHOP.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 +229 -63
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +228 -62
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.mts +13 -6
  16. package/dist/impl/static/index.d.ts +13 -6
  17. package/dist/impl/static/index.js +142 -46
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +142 -46
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-CLL31bEy.d.ts → index-CWY6yhkV.d.ts} +1 -1
  22. package/dist/{index-CUNnL38E.d.mts → index-DZyxHCcf.d.mts} +1 -1
  23. package/dist/index.d.mts +28 -20
  24. package/dist/index.d.ts +28 -20
  25. package/dist/index.js +374 -108
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +378 -108
  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-DC-ckZug.d.mts → types-Che4wTwA.d.mts} +1 -1
  36. package/dist/{types-BefDGkKa.d.ts → types-DtoI27Xh.d.ts} +1 -1
  37. package/dist/{types-legacy-Birv-Jx6.d.mts → types-legacy-B8ahaCbj.d.mts} +5 -1
  38. package/dist/{types-legacy-Birv-Jx6.d.ts → types-legacy-B8ahaCbj.d.ts} +5 -1
  39. package/dist/{userDB-C33Hzjgn.d.mts → userDB-B7zTQ123.d.ts} +88 -55
  40. package/dist/{userDB-DusL7OXe.d.ts → userDB-DJ8HMw83.d.mts} +88 -55
  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/index.ts +1 -1
  50. package/src/core/types/types-legacy.ts +5 -0
  51. package/src/impl/common/BaseUserDB.ts +45 -3
  52. package/src/impl/couch/CouchDBSyncStrategy.ts +2 -2
  53. package/src/impl/couch/PouchDataLayerProvider.ts +21 -0
  54. package/src/impl/couch/adminDB.ts +2 -2
  55. package/src/impl/couch/auth.ts +13 -4
  56. package/src/impl/couch/classroomDB.ts +10 -12
  57. package/src/impl/couch/courseAPI.ts +2 -2
  58. package/src/impl/couch/courseDB.ts +130 -11
  59. package/src/impl/couch/courseLookupDB.ts +4 -3
  60. package/src/impl/couch/index.ts +36 -4
  61. package/src/impl/couch/pouchdb-setup.ts +3 -3
  62. package/src/impl/couch/updateQueue.ts +51 -33
  63. package/src/impl/static/StaticDataLayerProvider.ts +11 -0
  64. package/src/impl/static/courseDB.ts +47 -8
  65. package/src/pouch/index.ts +2 -0
  66. package/src/study/SessionController.ts +168 -51
  67. package/src/study/SpacedRepetition.ts +1 -1
  68. package/tsup.config.ts +1 -0
@@ -18,7 +18,14 @@ import {
18
18
  StudySessionNewItem,
19
19
  StudySessionReviewItem,
20
20
  } from '../../core/interfaces/contentSource';
21
- import { CardData, DocType, SkuilderCourseData, Tag, TagStub } from '../../core/types/types-legacy';
21
+ import {
22
+ CardData,
23
+ DocType,
24
+ QualifiedCardID,
25
+ SkuilderCourseData,
26
+ Tag,
27
+ TagStub,
28
+ } from '../../core/types/types-legacy';
22
29
  import { logger } from '../../util/logger';
23
30
  import { GET_CACHED } from './clientCache';
24
31
  import { addNote55, addTagToCard, getCredentialledCourseConfig, getTagID } from './courseAPI';
@@ -263,7 +270,7 @@ export class CourseDB implements StudyContentSource, CourseDBInterface {
263
270
  limit: aboveLimit,
264
271
  startkey: elo + 1,
265
272
  });
266
- // console.log(JSON.stringify(below));
273
+ // logger.log(JSON.stringify(below));
267
274
 
268
275
  let cards = below.rows;
269
276
  cards = cards.concat(above.rows);
@@ -277,7 +284,13 @@ export class CourseDB implements StudyContentSource, CourseDBInterface {
277
284
  return s;
278
285
  }
279
286
  })
280
- .map((c) => `${this.id}-${c.id}-${c.key}`);
287
+ .map((c) => {
288
+ return {
289
+ courseID: this.id,
290
+ cardID: c.id,
291
+ elo: c.key,
292
+ };
293
+ });
281
294
 
282
295
  const str = `below:\n${below.rows.map((r) => `\t${r.id}-${r.key}\n`)}
283
296
 
@@ -342,7 +355,13 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
342
355
  tagId: string,
343
356
  updateELO?: boolean
344
357
  ): Promise<PouchDB.Core.Response> {
345
- return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
358
+ return await addTagToCard(
359
+ this.id,
360
+ cardId,
361
+ tagId,
362
+ (await this._getCurrentUser()).getUsername(),
363
+ updateELO
364
+ );
346
365
  }
347
366
 
348
367
  async removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response> {
@@ -535,7 +554,7 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
535
554
  limit: 99,
536
555
  elo: 'user',
537
556
  },
538
- filter?: (a: string) => boolean
557
+ filter?: (a: QualifiedCardID) => boolean
539
558
  ): Promise<StudySessionItem[]> {
540
559
  let targetElo: number;
541
560
 
@@ -554,12 +573,12 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
554
573
  } else if (options.elo === 'random') {
555
574
  const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
556
575
  targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
557
- // console.log(`Picked ${targetElo} from [${bounds.low}, ${bounds.high}]`);
576
+ // logger.log(`Picked ${targetElo} from [${bounds.low}, ${bounds.high}]`);
558
577
  } else {
559
578
  targetElo = options.elo;
560
579
  }
561
580
 
562
- let cards: string[] = [];
581
+ let cards: (QualifiedCardID & { elo?: number })[] = [];
563
582
  let mult: number = 4;
564
583
  let previousCount: number = -1;
565
584
  let newCount: number = 0;
@@ -579,7 +598,11 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
579
598
  mult *= 2;
580
599
  }
581
600
 
582
- const selectedCards: string[] = [];
601
+ const selectedCards: {
602
+ courseID: string;
603
+ cardID: string;
604
+ elo?: number;
605
+ }[] = [];
583
606
 
584
607
  while (selectedCards.length < options.limit && cards.length > 0) {
585
608
  const index = randIntWeightedTowardZero(cards.length);
@@ -588,17 +611,113 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
588
611
  }
589
612
 
590
613
  return selectedCards.map((c) => {
591
- const split = c.split('-');
592
614
  return {
593
615
  courseID: this.id,
594
- cardID: split[1],
595
- qualifiedID: `${split[0]}-${split[1]}`,
616
+ cardID: c.cardID,
596
617
  contentSourceType: 'course',
597
618
  contentSourceID: this.id,
619
+ elo: c.elo,
598
620
  status: 'new',
599
621
  };
600
622
  });
601
623
  }
624
+
625
+ // Admin search methods
626
+ public async searchCards(query: string): Promise<any[]> {
627
+ logger.log(`[CourseDB ${this.id}] Searching for: "${query}"`);
628
+
629
+ // Try multiple search approaches
630
+ let displayableData;
631
+
632
+ try {
633
+ // Try regex search on the correct data structure: data[0].data
634
+ displayableData = await this.db.find({
635
+ selector: {
636
+ docType: 'DISPLAYABLE_DATA',
637
+ 'data.0.data': { $regex: `.*${query}.*` },
638
+ },
639
+ });
640
+ logger.log(`[CourseDB ${this.id}] Regex search on data[0].data successful`);
641
+ } catch (regexError) {
642
+ logger.log(
643
+ `[CourseDB ${this.id}] Regex search failed, falling back to manual search:`,
644
+ regexError
645
+ );
646
+
647
+ // Fallback: get all displayable data and filter manually
648
+ const allDisplayable = await this.db.find({
649
+ selector: {
650
+ docType: 'DISPLAYABLE_DATA',
651
+ },
652
+ });
653
+
654
+ logger.log(
655
+ `[CourseDB ${this.id}] Retrieved ${allDisplayable.docs.length} documents for manual filtering`
656
+ );
657
+
658
+ displayableData = {
659
+ docs: allDisplayable.docs.filter((doc) => {
660
+ // Search entire document as JSON string - inclusive approach for admin tool
661
+ const docString = JSON.stringify(doc).toLowerCase();
662
+ const match = docString.includes(query.toLowerCase());
663
+ if (match) {
664
+ logger.log(`[CourseDB ${this.id}] Manual match found in document: ${doc._id}`);
665
+ }
666
+ return match;
667
+ }),
668
+ };
669
+ }
670
+
671
+ logger.log(
672
+ `[CourseDB ${this.id}] Found ${displayableData.docs.length} displayable data documents`
673
+ );
674
+
675
+ if (displayableData.docs.length === 0) {
676
+ // Debug: Let's see what displayable data exists
677
+ const allDisplayableData = await this.db.find({
678
+ selector: {
679
+ docType: 'DISPLAYABLE_DATA',
680
+ },
681
+ limit: 5, // Just sample a few
682
+ });
683
+
684
+ logger.log(
685
+ `[CourseDB ${this.id}] Sample displayable data:`,
686
+ allDisplayableData.docs.map((d) => ({
687
+ id: d._id,
688
+ docType: (d as any).docType,
689
+ dataStructure: (d as any).data ? Object.keys((d as any).data) : 'no data field',
690
+ dataContent: (d as any).data,
691
+ fullDoc: d,
692
+ }))
693
+ );
694
+ }
695
+
696
+ const allResults: any[] = [];
697
+
698
+ for (const dd of displayableData.docs) {
699
+ const cards = await this.db.find({
700
+ selector: {
701
+ docType: 'CARD',
702
+ id_displayable_data: { $in: [dd._id] },
703
+ },
704
+ });
705
+
706
+ logger.log(
707
+ `[CourseDB ${this.id}] Displayable data ${dd._id} linked to ${cards.docs.length} cards`
708
+ );
709
+ allResults.push(...cards.docs);
710
+ }
711
+
712
+ logger.log(`[CourseDB ${this.id}] Total cards found: ${allResults.length}`);
713
+ return allResults;
714
+ }
715
+
716
+ public async find(
717
+ request: PouchDB.Find.FindRequest<any>
718
+ ): Promise<PouchDB.Find.FindResponse<any>> {
719
+ return this.db.find(request);
720
+ }
602
721
  }
603
722
 
604
723
  /**
@@ -105,15 +105,15 @@ export default class CourseLookup {
105
105
  * @returns Promise<void>
106
106
  */
107
107
  static async addWithId(
108
- courseId: string,
109
- courseName: string,
108
+ courseId: string,
109
+ courseName: string,
110
110
  disambiguator?: string
111
111
  ): Promise<void> {
112
112
  const doc: Omit<CourseLookupDoc, '_rev'> = {
113
113
  _id: courseId,
114
114
  name: courseName,
115
115
  };
116
-
116
+
117
117
  if (disambiguator) {
118
118
  doc.disambiguator = disambiguator;
119
119
  }
@@ -130,6 +130,7 @@ export default class CourseLookup {
130
130
  return await CourseLookup._db.remove(doc);
131
131
  }
132
132
 
133
+ // [ ] rename to allCourses()
133
134
  static async allCourseWare(): Promise<CourseLookupDoc[]> {
134
135
  const resp = await CourseLookup._db.allDocs<CourseLookupDoc>({
135
136
  include_docs: true,
@@ -38,7 +38,7 @@ export function hexEncode(str: string): string {
38
38
 
39
39
  return returnStr;
40
40
  }
41
- export const pouchDBincludeCredentialsConfig: PouchDB.Configuration.RemoteDatabaseConfiguration = {
41
+ const pouchDBincludeCredentialsConfig: PouchDB.Configuration.RemoteDatabaseConfiguration = {
42
42
  fetch(url: string | Request, opts: RequestInit): Promise<Response> {
43
43
  opts.credentials = 'include';
44
44
 
@@ -46,10 +46,42 @@ export const pouchDBincludeCredentialsConfig: PouchDB.Configuration.RemoteDataba
46
46
  },
47
47
  } as PouchDB.Configuration.RemoteDatabaseConfiguration;
48
48
 
49
+ /**
50
+ * Creates PouchDB configuration with appropriate authentication method
51
+ * - Uses HTTP Basic Auth when credentials are available (Node.js/MCP)
52
+ * - Falls back to cookie auth for browser environments
53
+ */
54
+ export function createPouchDBConfig(): PouchDB.Configuration.RemoteDatabaseConfiguration {
55
+ // Check if running in Node.js with explicit credentials
56
+ const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
57
+ const isNodeEnvironment = typeof window === 'undefined';
58
+
59
+ if (hasExplicitCredentials && isNodeEnvironment) {
60
+ // Use HTTP Basic Auth for Node.js environments (MCP server)
61
+ return {
62
+ fetch(url: string | Request, opts: RequestInit = {}): Promise<Response> {
63
+ const basicAuth = btoa(`${ENV.COUCHDB_USERNAME}:${ENV.COUCHDB_PASSWORD}`);
64
+ const headers = new Headers(opts.headers || {});
65
+ headers.set('Authorization', `Basic ${basicAuth}`);
66
+
67
+ const newOpts = {
68
+ ...opts,
69
+ headers: headers
70
+ };
71
+
72
+ return (pouch as any).fetch(url, newOpts);
73
+ }
74
+ } as PouchDB.Configuration.RemoteDatabaseConfiguration;
75
+ }
76
+
77
+ // Use cookie-based auth for browser environments or when no explicit credentials
78
+ return pouchDBincludeCredentialsConfig;
79
+ }
80
+
49
81
  function getCouchDB(dbName: string): PouchDB.Database {
50
82
  return new pouch(
51
83
  ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
52
- pouchDBincludeCredentialsConfig
84
+ createPouchDBConfig()
53
85
  );
54
86
  }
55
87
 
@@ -57,7 +89,7 @@ export function getCourseDB(courseID: string): PouchDB.Database {
57
89
  // todo: keep a cache of opened courseDBs? need to benchmark this somehow
58
90
  return new pouch(
59
91
  ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + 'coursedb-' + courseID,
60
- pouchDBincludeCredentialsConfig
92
+ createPouchDBConfig()
61
93
  );
62
94
  }
63
95
 
@@ -188,7 +220,7 @@ export function getCouchUserDB(username: string): PouchDB.Database {
188
220
  // see: https://github.com/pouchdb-community/pouchdb-authentication/issues/239
189
221
  const ret = new pouch(
190
222
  ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
191
- pouchDBincludeCredentialsConfig
223
+ createPouchDBConfig()
192
224
  );
193
225
  if (guestAccount) {
194
226
  updateGuestAccountExpirationDate(ret);
@@ -8,9 +8,9 @@ PouchDB.plugin(PouchDBAuth);
8
8
 
9
9
  // Configure PouchDB globally
10
10
  PouchDB.defaults({
11
- ajax: {
12
- timeout: 60000,
13
- },
11
+ // ajax: {
12
+ // timeout: 60000,
13
+ // },
14
14
  });
15
15
 
16
16
  export default PouchDB;
@@ -52,43 +52,61 @@ export default class UpdateQueue extends Loggable {
52
52
  if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
53
53
  this.inprogressUpdates[id] = true;
54
54
 
55
- try {
56
- let doc = await this.readDB.get<T>(id);
57
- logger.debug(`Retrieved doc: ${id}`);
58
- while (this.pendingUpdates[id].length !== 0) {
59
- const update = this.pendingUpdates[id].splice(0, 1)[0];
60
- if (typeof update === 'function') {
61
- doc = { ...doc, ...update(doc) };
62
- } else {
63
- doc = {
64
- ...doc,
65
- ...update,
66
- };
55
+ const MAX_RETRIES = 5;
56
+ for (let i = 0; i < MAX_RETRIES; i++) {
57
+ try {
58
+ const doc = await this.readDB.get<T>(id);
59
+ logger.debug(`Retrieved doc: ${id}`);
60
+
61
+ // Create a new doc object to apply updates to for this attempt
62
+ let updatedDoc = { ...doc };
63
+
64
+ // Note: This loop is not fully safe if updates are functions that depend on a specific doc state
65
+ // that might change between retries. But for simple object merges, it's okay.
66
+ const updatesToApply = [...this.pendingUpdates[id]];
67
+ for (const update of updatesToApply) {
68
+ if (typeof update === 'function') {
69
+ updatedDoc = { ...updatedDoc, ...update(updatedDoc) };
70
+ } else {
71
+ updatedDoc = {
72
+ ...updatedDoc,
73
+ ...update,
74
+ };
75
+ }
67
76
  }
68
- }
69
- // for (const k in doc) {
70
- // console.log(`${k}: ${typeof k}`);
71
- // }
72
- // console.log(`Applied updates to doc: ${JSON.stringify(doc)}`);
73
- await this.writeDB.put<T>(doc);
74
- logger.debug(`Put doc: ${id}`);
75
77
 
76
- if (this.pendingUpdates[id].length === 0) {
77
- this.inprogressUpdates[id] = false;
78
- delete this.inprogressUpdates[id];
79
- } else {
80
- return this.applyUpdates<T>(id);
81
- }
82
- return doc;
83
- } catch (e) {
84
- // Clean up queue state before re-throwing
85
- delete this.inprogressUpdates[id];
86
- if (this.pendingUpdates[id]) {
87
- delete this.pendingUpdates[id];
78
+ await this.writeDB.put<T>(updatedDoc);
79
+ logger.debug(`Put doc: ${id}`);
80
+
81
+ // Success! Remove the updates we just applied.
82
+ this.pendingUpdates[id].splice(0, updatesToApply.length);
83
+
84
+ if (this.pendingUpdates[id].length === 0) {
85
+ this.inprogressUpdates[id] = false;
86
+ delete this.inprogressUpdates[id];
87
+ } else {
88
+ // More updates came in, run again.
89
+ return this.applyUpdates<T>(id);
90
+ }
91
+ return updatedDoc as any; // success, exit loop and function
92
+ } catch (e: any) {
93
+ if (e.name === 'conflict' && i < MAX_RETRIES - 1) {
94
+ logger.warn(`Conflict on update for doc ${id}, retry #${i + 1}`);
95
+ await new Promise((res) => setTimeout(res, 50 * Math.random()));
96
+ // continue to next iteration of the loop
97
+ } else {
98
+ // Max retries reached or a non-conflict error
99
+ delete this.inprogressUpdates[id];
100
+ if (this.pendingUpdates[id]) {
101
+ delete this.pendingUpdates[id];
102
+ }
103
+ logger.error(`Error on attemped update (retry ${i}): ${JSON.stringify(e)}`);
104
+ throw e; // Let caller handle
105
+ }
88
106
  }
89
- logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
90
- throw e; // Let caller handle (e.g., putCardRecord's 404 handling)
91
107
  }
108
+ // This should be unreachable, but it satisfies the compiler that a value is always returned or an error thrown.
109
+ throw new Error(`UpdateQueue failed for doc ${id} after ${MAX_RETRIES} retries.`);
92
110
  } else {
93
111
  throw new Error(`Empty Updates Queue Triggered`);
94
112
  }
@@ -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 { StaticCourseManifest } from '../../util/packer/types';
@@ -87,6 +88,16 @@ export class StaticDataLayerProvider implements DataLayerProvider {
87
88
  throw new Error('Admin functions not supported in static mode');
88
89
  }
89
90
 
91
+ async createUserReaderForUser(targetUsername: string): Promise<UserDBReader> {
92
+ logger.warn(`StaticDataLayerProvider: Multi-user access not supported in static mode`);
93
+ logger.warn(`Request: trying to access data for ${targetUsername}`);
94
+ logger.warn(`Returning current user's data instead`);
95
+
96
+ // In static mode, just return the current user's DB as a reader
97
+ // This is safe since static mode is typically for development/testing
98
+ return this.getUserDB() as UserDBReader;
99
+ }
100
+
90
101
  isReadOnly(): boolean {
91
102
  return true;
92
103
  }
@@ -10,7 +10,13 @@ import {
10
10
  import { StaticDataUnpacker } from './StaticDataUnpacker';
11
11
  import { StaticCourseManifest } from '../../util/packer/types';
12
12
  import { CourseConfig, CourseElo, DataShape, Status } from '@vue-skuilder/common';
13
- import { Tag, TagStub, DocType, SkuilderCourseData } from '../../core/types/types-legacy';
13
+ import {
14
+ Tag,
15
+ TagStub,
16
+ DocType,
17
+ SkuilderCourseData,
18
+ QualifiedCardID,
19
+ } from '../../core/types/types-legacy';
14
20
  import { DataLayerResult } from '../../core/types/db';
15
21
  import { ContentNavigationStrategyData } from '../../core/types/contentNavigationStrategy';
16
22
  import { ScheduledCard } from '../../core/types/user';
@@ -91,8 +97,20 @@ export class StaticCourseDB implements CourseDBInterface {
91
97
  };
92
98
  }
93
99
 
94
- async getCardsByELO(elo: number, limit?: number): Promise<string[]> {
95
- return this.unpacker.queryByElo(elo, limit || 25);
100
+ async getCardsByELO(
101
+ elo: number,
102
+ limit?: number
103
+ ): Promise<
104
+ {
105
+ courseID: string;
106
+ cardID: string;
107
+ elo?: number;
108
+ }[]
109
+ > {
110
+ return (await this.unpacker.queryByElo(elo, limit || 25)).map((card) => {
111
+ const [courseID, cardID, elo] = card.split('-');
112
+ return { courseID, cardID, elo: elo ? parseInt(elo) : undefined };
113
+ });
96
114
  }
97
115
 
98
116
  async getCardEloData(cardIds: string[]): Promise<CourseElo[]> {
@@ -129,7 +147,7 @@ export class StaticCourseDB implements CourseDBInterface {
129
147
 
130
148
  async getCardsCenteredAtELO(
131
149
  options: { limit: number; elo: 'user' | 'random' | number },
132
- filter?: (id: string) => boolean
150
+ filter?: (id: QualifiedCardID) => boolean
133
151
  ): Promise<StudySessionNewItem[]> {
134
152
  let targetElo = typeof options.elo === 'number' ? options.elo : 1000;
135
153
 
@@ -148,16 +166,21 @@ export class StaticCourseDB implements CourseDBInterface {
148
166
  targetElo = 800 + Math.random() * 400; // Random between 800-1200
149
167
  }
150
168
 
151
- let cardIds = await this.unpacker.queryByElo(targetElo, options.limit * 2);
169
+ let cardIds = (await this.unpacker.queryByElo(targetElo, options.limit * 2)).map((c) => {
170
+ return {
171
+ cardID: c,
172
+ courseID: this.courseId,
173
+ };
174
+ });
152
175
 
153
176
  if (filter) {
154
177
  cardIds = cardIds.filter(filter);
155
178
  }
156
179
 
157
- return cardIds.slice(0, options.limit).map((cardId) => ({
180
+ return cardIds.slice(0, options.limit).map((card) => ({
158
181
  status: 'new' as const,
159
- qualifiedID: `${this.courseId}-${cardId}`,
160
- cardID: cardId,
182
+ // qualifiedID: `${this.courseId}-${cardId}`,
183
+ cardID: card.cardID,
161
184
  contentSourceType: 'course' as const,
162
185
  contentSourceID: this.courseId,
163
186
  courseID: this.courseId,
@@ -390,4 +413,20 @@ export class StaticCourseDB implements CourseDBInterface {
390
413
  async getAttachmentBlob(docId: string, attachmentName: string): Promise<Blob | Buffer | null> {
391
414
  return this.unpacker.getAttachmentBlob(docId, attachmentName);
392
415
  }
416
+
417
+ // Admin search methods
418
+ async searchCards(_query: string): Promise<any[]> {
419
+ // In static mode, return empty results for now
420
+ // Could be implemented with local search if needed
421
+ return [];
422
+ }
423
+
424
+ async find(_request: PouchDB.Find.FindRequest<any>): Promise<PouchDB.Find.FindResponse<any>> {
425
+ // In static mode, return empty results for now
426
+ // Could be implemented with local search if needed
427
+ return {
428
+ docs: [],
429
+ warning: 'Find operations not supported in static mode',
430
+ } as any;
431
+ }
393
432
  }
@@ -0,0 +1,2 @@
1
+ // Export configured PouchDB instance
2
+ export { default } from '../impl/couch/pouchdb-setup.js';