diff --git a/packages/db/package.json b/packages/db/package.json index ddb7eda06..3abe81090 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -51,6 +51,7 @@ "pouchdb-find": "^9.0.0" }, "devDependencies": { + "@types/uuid": "^10.0.0", "tsup": "^8.0.2", "typescript": "~5.7.2" } diff --git a/packages/db/src/core/types/types-legacy.ts b/packages/db/src/core/types/types-legacy.ts index 77412a2c3..7b6ca59ec 100644 --- a/packages/db/src/core/types/types-legacy.ts +++ b/packages/db/src/core/types/types-legacy.ts @@ -86,7 +86,19 @@ export interface QuestionData extends SkuilderCourseData { dataShapeList: PouchDB.Core.DocumentId[]; } -export const cardHistoryPrefix = 'cardH'; +export const DocTypePrefixes: Record = { + [DocType.CARD]: 'c', + [DocType.DISPLAYABLE_DATA]: 'dd', + [DocType.TAG]: 'TAG', + [DocType.CARDRECORD]: 'cardH', + [DocType.SCHEDULED_CARD]: 'card_review_', + // Add other doctypes here as they get prefixed IDs + [DocType.DATASHAPE]: 'DATASHAPE', + [DocType.QUESTIONTYPE]: 'QUESTION', + [DocType.VIEW]: 'VIEW', + [DocType.PEDAGOGY]: 'PEDAGOGY', + [DocType.NAVIGATION_STRATEGY]: 'NAVIGATION_STRATEGY', +}; export interface CardHistory { _id: PouchDB.Core.DocumentId; diff --git a/packages/db/src/core/util/index.ts b/packages/db/src/core/util/index.ts index 9f030e922..a831a24e8 100644 --- a/packages/db/src/core/util/index.ts +++ b/packages/db/src/core/util/index.ts @@ -1,4 +1,4 @@ -import { cardHistoryPrefix, CardHistory, CardRecord, QuestionRecord } from '../types/types-legacy'; +import { DocType, DocTypePrefixes, CardHistory, CardRecord, QuestionRecord } from '../types/types-legacy'; export function areQuestionRecords(h: CardHistory): h is CardHistory { return isQuestionRecord(h.records[0]); @@ -9,7 +9,7 @@ export function isQuestionRecord(c: CardRecord): c is QuestionRecord { } export function getCardHistoryID(courseID: string, cardID: string): PouchDB.Core.DocumentId { - return `${cardHistoryPrefix}-${courseID}-${cardID}`; + return `${DocTypePrefixes[DocType.CARDRECORD]}-${courseID}-${cardID}`; } export function parseCardHistoryID(id: string): { @@ -20,9 +20,10 @@ export function parseCardHistoryID(id: string): { let error: string = ''; error += split.length === 3 ? '' : `\n\tgiven ID has incorrect number of '-' characters`; error += - split[0] === cardHistoryPrefix ? '' : `\n\tgiven ID does not start with ${cardHistoryPrefix}`; + split[0] === DocTypePrefixes[DocType.CARDRECORD] ? '' : ` + given ID does not start with ${DocTypePrefixes[DocType.CARDRECORD]}`; - if (split.length === 3 && split[0] === cardHistoryPrefix) { + if (split.length === 3 && split[0] === DocTypePrefixes[DocType.CARDRECORD]) { return { courseID: split[1], cardID: split[2], diff --git a/packages/db/src/impl/common/BaseUserDB.ts b/packages/db/src/impl/common/BaseUserDB.ts index addc44136..630f0da9b 100644 --- a/packages/db/src/impl/common/BaseUserDB.ts +++ b/packages/db/src/impl/common/BaseUserDB.ts @@ -1,3 +1,4 @@ +import { DocType, DocTypePrefixes } from '@db/core'; import { getCardHistoryID } from '@db/core/util'; import { CourseElo, Status } from '@vue-skuilder/common'; import moment, { Moment } from 'moment'; @@ -23,7 +24,6 @@ import type { SyncStrategy } from './SyncStrategy'; import { filterAllDocsByPrefix, getStartAndEndKeys, - REVIEW_PREFIX, REVIEW_TIME_FORMAT, getLocalUserDB, scheduleCardReviewLocal, @@ -38,8 +38,6 @@ const log = (s: any) => { logger.info(s); }; -const cardHistoryPrefix = 'cardH-'; - // console.log(`Connecting to remote: ${remoteStr}`); interface DesignDoc { @@ -174,8 +172,8 @@ Currently logged-in as ${this._username}.` const id = row.id; // Delete user progress data but preserve core user documents return ( - id.startsWith(cardHistoryPrefix) || // Card interaction history - id.startsWith(REVIEW_PREFIX) || // Scheduled reviews + id.startsWith(DocTypePrefixes[DocType.CARDRECORD]) || // Card interaction history + id.startsWith(DocTypePrefixes[DocType.SCHEDULED_CARD]) || // Scheduled reviews id === BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations id === BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations id === BaseUser.DOC_IDS.CONFIG // User config @@ -264,7 +262,7 @@ Currently logged-in as ${this._username}.` * */ public async getActiveCards() { - const keys = getStartAndEndKeys(REVIEW_PREFIX); + const keys = getStartAndEndKeys(DocTypePrefixes[DocType.SCHEDULED_CARD]); const reviews = await this.remoteDB.allDocs({ startkey: keys.startkey, @@ -359,7 +357,7 @@ Currently logged-in as ${this._username}.` } private async getReviewstoDate(targetDate: Moment, course_id?: string) { - const keys = getStartAndEndKeys(REVIEW_PREFIX); + const keys = getStartAndEndKeys(DocTypePrefixes[DocType.SCHEDULED_CARD]); const reviews = await this.remoteDB.allDocs({ startkey: keys.startkey, @@ -374,8 +372,11 @@ Currently logged-in as ${this._username}.` ); return reviews.rows .filter((r) => { - if (r.id.startsWith(REVIEW_PREFIX)) { - const date = moment.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT); + if (r.id.startsWith(DocTypePrefixes[DocType.SCHEDULED_CARD])) { + const date = moment.utc( + r.id.substr(DocTypePrefixes[DocType.SCHEDULED_CARD].length), + REVIEW_TIME_FORMAT + ); if (targetDate.isAfter(date)) { if (course_id === undefined || r.doc!.courseId === course_id) { return true; @@ -816,7 +817,7 @@ Currently logged-in as ${this._username}.` * @param course_id optional specification of individual course */ async getSeenCards(course_id?: string) { - let prefix = cardHistoryPrefix; + let prefix = DocTypePrefixes[DocType.CARDRECORD]; if (course_id) { prefix += course_id; } @@ -826,8 +827,8 @@ Currently logged-in as ${this._username}.` // const docs = await this.localDB.allDocs({}); const ret: PouchDB.Core.DocumentId[] = []; docs.rows.forEach((row) => { - if (row.id.startsWith(cardHistoryPrefix)) { - ret.push(row.id.substr(cardHistoryPrefix.length)); + if (row.id.startsWith(DocTypePrefixes[DocType.CARDRECORD])) { + ret.push(row.id.substr(DocTypePrefixes[DocType.CARDRECORD].length)); } }); return ret; @@ -840,7 +841,7 @@ Currently logged-in as ${this._username}.` async getHistory() { const cards = await filterAllDocsByPrefix>( this.remoteDB, - cardHistoryPrefix, + DocTypePrefixes[DocType.CARDRECORD], { include_docs: true, attachments: false, diff --git a/packages/db/src/impl/common/index.ts b/packages/db/src/impl/common/index.ts index a483bc462..cab91b8e0 100644 --- a/packages/db/src/impl/common/index.ts +++ b/packages/db/src/impl/common/index.ts @@ -11,7 +11,6 @@ export type { } from './types'; export { BaseUser } from './BaseUserDB'; export { - REVIEW_PREFIX, REVIEW_TIME_FORMAT, hexEncode, filterAllDocsByPrefix, diff --git a/packages/db/src/impl/common/userDBHelpers.ts b/packages/db/src/impl/common/userDBHelpers.ts index 537667909..ecb76ad8e 100644 --- a/packages/db/src/impl/common/userDBHelpers.ts +++ b/packages/db/src/impl/common/userDBHelpers.ts @@ -1,10 +1,10 @@ // packages/db/src/impl/common/userDBHelpers.ts import moment from 'moment'; +import { DocType, DocTypePrefixes } from '@db/core'; import { logger } from '../../util/logger'; import { ScheduledCard } from '@db/core/types/user'; -export const REVIEW_PREFIX: string = 'card_review_'; export const REVIEW_TIME_FORMAT: string = 'YYYY-MM-DD--kk:mm:ss-SSS'; import pouch from '../couch/pouchdb-setup'; @@ -123,7 +123,7 @@ export function scheduleCardReviewLocal( const now = moment.utc(); logger.info(`Scheduling for review in: ${review.time.diff(now, 'h') / 24} days`); void userDB.put({ - _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT), + _id: DocTypePrefixes[DocType.SCHEDULED_CARD] + review.time.format(REVIEW_TIME_FORMAT), cardId: review.card_id, reviewTime: review.time, courseId: review.course_id, diff --git a/packages/db/src/impl/couch/courseAPI.ts b/packages/db/src/impl/couch/courseAPI.ts index dba4c307a..ff4dc1344 100644 --- a/packages/db/src/impl/couch/courseAPI.ts +++ b/packages/db/src/impl/couch/courseAPI.ts @@ -6,10 +6,11 @@ import { NameSpacer, ShapeDescriptor } from '@vue-skuilder/common'; import { CourseConfig, DataShape } from '@vue-skuilder/common'; import { CourseElo, blankCourseElo, toCourseElo } from '@vue-skuilder/common'; import { CourseDB, createTag } from './courseDB'; -import { CardData, DisplayableData, DocType, Tag } from '../../core/types/types-legacy'; +import { CardData, DisplayableData, DocType, Tag, DocTypePrefixes } from '../../core/types/types-legacy'; import { prepareNote55 } from '@vue-skuilder/common'; import { BaseUser } from '../common'; import { logger } from '@db/util/logger'; +import { v4 as uuidv4 } from 'uuid'; /** * @@ -33,9 +34,8 @@ export async function addNote55( ): Promise { const db = getCourseDB(courseID); const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads); - // [ ] NAMESPACING: consider put( _id: "displayable_data-uuid") - // consider also semantic hashing - const result = await db.post(payload); + const _id = `${DocTypePrefixes[DocType.DISPLAYABLE_DATA]}-${uuidv4()}`; + const result = await db.put({ ...payload, _id }); const dataShapeId = NameSpacer.getDataShapeString({ course: codeCourse, @@ -153,9 +153,10 @@ async function addCard( tags: string[], author: string ): Promise { - // [ ] NAMESPACING: consider put( _id: "card-uuid") const db = getCourseDB(courseID); - const card = await db.post({ + const _id = `${DocTypePrefixes[DocType.CARD]}-${uuidv4()}`; + const card = await db.put({ + _id, course, id_displayable_data, id_view, diff --git a/packages/db/src/impl/couch/index.ts b/packages/db/src/impl/couch/index.ts index 2352b0d62..793a2f65a 100644 --- a/packages/db/src/impl/couch/index.ts +++ b/packages/db/src/impl/couch/index.ts @@ -1,5 +1,11 @@ import { ENV } from '@db/factory'; -import { DocType, GuestUsername, log, SkuilderCourseData } from '../../core/types/types-legacy'; +import { + DocType, + DocTypePrefixes, + GuestUsername, + log, + SkuilderCourseData, +} from '../../core/types/types-legacy'; // import { getCurrentUser } from '../../stores/useAuthStore'; import moment, { Moment } from 'moment'; import { logger } from '@db/util/logger'; @@ -155,7 +161,6 @@ export async function getRandomCards(courseIDs: string[]) { } } -export const REVIEW_PREFIX: string = 'card_review_'; export const REVIEW_TIME_FORMAT: string = 'YYYY-MM-DD--kk:mm:ss-SSS'; export function getCouchUserDB(username: string): PouchDB.Database { @@ -191,7 +196,7 @@ export function scheduleCardReview(review: { const now = moment.utc(); logger.info(`Scheduling for review in: ${review.time.diff(now, 'h') / 24} days`); void getCouchUserDB(review.user).put({ - _id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT), + _id: DocTypePrefixes[DocType.SCHEDULED_CARD] + review.time.format(REVIEW_TIME_FORMAT), cardId: review.card_id, reviewTime: review.time, courseId: review.course_id, diff --git a/packages/db/src/impl/couch/user-course-relDB.ts b/packages/db/src/impl/couch/user-course-relDB.ts index 23015d356..17f60e02c 100644 --- a/packages/db/src/impl/couch/user-course-relDB.ts +++ b/packages/db/src/impl/couch/user-course-relDB.ts @@ -4,20 +4,18 @@ import { UserCourseSettings, UsrCrsDataInterface, } from '@db/core'; + import moment, { Moment } from 'moment'; -import { getStartAndEndKeys, REVIEW_PREFIX, REVIEW_TIME_FORMAT } from '.'; -import { CourseDB } from './courseDB'; -import { User } from './userDB'; + +import { UserDBInterface } from '@db/core'; import { logger } from '../../util/logger'; export class UsrCrsData implements UsrCrsDataInterface { - private user: User; - private course: CourseDB; + private user: UserDBInterface; private _courseId: string; - constructor(user: User, courseId: string) { + constructor(user: UserDBInterface, courseId: string) { this.user = user; - this.course = new CourseDB(courseId, async () => this.user); this._courseId = courseId; } @@ -47,32 +45,24 @@ export class UsrCrsData implements UsrCrsDataInterface { } } public updateCourseSettings(updates: UserCourseSetting[]): void { - void this.user.updateCourseSettings(this._courseId, updates); + // TODO: Add updateCourseSettings method to UserDBInterface + // For now, we'll need to cast to access the concrete implementation + if ('updateCourseSettings' in this.user) { + void (this.user as any).updateCourseSettings(this._courseId, updates); + } } private async getReviewstoDate(targetDate: Moment) { - const keys = getStartAndEndKeys(REVIEW_PREFIX); - - const reviews = await this.user.remote().allDocs({ - startkey: keys.startkey, - endkey: keys.endkey, - include_docs: true, - }); + // Use the interface method instead of direct database access + const allReviews = await this.user.getPendingReviews(this._courseId); logger.debug( `Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.` ); - return reviews.rows - .filter((r) => { - if (r.id.startsWith(REVIEW_PREFIX)) { - const date = moment.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT); - if (targetDate.isAfter(date)) { - if (this._courseId === undefined || r.doc!.courseId === this._courseId) { - return true; - } - } - } - }) - .map((r) => r.doc!); + + return allReviews.filter((review: ScheduledCard) => { + const reviewTime = moment.utc(review.reviewTime); + return targetDate.isAfter(reviewTime); + }); } } diff --git a/packages/db/src/impl/static/StaticDataUnpacker.ts b/packages/db/src/impl/static/StaticDataUnpacker.ts index cd9d5057d..a074362da 100644 --- a/packages/db/src/impl/static/StaticDataUnpacker.ts +++ b/packages/db/src/impl/static/StaticDataUnpacker.ts @@ -2,7 +2,7 @@ import { StaticCourseManifest, ChunkMetadata } from '../../util/packer/types'; import { logger } from '../../util/logger'; -import { DocType } from '@db/core'; +import { DocType, DocTypePrefixes } from '@db/core'; // Browser-compatible path utilities const pathUtils = { @@ -151,57 +151,38 @@ export class StaticDataUnpacker { return (await this.loadIndex('tags')) as TagsIndex; } + private getDocTypeFromId(id: string): DocType | undefined { + for (const docTypeKey in DocTypePrefixes) { + const prefix = DocTypePrefixes[docTypeKey as DocType]; + if (id.startsWith(`${prefix}-`)) { + return docTypeKey as DocType; + } + } + return undefined; + } + /** * Find which chunk contains a specific document ID */ private async findChunkForDocument(docId: string): Promise { - // Determine document type from ID pattern by checking all DocType enum members - let expectedDocType: DocType | undefined = undefined; - - // Check for ID prefixes matching any DocType enum value - for (const docType of Object.values(DocType)) { - if (docId.startsWith(`${docType}-`)) { - expectedDocType = docType; - break; - } - } + const expectedDocType = this.getDocTypeFromId(docId); - if (expectedDocType !== undefined) { - // Use chunk filtering by docType for documents with recognized prefixes + if (expectedDocType) { const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType); for (const chunk of typeChunks) { if (docId >= chunk.startKey && docId <= chunk.endKey) { - // Verify document actually exists in chunk const exists = await this.verifyDocumentInChunk(docId, chunk); if (exists) { return chunk; } } } - - return undefined; } else { - // Fall back to trying all chunk types with strict verification - // Since card IDs and displayable data IDs can overlap in range, we need to verify actual existence - - // First try DISPLAYABLE_DATA chunks (most likely for documents without prefixes) - const displayableChunks = this.manifest.chunks.filter( - (c) => c.docType === 'DISPLAYABLE_DATA' - ); - for (const chunk of displayableChunks) { - if (docId >= chunk.startKey && docId <= chunk.endKey) { - // Verify document actually exists in chunk - const exists = await this.verifyDocumentInChunk(docId, chunk); - if (exists) { - return chunk; - } - } - } - - // Then try CARD chunks (for legacy card IDs without prefixes) - const cardChunks = this.manifest.chunks.filter((c) => c.docType === 'CARD'); - for (const chunk of cardChunks) { + // Fallback for documents without recognized prefixes (e.g., CourseConfig, or old documents) + // This part remains for backward compatibility and non-prefixed documents. + // It's less efficient but necessary if not all document types are prefixed. + for (const chunk of this.manifest.chunks) { if (docId >= chunk.startKey && docId <= chunk.endKey) { // Verify document actually exists in chunk const exists = await this.verifyDocumentInChunk(docId, chunk); @@ -227,6 +208,7 @@ export class StaticDataUnpacker { return undefined; } + return undefined; } /** diff --git a/yarn.lock b/yarn.lock index 1447c6cd7..3d5ec10f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4538,6 +4538,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: 10c0/9a1404bf287164481cb9b97f6bb638f78f955be57c40c6513b7655160beb29df6f84c915aaf4089a1559c216557dc4d2f79b48d978742d3ae10b937420ddac60 + languageName: node + linkType: hard + "@types/wavesurfer.js@npm:^5.1.0": version: 5.2.2 resolution: "@types/wavesurfer.js@npm:5.2.2" @@ -5315,6 +5322,7 @@ __metadata: resolution: "@vue-skuilder/db@workspace:packages/db" dependencies: "@nilock2/pouchdb-authentication": "npm:^1.0.2" + "@types/uuid": "npm:^10.0.0" "@vue-skuilder/common": "workspace:*" moment: "npm:^2.29.4" pouchdb: "npm:^9.0.0"