@vue-skuilder/db 0.1.4 → 0.1.5

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 (80) hide show
  1. package/dist/SyncStrategy-DnJRj-Xp.d.mts +74 -0
  2. package/dist/SyncStrategy-DnJRj-Xp.d.ts +74 -0
  3. package/dist/core/index.d.mts +90 -2
  4. package/dist/core/index.d.ts +90 -2
  5. package/dist/core/index.js +798 -650
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +769 -621
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/dataLayerProvider-B8wquRiB.d.mts +37 -0
  10. package/dist/dataLayerProvider-DRjMZMaf.d.ts +37 -0
  11. package/dist/impl/couch/index.d.mts +292 -0
  12. package/dist/impl/couch/index.d.ts +292 -0
  13. package/dist/impl/couch/index.js +8522 -0
  14. package/dist/impl/couch/index.js.map +1 -0
  15. package/dist/impl/couch/index.mjs +8474 -0
  16. package/dist/impl/couch/index.mjs.map +1 -0
  17. package/dist/impl/static/index.d.mts +204 -0
  18. package/dist/impl/static/index.d.ts +204 -0
  19. package/dist/impl/static/index.js +8499 -0
  20. package/dist/impl/static/index.js.map +1 -0
  21. package/dist/impl/static/index.mjs +8488 -0
  22. package/dist/impl/static/index.mjs.map +1 -0
  23. package/dist/index.d.mts +13 -4
  24. package/dist/index.d.ts +13 -4
  25. package/dist/index.js +3280 -2108
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +3290 -2118
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/types-B0GJsjOr.d.ts +47 -0
  30. package/dist/types-DIgj8pP7.d.mts +47 -0
  31. package/dist/types-legacy-CTsJvvxI.d.mts +137 -0
  32. package/dist/types-legacy-CTsJvvxI.d.ts +137 -0
  33. package/dist/{index-QMtzQI65.d.mts → userDB-C5dcuRZs.d.ts} +3 -251
  34. package/dist/{index-QMtzQI65.d.ts → userDB-ZSwOXiYN.d.mts} +3 -251
  35. package/dist/util/packer/index.d.mts +25 -0
  36. package/dist/util/packer/index.d.ts +25 -0
  37. package/dist/util/packer/index.js +307 -0
  38. package/dist/util/packer/index.js.map +1 -0
  39. package/dist/util/packer/index.mjs +280 -0
  40. package/dist/util/packer/index.mjs.map +1 -0
  41. package/package.json +12 -2
  42. package/src/core/interfaces/contentSource.ts +8 -6
  43. package/src/core/interfaces/userDB.ts +2 -2
  44. package/src/factory.ts +10 -7
  45. package/src/impl/{pouch/userDB.ts → common/BaseUserDB.ts} +225 -260
  46. package/src/impl/common/SyncStrategy.ts +90 -0
  47. package/src/impl/common/index.ts +23 -0
  48. package/src/impl/common/types.ts +50 -0
  49. package/src/impl/common/userDBHelpers.ts +144 -0
  50. package/src/impl/couch/CouchDBSyncStrategy.ts +209 -0
  51. package/src/impl/{pouch → couch}/PouchDataLayerProvider.ts +12 -7
  52. package/src/impl/{pouch → couch}/adminDB.ts +3 -3
  53. package/src/impl/{pouch → couch}/auth.ts +2 -2
  54. package/src/impl/{pouch → couch}/classroomDB.ts +6 -6
  55. package/src/impl/{pouch → couch}/courseAPI.ts +28 -5
  56. package/src/impl/{pouch → couch}/courseDB.ts +24 -10
  57. package/src/impl/{pouch → couch}/courseLookupDB.ts +1 -1
  58. package/src/impl/{pouch → couch}/index.ts +27 -20
  59. package/src/impl/{pouch → couch}/updateQueue.ts +5 -1
  60. package/src/impl/{pouch → couch}/user-course-relDB.ts +6 -1
  61. package/src/impl/static/NoOpSyncStrategy.ts +70 -0
  62. package/src/impl/static/StaticDataLayerProvider.ts +89 -0
  63. package/src/impl/static/StaticDataUnpacker.ts +376 -0
  64. package/src/impl/static/courseDB.ts +257 -0
  65. package/src/impl/static/coursesDB.ts +37 -0
  66. package/src/impl/static/index.ts +8 -0
  67. package/src/impl/static/userDB.ts +179 -0
  68. package/src/index.ts +1 -1
  69. package/src/study/SessionController.ts +4 -4
  70. package/src/study/SpacedRepetition.ts +3 -3
  71. package/src/study/getCardDataShape.ts +2 -2
  72. package/src/util/index.ts +1 -0
  73. package/src/util/packer/CouchDBToStaticPacker.ts +349 -0
  74. package/src/util/packer/index.ts +4 -0
  75. package/src/util/packer/types.ts +52 -0
  76. package/tsconfig.json +7 -10
  77. package/tsup.config.ts +5 -3
  78. /package/src/impl/{pouch → couch}/clientCache.ts +0 -0
  79. /package/src/impl/{pouch → couch}/pouchdb-setup.ts +0 -0
  80. /package/src/impl/{pouch → couch}/types.ts +0 -0
