@vue-skuilder/db 0.1.1

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 (62) hide show
  1. package/README.md +26 -0
  2. package/dist/core/index.d.mts +3 -0
  3. package/dist/core/index.d.ts +3 -0
  4. package/dist/core/index.js +7906 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/core/index.mjs +7886 -0
  7. package/dist/core/index.mjs.map +1 -0
  8. package/dist/index-QMtzQI65.d.mts +734 -0
  9. package/dist/index-QMtzQI65.d.ts +734 -0
  10. package/dist/index.d.mts +133 -0
  11. package/dist/index.d.ts +133 -0
  12. package/dist/index.js +8726 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/index.mjs +8699 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/eslint.config.mjs +20 -0
  17. package/package.json +47 -0
  18. package/src/core/bulkImport/cardProcessor.ts +165 -0
  19. package/src/core/bulkImport/index.ts +2 -0
  20. package/src/core/bulkImport/types.ts +27 -0
  21. package/src/core/index.ts +9 -0
  22. package/src/core/interfaces/adminDB.ts +27 -0
  23. package/src/core/interfaces/classroomDB.ts +75 -0
  24. package/src/core/interfaces/contentSource.ts +64 -0
  25. package/src/core/interfaces/courseDB.ts +139 -0
  26. package/src/core/interfaces/dataLayerProvider.ts +46 -0
  27. package/src/core/interfaces/index.ts +7 -0
  28. package/src/core/interfaces/navigationStrategyManager.ts +46 -0
  29. package/src/core/interfaces/userDB.ts +183 -0
  30. package/src/core/navigators/elo.ts +76 -0
  31. package/src/core/navigators/index.ts +57 -0
  32. package/src/core/readme.md +9 -0
  33. package/src/core/types/contentNavigationStrategy.ts +21 -0
  34. package/src/core/types/db.ts +7 -0
  35. package/src/core/types/types-legacy.ts +155 -0
  36. package/src/core/types/user.ts +70 -0
  37. package/src/core/util/index.ts +42 -0
  38. package/src/factory.ts +86 -0
  39. package/src/impl/pouch/PouchDataLayerProvider.ts +102 -0
  40. package/src/impl/pouch/adminDB.ts +91 -0
  41. package/src/impl/pouch/auth.ts +48 -0
  42. package/src/impl/pouch/classroomDB.ts +306 -0
  43. package/src/impl/pouch/clientCache.ts +19 -0
  44. package/src/impl/pouch/courseAPI.ts +245 -0
  45. package/src/impl/pouch/courseDB.ts +772 -0
  46. package/src/impl/pouch/courseLookupDB.ts +135 -0
  47. package/src/impl/pouch/index.ts +235 -0
  48. package/src/impl/pouch/pouchdb-setup.ts +16 -0
  49. package/src/impl/pouch/types.ts +7 -0
  50. package/src/impl/pouch/updateQueue.ts +89 -0
  51. package/src/impl/pouch/user-course-relDB.ts +73 -0
  52. package/src/impl/pouch/userDB.ts +1097 -0
  53. package/src/index.ts +8 -0
  54. package/src/study/SessionController.ts +401 -0
  55. package/src/study/SpacedRepetition.ts +128 -0
  56. package/src/study/getCardDataShape.ts +34 -0
  57. package/src/study/index.ts +2 -0
  58. package/src/util/Loggable.ts +11 -0
  59. package/src/util/index.ts +1 -0
  60. package/src/util/logger.ts +55 -0
  61. package/tsconfig.json +12 -0
  62. package/tsup.config.ts +17 -0
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './core';
2
+
3
+ export { default as CourseLookup } from './impl/pouch/courseLookupDB';
4
+
5
+ export * from './study';
6
+
7
+ export * from './util';
8
+ export * from './factory';
@@ -0,0 +1,401 @@
1
+ import {
2
+ isReview,
3
+ StudyContentSource,
4
+ StudySessionFailedItem,
5
+ StudySessionItem,
6
+ StudySessionNewItem,
7
+ StudySessionReviewItem,
8
+ } from '@/impl/pouch';
9
+
10
+ import { CardRecord } from '@/core';
11
+ import { Loggable } from '@/util';
12
+ import { ScheduledCard } from '@/core/types/user';
13
+
14
+ function randomInt(min: number, max: number): number {
15
+ return Math.floor(Math.random() * (max - min + 1)) + min;
16
+ }
17
+
18
+ export interface StudySessionRecord {
19
+ card: {
20
+ course_id: string;
21
+ card_id: string;
22
+ card_elo: number;
23
+ };
24
+ item: StudySessionItem;
25
+ records: CardRecord[];
26
+ }
27
+
28
+ class ItemQueue<T extends StudySessionItem> {
29
+ private q: T[] = [];
30
+ private seenCardIds: string[] = [];
31
+ private _dequeueCount: number = 0;
32
+ public get dequeueCount(): number {
33
+ return this._dequeueCount;
34
+ }
35
+
36
+ public add(item: T) {
37
+ if (this.seenCardIds.find((d) => d === item.cardID)) {
38
+ return; // do not re-add a card to the same queue
39
+ }
40
+
41
+ this.seenCardIds.push(item.cardID);
42
+ this.q.push(item);
43
+ }
44
+ public addAll(items: T[]) {
45
+ items.forEach((i) => this.add(i));
46
+ }
47
+ public get length() {
48
+ return this.q.length;
49
+ }
50
+ public peek(index: number): T {
51
+ return this.q[index];
52
+ }
53
+
54
+ public dequeue(): T | null {
55
+ if (this.q.length !== 0) {
56
+ this._dequeueCount++;
57
+ return this.q.splice(0, 1)[0];
58
+ } else {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ public get toString(): string {
64
+ return (
65
+ `${typeof this.q[0]}:\n` + this.q.map((i) => `\t${i.qualifiedID}: ${i.status}`).join('\n')
66
+ );
67
+ }
68
+ }
69
+
70
+ export class SessionController extends Loggable {
71
+ _className = 'SessionController';
72
+ private sources: StudyContentSource[];
73
+ private _sessionRecord: StudySessionRecord[] = [];
74
+ public set sessionRecord(r: StudySessionRecord[]) {
75
+ this._sessionRecord = r;
76
+ }
77
+
78
+ private reviewQ: ItemQueue<StudySessionReviewItem> = new ItemQueue<StudySessionReviewItem>();
79
+ private newQ: ItemQueue<StudySessionNewItem> = new ItemQueue<StudySessionNewItem>();
80
+ private failedQ: ItemQueue<StudySessionFailedItem> = new ItemQueue<StudySessionFailedItem>();
81
+ private _currentCard: StudySessionItem | null = null;
82
+ /**
83
+ * Indicates whether the session has been initialized - eg, the
84
+ * queues have been populated.
85
+ */
86
+ private _isInitialized: boolean = false;
87
+
88
+ private startTime: Date;
89
+ private endTime: Date;
90
+ private _secondsRemaining: number;
91
+ public get secondsRemaining(): number {
92
+ return this._secondsRemaining;
93
+ }
94
+ public get report(): string {
95
+ return `${this.reviewQ.dequeueCount} reviews, ${this.newQ.dequeueCount} new cards`;
96
+ }
97
+ public get detailedReport(): string {
98
+ return this.newQ.toString + '\n' + this.reviewQ.toString + '\n' + this.failedQ.toString;
99
+ }
100
+ // @ts-expect-error NodeJS.Timeout type not available in browser context
101
+ private _intervalHandle: NodeJS.Timeout;
102
+
103
+ /**
104
+ *
105
+ */
106
+ constructor(sources: StudyContentSource[], time: number) {
107
+ super();
108
+
109
+ this.sources = sources;
110
+ this.startTime = new Date();
111
+ this._secondsRemaining = time;
112
+ this.endTime = new Date(this.startTime.valueOf() + 1000 * this._secondsRemaining);
113
+
114
+ this.log(`Session constructed:
115
+ startTime: ${this.startTime}
116
+ endTime: ${this.endTime}`);
117
+ }
118
+
119
+ private tick() {
120
+ this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1000);
121
+ // this.log(this.secondsRemaining);
122
+
123
+ if (this._secondsRemaining === 0) {
124
+ clearInterval(this._intervalHandle);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Returns a rough, erring toward conservative, guess at
130
+ * the amount of time the failed cards queue will require
131
+ * to clean up.
132
+ *
133
+ * (seconds)
134
+ */
135
+ private estimateCleanupTime(): number {
136
+ let time: number = 0;
137
+ for (let i = 0; i < this.failedQ.length; i++) {
138
+ const c = this.failedQ.peek(i);
139
+ // this.log(`Failed card ${c.qualifiedID} found`)
140
+
141
+ const record = this._sessionRecord.find((r) => r.item.cardID === c.cardID);
142
+ let cardTime = 0;
143
+
144
+ if (record) {
145
+ // this.log(`Card Record Found...`);
146
+ for (let j = 0; j < record.records.length; j++) {
147
+ cardTime += record.records[j].timeSpent;
148
+ }
149
+ cardTime = cardTime / record.records.length;
150
+ time += cardTime;
151
+ }
152
+ }
153
+
154
+ const ret: number = time / 1000;
155
+ this.log(`Failed card cleanup estimate: ${Math.round(ret)}`);
156
+ return ret;
157
+ }
158
+
159
+ /**
160
+ * Extremely rough, conservative, estimate of amound of time to complete
161
+ * all scheduled reviews
162
+ */
163
+ private estimateReviewTime(): number {
164
+ const ret = 5 * this.reviewQ.length;
165
+ this.log(`Review card time estimate: ${ret}`);
166
+ return ret;
167
+ }
168
+
169
+ public async prepareSession() {
170
+ try {
171
+ await Promise.all([this.getScheduledReviews(), this.getNewCards()]);
172
+ } catch (e) {
173
+ this.error('Error preparing study session:', e);
174
+ }
175
+
176
+ this._isInitialized = true;
177
+
178
+ this._intervalHandle = setInterval(() => {
179
+ this.tick();
180
+ }, 1000);
181
+ }
182
+
183
+ public addTime(seconds: number) {
184
+ this.endTime = new Date(this.endTime.valueOf() + 1000 * seconds);
185
+ }
186
+
187
+ public get failedCount(): number {
188
+ return this.failedQ.length;
189
+ }
190
+
191
+ public toString() {
192
+ return `Session: ${this.reviewQ.length} Reviews, ${this.newQ.length} New, ${this.failedQ.length} failed`;
193
+ }
194
+ public reportString() {
195
+ return `${this.reviewQ.dequeueCount} Reviews, ${this.newQ.dequeueCount} New, ${this.failedQ.dequeueCount} failed`;
196
+ }
197
+
198
+ private async getScheduledReviews() {
199
+ const reviews = await Promise.all(
200
+ this.sources.map((c) =>
201
+ c.getPendingReviews().catch((error) => {
202
+ this.error(`Failed to get reviews for source ${c}:`, error);
203
+ return [];
204
+ })
205
+ )
206
+ );
207
+
208
+ const dueCards: (StudySessionReviewItem & ScheduledCard)[] = [];
209
+
210
+ while (reviews.length != 0 && reviews.some((r) => r.length > 0)) {
211
+ // pick a random review source
212
+ const index = randomInt(0, reviews.length - 1);
213
+ const source = reviews[index];
214
+
215
+ if (source.length === 0) {
216
+ reviews.splice(index, 1);
217
+ continue;
218
+ } else {
219
+ dueCards.push(source.shift()!);
220
+ }
221
+ }
222
+
223
+ let report = 'Review session created with:\n';
224
+ for (let i = 0; i < dueCards.length; i++) {
225
+ const card = dueCards[i];
226
+ this.reviewQ.add(card);
227
+ report += `\t${card.qualifiedID}}\n`;
228
+ }
229
+ this.log(report);
230
+ }
231
+
232
+ private async getNewCards(n: number = 10) {
233
+ const perCourse = Math.ceil(n / this.sources.length);
234
+ const newContent = await Promise.all(this.sources.map((c) => c.getNewCards(perCourse)));
235
+
236
+ // [ ] is this a noop?
237
+ newContent.forEach((newContentFromSource) => {
238
+ newContentFromSource.filter((c) => {
239
+ return this._sessionRecord.find((record) => record.card.card_id === c.cardID) === undefined;
240
+ });
241
+ });
242
+
243
+ while (n > 0 && newContent.some((nc) => nc.length > 0)) {
244
+ for (let i = 0; i < newContent.length; i++) {
245
+ if (newContent[i].length > 0) {
246
+ const item = newContent[i].splice(0, 1)[0];
247
+ this.log(`Adding new card: ${item.qualifiedID}`);
248
+ this.newQ.add(item);
249
+ n--;
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ private nextNewCard(): StudySessionNewItem | null {
256
+ const item = this.newQ.dequeue();
257
+
258
+ // queue some more content if we are getting low
259
+ if (this._isInitialized && this.newQ.length < 5) {
260
+ void this.getNewCards();
261
+ }
262
+
263
+ return item;
264
+ }
265
+
266
+ public nextCard(
267
+ action:
268
+ | 'dismiss-success'
269
+ | 'dismiss-failed'
270
+ | 'marked-failed'
271
+ | 'dismiss-error' = 'dismiss-success'
272
+ ): StudySessionItem | null {
273
+ // dismiss (or sort to failedQ) the current card
274
+ this.dismissCurrentCard(action);
275
+
276
+ const choice = Math.random();
277
+ let newBound: number = 0.1;
278
+ let reviewBound: number = 0.75;
279
+
280
+ if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
281
+ // all queues empty - session is over (and course is complete?)
282
+ this._currentCard = null;
283
+ return this._currentCard;
284
+ }
285
+
286
+ if (this._secondsRemaining < 2 && this.failedQ.length === 0) {
287
+ // session is over!
288
+ this._currentCard = null;
289
+ return this._currentCard;
290
+ }
291
+
292
+ // supply new cards at start of session
293
+ if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
294
+ this._currentCard = this.nextNewCard();
295
+ return this._currentCard;
296
+ }
297
+
298
+ const cleanupTime = this.estimateCleanupTime();
299
+ const reviewTime = this.estimateReviewTime();
300
+ const availableTime = this._secondsRemaining - (cleanupTime + reviewTime);
301
+
302
+ // if time-remaing vs (reviewQ + failureQ) looks good,
303
+ // lean toward newQ
304
+ if (availableTime > 20) {
305
+ newBound = 0.5;
306
+ reviewBound = 0.9;
307
+ }
308
+ // else if time-remaining vs failureQ looks good,
309
+ // lean toward reviewQ
310
+ else if (this._secondsRemaining - cleanupTime > 20) {
311
+ newBound = 0.05;
312
+ reviewBound = 0.9;
313
+ }
314
+ // else (time-remaining vs failureQ looks bad!)
315
+ // lean heavily toward failureQ
316
+ else {
317
+ newBound = 0.01;
318
+ reviewBound = 0.1;
319
+ }
320
+
321
+ // prevent (unless no other option available) re-display of
322
+ // most recent card
323
+ if (this.failedQ.length === 1 && action === 'marked-failed') {
324
+ reviewBound = 1;
325
+ }
326
+
327
+ // exclude possibility of drawing from empty queues
328
+ if (this.failedQ.length === 0) {
329
+ reviewBound = 1;
330
+ }
331
+ if (this.reviewQ.length === 0) {
332
+ newBound = reviewBound;
333
+ }
334
+
335
+ if (choice < newBound && this.newQ.length) {
336
+ this._currentCard = this.nextNewCard();
337
+ } else if (choice < reviewBound && this.reviewQ.length) {
338
+ this._currentCard = this.reviewQ.dequeue();
339
+ } else if (this.failedQ.length) {
340
+ this._currentCard = this.failedQ.dequeue();
341
+ } else {
342
+ this.log(`No more cards available for the session!`);
343
+ this._currentCard = null;
344
+ }
345
+
346
+ return this._currentCard;
347
+ }
348
+
349
+ private dismissCurrentCard(
350
+ action:
351
+ | 'dismiss-success'
352
+ | 'dismiss-failed'
353
+ | 'marked-failed'
354
+ | 'dismiss-error' = 'dismiss-success'
355
+ ) {
356
+ if (this._currentCard) {
357
+ // this.log(`Running dismissCurrentCard on ${this._currentCard!.qualifiedID}`);
358
+ // if (action.includes('dismiss')) {
359
+ // if (this._currentCard.status === 'review' ||
360
+ // this._currentCard.status === 'failed-review') {
361
+ // removeScheduledCardReview(this.user.getUsername(),
362
+ // (this._currentCard as StudySessionReviewItem).reviewID);
363
+ // this.log(`Dismissed review card: ${this._currentCard.qualifiedID}`)
364
+ // }
365
+ // }
366
+
367
+ if (action === 'dismiss-success') {
368
+ // schedule a review - currently done in Study.vue
369
+ } else if (action === 'marked-failed') {
370
+ let failedItem: StudySessionFailedItem;
371
+
372
+ if (isReview(this._currentCard)) {
373
+ failedItem = {
374
+ cardID: this._currentCard.cardID,
375
+ courseID: this._currentCard.courseID,
376
+ qualifiedID: this._currentCard.qualifiedID,
377
+ contentSourceID: this._currentCard.contentSourceID,
378
+ contentSourceType: this._currentCard.contentSourceType,
379
+ status: 'failed-review',
380
+ reviewID: this._currentCard.reviewID,
381
+ };
382
+ } else {
383
+ failedItem = {
384
+ cardID: this._currentCard.cardID,
385
+ courseID: this._currentCard.courseID,
386
+ qualifiedID: this._currentCard.qualifiedID,
387
+ contentSourceID: this._currentCard.contentSourceID,
388
+ contentSourceType: this._currentCard.contentSourceType,
389
+ status: 'failed-new',
390
+ };
391
+ }
392
+
393
+ this.failedQ.add(failedItem);
394
+ } else if (action === 'dismiss-error') {
395
+ // some error logging?
396
+ } else if (action === 'dismiss-failed') {
397
+ // handled by Study.vue
398
+ }
399
+ }
400
+ }
401
+ }
@@ -0,0 +1,128 @@
1
+ import { CardHistory, CardRecord, QuestionRecord } from '@/core/types/types-legacy';
2
+ import { areQuestionRecords } from '@/core/util';
3
+ import { Update } from '@/impl/pouch/updateQueue';
4
+ import moment from 'moment';
5
+ import { logger } from '../util/logger';
6
+
7
+ type Moment = moment.Moment;
8
+ const duration = moment.duration;
9
+
10
+ export interface DocumentUpdater {
11
+ update<T extends PouchDB.Core.Document<object>>(id: string, update: Update<T>): Promise<T>;
12
+ }
13
+
14
+ /**
15
+ * Returns the minimum number of seconds that should pass before a
16
+ * card is redisplayed for review / practice.
17
+ *
18
+ * @param cardHistory The user's history working with the given card
19
+ */
20
+ export function newInterval(user: DocumentUpdater, cardHistory: CardHistory<CardRecord>): number {
21
+ if (areQuestionRecords(cardHistory)) {
22
+ return newQuestionInterval(user, cardHistory);
23
+ } else {
24
+ return 100000; // random - replace
25
+ }
26
+ }
27
+
28
+ function newQuestionInterval(user: DocumentUpdater, cardHistory: CardHistory<QuestionRecord>) {
29
+ const records = cardHistory.records;
30
+ const currentAttempt = records[records.length - 1];
31
+ const lastInterval: number = lastSuccessfulInterval(records);
32
+
33
+ if (lastInterval > cardHistory.bestInterval) {
34
+ cardHistory.bestInterval = lastInterval;
35
+ // update bestInterval on cardHistory in db
36
+ void user.update<CardHistory<QuestionRecord>>(cardHistory._id, {
37
+ bestInterval: lastInterval,
38
+ });
39
+ }
40
+
41
+ if (currentAttempt.isCorrect) {
42
+ const skill = currentAttempt.performance as number;
43
+ logger.debug(`Demontrated skill: \t${skill}`);
44
+ const interval: number = lastInterval * (0.75 + skill);
45
+ cardHistory.lapses = getLapses(cardHistory.records);
46
+ cardHistory.streak = getStreak(cardHistory.records);
47
+
48
+ if (
49
+ cardHistory.lapses &&
50
+ cardHistory.streak &&
51
+ cardHistory.bestInterval &&
52
+ (cardHistory.lapses >= 0 || cardHistory.streak >= 0)
53
+ ) {
54
+ // weighted average of best-ever performance vs current performance, based
55
+ // on how often the card has been failed, and the current streak of success
56
+ const ret =
57
+ (cardHistory.lapses * interval + cardHistory.streak * cardHistory.bestInterval) /
58
+ (cardHistory.lapses + cardHistory.streak);
59
+ logger.debug(`Weighted average interval calculation:
60
+ \t(${cardHistory.lapses} * ${interval} + ${cardHistory.streak} * ${cardHistory.bestInterval}) / (${cardHistory.lapses} + ${cardHistory.streak}) = ${ret}`);
61
+ return ret;
62
+ } else {
63
+ return interval;
64
+ }
65
+ } else {
66
+ return 0;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Returns the amount of time, in seconds, of the most recent successful
72
+ * interval for this card. An interval is successful if the user answers
73
+ * a question correctly on the first attempt.
74
+ *
75
+ * @param cardHistory The record of user attempts with the question
76
+ */
77
+ function lastSuccessfulInterval(cardHistory: QuestionRecord[]): number {
78
+ for (let i = cardHistory.length - 1; i >= 1; i--) {
79
+ if (cardHistory[i].priorAttemps === 0 && cardHistory[i].isCorrect) {
80
+ const lastInterval = secondsBetween(cardHistory[i - 1].timeStamp, cardHistory[i].timeStamp);
81
+ const ret = Math.max(lastInterval, 20 * 60 * 60);
82
+ logger.debug(`Last interval w/ this card was: ${lastInterval}s, returning ${ret}s`);
83
+ return ret;
84
+ }
85
+ }
86
+
87
+ return getInitialInterval(cardHistory); // used as a magic number here - indicates no prior intervals
88
+ }
89
+
90
+ function getStreak(records: QuestionRecord[]): number {
91
+ let streak = 0;
92
+ let index = records.length - 1;
93
+
94
+ while (index >= 0 && records[index].isCorrect) {
95
+ index--;
96
+ streak++;
97
+ }
98
+
99
+ return streak;
100
+ }
101
+ function getLapses(records: QuestionRecord[]): number {
102
+ return records.filter((r) => r.isCorrect === false).length;
103
+ }
104
+
105
+ function getInitialInterval(cardHistory: QuestionRecord[]): number {
106
+ logger.warn(`history of length: ${cardHistory.length} ignored!`);
107
+
108
+ // todo make this a data-driven service, relying on:
109
+ // - global experience w/ the card (ie, what interval
110
+ // seems to be working well across the population)
111
+ // - the individual user (how do they respond in general
112
+ // when compared to the population)
113
+ return 60 * 60 * 24 * 3; // 3 days
114
+ }
115
+
116
+ /**
117
+ * Returns the time in seconds between two Moment objects
118
+ * @param start The first time
119
+ * @param end The second time
120
+ */
121
+ function secondsBetween(start: Moment, end: Moment): number {
122
+ // assertion guard against mis-typed json from database
123
+ start = moment(start);
124
+ end = moment(end);
125
+ const ret = duration(end.diff(start)).asSeconds();
126
+ // console.log(`From start: ${start} to finish: ${end} is ${ret} seconds`);
127
+ return ret;
128
+ }
@@ -0,0 +1,34 @@
1
+ import { allCourses } from '@vue-skuilder/courses';
2
+ import { log, NameSpacer, CourseConfig, DataShape } from '@vue-skuilder/common';
3
+ import { CardData, DisplayableData } from '@/core';
4
+ import { getCourseDB } from '@/impl/pouch/courseAPI';
5
+
6
+ export async function getCardDataShape(courseID: string, cardID: string) {
7
+ const dataShapes: DataShape[] = [];
8
+ allCourses.courses.forEach((course) => {
9
+ course.questions.forEach((question) => {
10
+ question.dataShapes.forEach((ds) => {
11
+ dataShapes.push(ds);
12
+ });
13
+ });
14
+ });
15
+
16
+ // log(`Datashapes: ${JSON.stringify(dataShapes)}`);
17
+ const db = getCourseDB(courseID);
18
+ const card = await db.get<CardData>(cardID);
19
+ const disp = await db.get<DisplayableData>(card.id_displayable_data[0]);
20
+ const cfg = await db.get<CourseConfig>('CourseConfig');
21
+
22
+ // log(`Config: ${JSON.stringify(cfg)}`);
23
+ // log(`DisplayableData: ${JSON.stringify(disp)}`);
24
+ const dataShape = cfg!.dataShapes.find((ds) => {
25
+ return ds.name === disp.id_datashape;
26
+ });
27
+
28
+ const ret = dataShapes.find((ds) => {
29
+ return ds.name === NameSpacer.getDataShapeDescriptor(dataShape!.name).dataShape;
30
+ })!;
31
+
32
+ log(`Returning ${JSON.stringify(ret)}`);
33
+ return ret;
34
+ }
@@ -0,0 +1,2 @@
1
+ export * from './SessionController';
2
+ export * from './SpacedRepetition';
@@ -0,0 +1,11 @@
1
+ export abstract class Loggable {
2
+ protected abstract readonly _className: string;
3
+ protected log(...args: unknown[]): void {
4
+ // eslint-disable-next-line no-console
5
+ console.log(`LOG-${this._className}@${new Date()}:`, ...args);
6
+ }
7
+ protected error(...args: unknown[]): void {
8
+ // eslint-disable-next-line no-console
9
+ console.error(`ERROR-${this._className}@${new Date()}:`, ...args);
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ export * from './Loggable';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Simple logging utility for @vue-skuilder/db package
3
+ *
4
+ * This utility provides environment-aware logging with ESLint suppressions
5
+ * to resolve console statement violations while maintaining logging functionality.
6
+ */
7
+
8
+ const isDevelopment = typeof process !== 'undefined' && process.env.NODE_ENV === 'development';
9
+ const _isBrowser = typeof window !== 'undefined';
10
+
11
+ export const logger = {
12
+ /**
13
+ * Debug-level logging - only shown in development
14
+ */
15
+ debug: (message: string, ...args: any[]): void => {
16
+ if (isDevelopment) {
17
+ // eslint-disable-next-line no-console
18
+ console.debug(`[DB:DEBUG] ${message}`, ...args);
19
+ }
20
+ },
21
+
22
+ /**
23
+ * Info-level logging - general information
24
+ */
25
+ info: (message: string, ...args: any[]): void => {
26
+ // eslint-disable-next-line no-console
27
+ console.info(`[DB:INFO] ${message}`, ...args);
28
+ },
29
+
30
+ /**
31
+ * Warning-level logging - potential issues
32
+ */
33
+ warn: (message: string, ...args: any[]): void => {
34
+ // eslint-disable-next-line no-console
35
+ console.warn(`[DB:WARN] ${message}`, ...args);
36
+ },
37
+
38
+ /**
39
+ * Error-level logging - serious problems
40
+ */
41
+ error: (message: string, ...args: any[]): void => {
42
+ // eslint-disable-next-line no-console
43
+ console.error(`[DB:ERROR] ${message}`, ...args);
44
+ },
45
+
46
+ /**
47
+ * Log function for backward compatibility with existing log() usage
48
+ */
49
+ log: (message: string, ...args: any[]): void => {
50
+ if (isDevelopment) {
51
+ // eslint-disable-next-line no-console
52
+ console.log(`[DB:LOG] ${message}`, ...args);
53
+ }
54
+ },
55
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "noEmit": true,
7
+ "paths": {
8
+ "@/*": ["./src/*"]
9
+ }
10
+ },
11
+ "include": ["src"]
12
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: [
5
+ 'src/index.ts',
6
+ 'src/core/index.ts',
7
+ 'src/pouch/index.ts'
8
+ ],
9
+ format: ['cjs', 'esm'],
10
+ dts: true,
11
+ splitting: false,
12
+ sourcemap: true,
13
+ clean: true,
14
+ outExtension: ({ format }) => ({
15
+ js: format === 'esm' ? '.mjs' : '.js'
16
+ })
17
+ });