@@ -1,15 +1,15 @@
1
1
  import pouch from './pouchdb-setup';
2
2
  import { pouchDBincludeCredentialsConfig } from '.';
3
- import { ENV } from '@/factory';
3
+ import { ENV } from '@db/factory';
4
4
  // import { DataShape } from '../..base-course/Interfaces/DataShape';
5
5
  import { NameSpacer, ShapeDescriptor } from '@vue-skuilder/common';
6
6
  import { CourseConfig, DataShape } from '@vue-skuilder/common';
7
7
  import { CourseElo, blankCourseElo, toCourseElo } from '@vue-skuilder/common';
8
- import { CourseDB, createTag, updateCardElo } from './courseDB';
8
+ import { CourseDB, createTag } from './courseDB';
9
9
  import { CardData, DisplayableData, DocType, Tag } from '../../core/types/types-legacy';
10
10
  import { prepareNote55 } from '@vue-skuilder/common';
11
- import { User } from './userDB';
12
- import { logger } from '@/util/logger';
11
+ import { BaseUser } from '../common';
12
+ import { logger } from '@db/util/logger';
13
13
 
14
14
  /**
15
15
  *
@@ -33,6 +33,8 @@ export async function addNote55(
33
33
  ): Promise<PouchDB.Core.Response> {
34
34
  const db = getCourseDB(courseID);
35
35
  const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
36
+ // [ ] NAMESPACING: consider put( _id: "displayable_data-uuid")
37
+ // consider also semantic hashing
36
38
  const result = await db.post<DisplayableData>(payload);
37
39
 
38
40
  const dataShapeId = NameSpacer.getDataShapeString({
@@ -139,6 +141,7 @@ async function addCard(
139
141
  elo: CourseElo,
140
142
  tags: string[]
141
143
  ): Promise<PouchDB.Core.Response> {
144
+ // [ ] NAMESPACING: consider put( _id: "card-uuid")
142
145
  const card = await getCourseDB(courseID).post<CardData>({
143
146
  course,
144
147
  id_displayable_data,
@@ -187,7 +190,16 @@ export async function addTagToCard(
187
190
  // In this case, should be converted to a server-request
188
191
  const prefixedTagID = getTagID(tagID);
189
192
  const courseDB = getCourseDB(courseID);
190
- const courseApi = new CourseDB(courseID, async () => User.Dummy());
193
+ const courseApi = new CourseDB(courseID, async () => {
194
+ const dummySyncStrategy = {
195
+ setupRemoteDB: () => null as any,
196
+ startSync: () => {},
197
+ canCreateAccount: () => false,
198
+ canAuthenticate: () => false,
199
+ getCurrentUsername: async () => 'DummyUser',
200
+ };
201
+ return BaseUser.Dummy(dummySyncStrategy);
202
+ });
191
203
  try {
192
204
  logger.info(`Applying tag ${tagID} to card ${courseID + '-' + cardID}...`);
193
205
  const tag = await courseDB.get<Tag>(prefixedTagID);
@@ -220,6 +232,17 @@ export async function addTagToCard(
220
232
  }
221
233
  }
222
234
 
235
+ async function updateCardElo(courseID: string, cardID: string, elo: CourseElo) {
236
+ if (elo) {
237
+ // checking against null, undefined, NaN
238
+ const cDB = getCourseDB(courseID);
239
+ const card = await cDB.get<CardData>(cardID);
240
+ logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
241
+ card.elo = elo;
242
+ return cDB.put(card); // race conditions - is it important? probably not (net-zero effect)
243
+ }
244
+ }
245
+
223
246
  class AlreadyTaggedErr extends Error {
224
247
  constructor(message: string) {
225
248
  super(message);
@@ -1,5 +1,5 @@
1
- import { CourseDBInterface, CourseInfo, CoursesDBInterface, UserDBInterface } from '@/core';
2
- import { ScheduledCard } from '@/core/types/user';
1
+ import { CourseDBInterface, CourseInfo, CoursesDBInterface, UserDBInterface } from '@db/core';
2
+ import { ScheduledCard } from '@db/core/types/user';
3
3
  import {
4
4
  CourseConfig,
5
5
  CourseElo,
@@ -11,6 +11,7 @@ import {
11
11
  } from '@vue-skuilder/common';
12
12
  import _ from 'lodash';
13
13
  import { filterAllDocsByPrefix, getCourseDB, getCourseDoc, getCourseDocs } from '.';
14
+ import UpdateQueue from './updateQueue';
14
15
  import {
15
16
  StudyContentSource,
16
17
  StudySessionItem,
@@ -21,11 +22,11 @@ import { CardData, DocType, SkuilderCourseData, Tag, TagStub } from '../../core/
21
22
  import { logger } from '../../util/logger';
22
23
  import { GET_CACHED } from './clientCache';
23
24
  import { addNote55, addTagToCard, getCredentialledCourseConfig, getTagID } from './courseAPI';
24
- import { DataLayerResult } from '@/core/types/db';
25
+ import { DataLayerResult } from '@db/core/types/db';
25
26
  import { PouchError } from './types';
26
27
  import CourseLookup from './courseLookupDB';
27
- import { ContentNavigationStrategyData } from '@/core/types/contentNavigationStrategy';
28
- import { ContentNavigator, Navigators } from '@/core/navigators';
28
+ import { ContentNavigationStrategyData } from '@db/core/types/contentNavigationStrategy';
29
+ import { ContentNavigator, Navigators } from '@db/core/navigators';
29
30
 
30
31
  export class CoursesDB implements CoursesDBInterface {
31
32
  _courseIDs: string[] | undefined;
@@ -92,11 +93,13 @@ export class CourseDB implements StudyContentSource, CourseDBInterface {
92
93
  private db: PouchDB.Database;
93
94
  private id: string;
94
95
  private _getCurrentUser: () => Promise<UserDBInterface>;
96
+ private updateQueue: UpdateQueue;
95
97
 
96
98
  constructor(id: string, userLookup: () => Promise<UserDBInterface>) {
97
99
  this.id = id;
98
100
  this.db = getCourseDB(this.id);
99
101
  this._getCurrentUser = userLookup;
102
+ this.updateQueue = new UpdateQueue(this.db);
100
103
  }
101
104
 
102
105
  public getCourseID(): string {
@@ -305,11 +308,22 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
305
308
  }
306
309
  }
307
310
 
308
- async updateCardElo(cardId: string, elo: CourseElo) {
309
- const ret = await updateCardElo(this.id, cardId, elo);
310
- if (ret) {
311
- return ret;
312
- } else {
311
+ async updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response> {
312
+ if (!elo) {
313
+ throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
314
+ }
315
+
316
+ try {
317
+ const result = await this.updateQueue.update<
318
+ CardData & PouchDB.Core.GetMeta & PouchDB.Core.IdMeta
319
+ >(cardId, (card) => {
320
+ logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
321
+ card.elo = elo;
322
+ return card;
323
+ });
324
+ return { ok: true, id: cardId, rev: result._rev };
325
+ } catch (error) {
326
+ logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
313
327
  throw new Error(`Failed to update card elo for card ID: ${cardId}`);
314
328
  }
315
329
  }
@@ -1,5 +1,5 @@
1
1
  import pouch from './pouchdb-setup';
2
- import { ENV } from '@/factory';
2
+ import { ENV } from '@db/factory';
3
3
  import { logger } from '../../util/logger';
4
4
 
5
5
  const courseLookupDBTitle = 'coursedb-lookup';
@@ -1,14 +1,13 @@
1
- import { ENV } from '@/factory';
1
+ import { ENV } from '@db/factory';
2
2
  import { DocType, GuestUsername, log, SkuilderCourseData } from '../../core/types/types-legacy';
3
3
  // import { getCurrentUser } from '../../stores/useAuthStore';
4
4
  import moment, { Moment } from 'moment';
5
- import { logger } from '@/util/logger';
5
+ import { logger } from '@db/util/logger';
6
6
 
7
7
  import pouch from './pouchdb-setup';
8
8
 
9
- import { ScheduledCard } from '@/core/types/user';
9
+ import { ScheduledCard } from '@db/core/types/user';
10
10
  import process from 'process';
11
- import { getUserDB } from './userDB';
12
11
 
13
12
  const isBrowser = typeof window !== 'undefined';
14
13
 
@@ -159,6 +158,28 @@ export async function getRandomCards(courseIDs: string[]) {
159
158
  export const REVIEW_PREFIX: string = 'card_review_';
160
159
  export const REVIEW_TIME_FORMAT: string = 'YYYY-MM-DD--kk:mm:ss-SSS';
161
160
 
161
+ export function getCouchUserDB(username: string): PouchDB.Database {
162
+ const guestAccount: boolean = false;
163
+ // console.log(`Getting user db: ${username}`);
164
+
165
+ const hexName = hexEncode(username);
166
+ const dbName = `userdb-${hexName}`;
167
+ log(`Fetching user database: ${dbName} (${username})`);
168
+
169
+ // odd construction here the result of a bug in the
170
+ // interaction between pouch, pouch-auth.
171
+ // see: https://github.com/pouchdb-community/pouchdb-authentication/issues/239
172
+ const ret = new pouch(
173
+ ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
174
+ pouchDBincludeCredentialsConfig
175
+ );
176
+ if (guestAccount) {
177
+ updateGuestAccountExpirationDate(ret);
178
+ }
179
+
180
+ return ret;
181
+ }
182
+
162
183
  export function scheduleCardReview(review: {
163
184
  user: string;
164
185
  course_id: string;
@@ -169,7 +190,7 @@ export function scheduleCardReview(review: {
169
190
  }) {
170
191
  const now = moment.utc();
171
192
  logger.info(`Scheduling for review in: ${review.time.diff(now, 'h') / 24} days`);
172
- void getUserDB(review.user).put<ScheduledCard>({
193
+ void getCouchUserDB(review.user).put<ScheduledCard>({
173
194
  _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
174
195
  cardId: review.card_id,
175
196
  reviewTime: review.time,
@@ -180,20 +201,6 @@ export function scheduleCardReview(review: {
180
201
  });
181
202
  }
182
203
 
183
- export async function removeScheduledCardReview(user: string, reviewDocID: string) {
184
- const db = getUserDB(user);
185
- const reviewDoc = await db.get(reviewDocID);
186
- db.remove(reviewDoc)
187
- .then((res) => {
188
- if (res.ok) {
189
- log(`Removed Review Doc: ${reviewDocID}`);
190
- }
191
- })
192
- .catch((err) => {
193
- log(`Failed to remove Review Doc: ${reviewDocID},\n${JSON.stringify(err)}`);
194
- });
195
- }
196
-
197
204
  export function filterAllDocsByPrefix<T>(
198
205
  db: PouchDB.Database,
199
206
  prefix: string,
@@ -232,4 +239,4 @@ export * from './adminDB';
232
239
  export * from './classroomDB';
233
240
  export * from './courseAPI';
234
241
  export * from './courseDB';
235
- export * from './userDB';
242
+ export * from './CouchDBSyncStrategy';
@@ -77,9 +77,13 @@ export default class UpdateQueue extends Loggable {
77
77
  }
78
78
  return doc;
79
79
  } catch (e) {
80
+ // Clean up queue state before re-throwing
80
81
  delete this.inprogressUpdates[id];
82
+ if (this.pendingUpdates[id]) {
83
+ delete this.pendingUpdates[id];
84
+ }
81
85
  logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
82
- throw e;
86
+ throw e; // Let caller handle (e.g., putCardRecord's 404 handling)
83
87
  }
84
88
  } else {
85
89
  throw new Error(`Empty Updates Queue Triggered`);
@@ -1,4 +1,9 @@
1
- import { ScheduledCard, UserCourseSetting, UserCourseSettings, UsrCrsDataInterface } from '@/core';
1
+ import {
2
+ ScheduledCard,
3
+ UserCourseSetting,
4
+ UserCourseSettings,
5
+ UsrCrsDataInterface,
6
+ } from '@db/core';
2
7
  import moment, { Moment } from 'moment';
3
8
  import { getStartAndEndKeys, REVIEW_PREFIX, REVIEW_TIME_FORMAT } from '.';
4
9
  import { CourseDB } from './courseDB';
@@ -0,0 +1,70 @@
1
+ // packages/db/src/impl/static/NoOpSyncStrategy.ts
2
+
3
+ import { GuestUsername } from '../../core/types/types-legacy';
4
+ import type { SyncStrategy } from '../common/SyncStrategy';
5
+ import type { AccountCreationResult, AuthenticationResult } from '../common/types';
6
+ import { getLocalUserDB } from '../common';
7
+
8
+ /**
9
+ * Sync strategy for static deployments that provides no-op implementations
10
+ * for remote operations. Uses only local storage with no remote synchronization.
11
+ */
12
+ export class NoOpSyncStrategy implements SyncStrategy {
13
+ private currentUsername: string = GuestUsername;
14
+
15
+ setupRemoteDB(username: string): PouchDB.Database {
16
+ // Return the same database instance as local - sync with self is a no-op
17
+ return getLocalUserDB(username);
18
+ }
19
+
20
+ startSync(_localDB: PouchDB.Database, _remoteDB: PouchDB.Database): void {
21
+ // No-op - in static mode, local and remote are the same database instance
22
+ // PouchDB sync with itself is harmless and efficient
23
+ }
24
+
25
+ stopSync?(): void {
26
+ // No-op - no sync to stop in static mode
27
+ }
28
+
29
+ canCreateAccount(): boolean {
30
+ return false; // Account creation not supported in static mode
31
+ }
32
+
33
+ canAuthenticate(): boolean {
34
+ return false; // Remote authentication not supported in static mode
35
+ }
36
+
37
+ async createAccount(_username: string, _password: string): Promise<AccountCreationResult> {
38
+ throw new Error(
39
+ 'Account creation not supported in static mode. Use local account switching instead.'
40
+ );
41
+ }
42
+
43
+ async authenticate(_username: string, _password: string): Promise<AuthenticationResult> {
44
+ throw new Error(
45
+ 'Remote authentication not supported in static mode. Use local account switching instead.'
46
+ );
47
+ }
48
+
49
+ async logout(): Promise<AuthenticationResult> {
50
+ // In static mode, "logout" means switch back to guest user
51
+ this.currentUsername = GuestUsername;
52
+ return {
53
+ ok: true,
54
+ };
55
+ }
56
+
57
+ async getCurrentUsername(): Promise<string> {
58
+ // In static mode, always return guest username
59
+ // TODO: This will be enhanced with local account switching to support multiple local users
60
+ return this.currentUsername;
61
+ }
62
+
63
+ /**
64
+ * Set the current username (for local account switching)
65
+ * This method is specific to NoOpSyncStrategy and not part of the base interface
66
+ */
67
+ setCurrentUsername(username: string): void {
68
+ this.currentUsername = username;
69
+ }
70
+ }
@@ -0,0 +1,89 @@
1
+ // packages/db/src/impl/static/StaticDataLayerProvider.ts
2
+
3
+ import {
4
+ AdminDBInterface,
5
+ ClassroomDBInterface,
6
+ CoursesDBInterface,
7
+ CourseDBInterface,
8
+ DataLayerProvider,
9
+ UserDBInterface,
10
+ } from '../../core/interfaces';
11
+ import { logger } from '../../util/logger';
12
+ import { StaticCourseManifest } from '../../util/packer/types';
13
+ import { StaticDataUnpacker } from './StaticDataUnpacker';
14
+ import { StaticCourseDB } from './courseDB';
15
+ import { StaticCoursesDB } from './coursesDB';
16
+ import { BaseUser } from '../common';
17
+ import { NoOpSyncStrategy } from './NoOpSyncStrategy';
18
+
19
+ interface StaticDataLayerConfig {
20
+ staticContentPath: string;
21
+ localStoragePrefix?: string;
22
+ manifests: Record<string, StaticCourseManifest>; // courseId -> manifest
23
+ }
24
+
25
+ export class StaticDataLayerProvider implements DataLayerProvider {
26
+ private config: StaticDataLayerConfig;
27
+ private initialized: boolean = false;
28
+ private courseUnpackers: Map<string, StaticDataUnpacker> = new Map();
29
+
30
+ constructor(config: Partial<StaticDataLayerConfig>) {
31
+ this.config = {
32
+ staticContentPath: config.staticContentPath || '/static-courses',
33
+ localStoragePrefix: config.localStoragePrefix || 'skuilder-static',
34
+ manifests: config.manifests || {},
35
+ };
36
+ }
37
+
38
+ async initialize(): Promise<void> {
39
+ if (this.initialized) return;
40
+
41
+ logger.info('Initializing static data layer provider');
42
+
43
+ // Load manifests for all courses
44
+ for (const [courseId, manifest] of Object.entries(this.config.manifests)) {
45
+ const unpacker = new StaticDataUnpacker(
46
+ manifest,
47
+ `${this.config.staticContentPath}/${courseId}`
48
+ );
49
+ this.courseUnpackers.set(courseId, unpacker);
50
+ }
51
+
52
+ this.initialized = true;
53
+ }
54
+
55
+ async teardown(): Promise<void> {
56
+ this.courseUnpackers.clear();
57
+ this.initialized = false;
58
+ }
59
+
60
+ getUserDB(): UserDBInterface {
61
+ const syncStrategy = new NoOpSyncStrategy();
62
+ // For now, use guest user - local account switching will be added later
63
+ return BaseUser.Dummy(syncStrategy);
64
+ }
65
+
66
+ getCourseDB(courseId: string): CourseDBInterface {
67
+ const unpacker = this.courseUnpackers.get(courseId);
68
+ if (!unpacker) {
69
+ throw new Error(`Course ${courseId} not found in static data`);
70
+ }
71
+ const manifest = this.config.manifests[courseId];
72
+ return new StaticCourseDB(courseId, unpacker, this.getUserDB(), manifest);
73
+ }
74
+
75
+ getCoursesDB(): CoursesDBInterface {
76
+ return new StaticCoursesDB(this.config.manifests);
77
+ }
78
+
79
+ async getClassroomDB(
80
+ _classId: string,
81
+ _type: 'student' | 'teacher'
82
+ ): Promise<ClassroomDBInterface> {
83
+ throw new Error('Classrooms not supported in static mode');
84
+ }
85
+
86
+ getAdminDB(): AdminDBInterface {
87
+ throw new Error('Admin functions not supported in static mode');
88
+ }
89
+ }