@vue-skuilder/db 0.1.6 → 0.1.8-0
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.
- package/dist/{SyncStrategy-DnJRj-Xp.d.mts → SyncStrategy-CyATpyLQ.d.mts} +6 -0
- package/dist/{SyncStrategy-DnJRj-Xp.d.ts → SyncStrategy-CyATpyLQ.d.ts} +6 -0
- package/dist/core/index.d.mts +5 -5
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +825 -762
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +812 -750
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BZmLyBVw.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
- package/dist/{dataLayerProvider-BuntXkCs.d.ts → dataLayerProvider-DqtNroSh.d.ts} +1 -1
- package/dist/impl/couch/index.d.mts +6 -6
- package/dist/impl/couch/index.d.ts +6 -6
- package/dist/impl/couch/index.js +2261 -2081
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2274 -2095
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.mts +8 -6
- package/dist/impl/static/index.d.ts +8 -6
- package/dist/impl/static/index.js +524 -1064
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +515 -1058
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index-CLL31bEy.d.ts +137 -0
- package/dist/index-CUNnL38E.d.mts +137 -0
- package/dist/index.d.mts +200 -9
- package/dist/index.d.ts +200 -9
- package/dist/index.js +4123 -2820
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4119 -2830
- package/dist/index.mjs.map +1 -1
- package/dist/{types-D6SnlHPm.d.ts → types-BefDGkKa.d.ts} +1 -1
- package/dist/{types-DPRvCrIk.d.mts → types-DC-ckZug.d.mts} +1 -1
- package/dist/{types-legacy-WPe8CtO-.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
- package/dist/{types-legacy-WPe8CtO-.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
- package/dist/{userDB-D9EuWTp1.d.ts → userDB-C33Hzjgn.d.mts} +11 -4
- package/dist/{userDB-31gsvxyd.d.mts → userDB-DusL7OXe.d.ts} +11 -4
- package/dist/util/packer/index.d.mts +3 -63
- package/dist/util/packer/index.d.ts +3 -63
- package/dist/util/packer/index.js +53 -1
- package/dist/util/packer/index.js.map +1 -1
- package/dist/util/packer/index.mjs +53 -1
- package/dist/util/packer/index.mjs.map +1 -1
- package/package.json +7 -4
- package/src/core/types/types-legacy.ts +13 -1
- package/src/core/types/user.ts +9 -2
- package/src/core/util/index.ts +5 -4
- package/src/factory.ts +25 -0
- package/src/impl/common/BaseUserDB.ts +62 -28
- package/src/impl/common/SyncStrategy.ts +7 -0
- package/src/impl/common/index.ts +0 -1
- package/src/impl/common/userDBHelpers.ts +15 -5
- package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
- package/src/impl/couch/courseAPI.ts +7 -6
- package/src/impl/couch/courseLookupDB.ts +24 -0
- package/src/impl/couch/index.ts +10 -5
- package/src/impl/couch/updateQueue.ts +12 -8
- package/src/impl/couch/user-course-relDB.ts +17 -27
- package/src/impl/static/NoOpSyncStrategy.ts +5 -0
- package/src/impl/static/StaticDataUnpacker.ts +18 -36
- package/src/impl/static/courseDB.ts +135 -17
- package/src/util/dataDirectory.test.ts +53 -0
- package/src/util/dataDirectory.ts +52 -0
- package/src/util/index.ts +3 -0
- package/src/util/migrator/FileSystemAdapter.ts +79 -0
- package/src/util/migrator/StaticToCouchDBMigrator.ts +713 -0
- package/src/util/migrator/index.ts +18 -0
- package/src/util/migrator/types.ts +84 -0
- package/src/util/migrator/validation.ts +517 -0
- package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
- package/src/util/tuiLogger.ts +139 -0
|
@@ -6,10 +6,11 @@ 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
8
|
import { CourseDB, createTag } from './courseDB';
|
|
9
|
-
import { CardData, DisplayableData, DocType, Tag } from '../../core/types/types-legacy';
|
|
9
|
+
import { CardData, DisplayableData, DocType, Tag, DocTypePrefixes } from '../../core/types/types-legacy';
|
|
10
10
|
import { prepareNote55 } from '@vue-skuilder/common';
|
|
11
11
|
import { BaseUser } from '../common';
|
|
12
12
|
import { logger } from '@db/util/logger';
|
|
13
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
*
|
|
@@ -33,9 +34,8 @@ export async function addNote55(
|
|
|
33
34
|
): Promise<PouchDB.Core.Response> {
|
|
34
35
|
const db = getCourseDB(courseID);
|
|
35
36
|
const payload = prepareNote55(courseID, codeCourse, shape, data, author, tags, uploads);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const result = await db.post<DisplayableData>(payload);
|
|
37
|
+
const _id = `${DocTypePrefixes[DocType.DISPLAYABLE_DATA]}-${uuidv4()}`;
|
|
38
|
+
const result = await db.put<DisplayableData>({ ...payload, _id });
|
|
39
39
|
|
|
40
40
|
const dataShapeId = NameSpacer.getDataShapeString({
|
|
41
41
|
course: codeCourse,
|
|
@@ -153,9 +153,10 @@ async function addCard(
|
|
|
153
153
|
tags: string[],
|
|
154
154
|
author: string
|
|
155
155
|
): Promise<PouchDB.Core.Response> {
|
|
156
|
-
// [ ] NAMESPACING: consider put( _id: "card-uuid")
|
|
157
156
|
const db = getCourseDB(courseID);
|
|
158
|
-
const
|
|
157
|
+
const _id = `${DocTypePrefixes[DocType.CARD]}-${uuidv4()}`;
|
|
158
|
+
const card = await db.put<CardData>({
|
|
159
|
+
_id,
|
|
159
160
|
course,
|
|
160
161
|
id_displayable_data,
|
|
161
162
|
id_view,
|
|
@@ -97,6 +97,30 @@ export default class CourseLookup {
|
|
|
97
97
|
return resp.id;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Adds a new course to the lookup database with a specific courseID
|
|
102
|
+
* @param courseId The specific course ID to use
|
|
103
|
+
* @param courseName The course name
|
|
104
|
+
* @param disambiguator Optional disambiguator
|
|
105
|
+
* @returns Promise<void>
|
|
106
|
+
*/
|
|
107
|
+
static async addWithId(
|
|
108
|
+
courseId: string,
|
|
109
|
+
courseName: string,
|
|
110
|
+
disambiguator?: string
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const doc: Omit<CourseLookupDoc, '_rev'> = {
|
|
113
|
+
_id: courseId,
|
|
114
|
+
name: courseName,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (disambiguator) {
|
|
118
|
+
doc.disambiguator = disambiguator;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await CourseLookup._db.put(doc);
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
/**
|
|
101
125
|
* Removes a course from the index
|
|
102
126
|
* @param courseID
|
package/src/impl/couch/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ENV } from '@db/factory';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DocType,
|
|
4
|
+
DocTypePrefixes,
|
|
5
|
+
GuestUsername,
|
|
6
|
+
log,
|
|
7
|
+
SkuilderCourseData,
|
|
8
|
+
} from '../../core/types/types-legacy';
|
|
3
9
|
// import { getCurrentUser } from '../../stores/useAuthStore';
|
|
4
10
|
import moment, { Moment } from 'moment';
|
|
5
11
|
import { logger } from '@db/util/logger';
|
|
@@ -155,7 +161,6 @@ export async function getRandomCards(courseIDs: string[]) {
|
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
export const REVIEW_PREFIX: string = 'card_review_';
|
|
159
164
|
export const REVIEW_TIME_FORMAT: string = 'YYYY-MM-DD--kk:mm:ss-SSS';
|
|
160
165
|
|
|
161
166
|
export function getCouchUserDB(username: string): PouchDB.Database {
|
|
@@ -191,11 +196,11 @@ export function scheduleCardReview(review: {
|
|
|
191
196
|
const now = moment.utc();
|
|
192
197
|
logger.info(`Scheduling for review in: ${review.time.diff(now, 'h') / 24} days`);
|
|
193
198
|
void getCouchUserDB(review.user).put<ScheduledCard>({
|
|
194
|
-
_id:
|
|
199
|
+
_id: DocTypePrefixes[DocType.SCHEDULED_CARD] + review.time.format(REVIEW_TIME_FORMAT),
|
|
195
200
|
cardId: review.card_id,
|
|
196
|
-
reviewTime: review.time,
|
|
201
|
+
reviewTime: review.time.toISOString(),
|
|
197
202
|
courseId: review.course_id,
|
|
198
|
-
scheduledAt: now,
|
|
203
|
+
scheduledAt: now.toISOString(),
|
|
199
204
|
scheduledFor: review.scheduledFor,
|
|
200
205
|
schedulingAgentId: review.schedulingAgentId,
|
|
201
206
|
});
|
|
@@ -12,7 +12,8 @@ export default class UpdateQueue extends Loggable {
|
|
|
12
12
|
[index: string]: boolean;
|
|
13
13
|
} = {};
|
|
14
14
|
|
|
15
|
-
private
|
|
15
|
+
private readDB: PouchDB.Database; // Database for read operations
|
|
16
|
+
private writeDB: PouchDB.Database; // Database for write operations (local-first)
|
|
16
17
|
|
|
17
18
|
public update<T extends PouchDB.Core.Document<object>>(
|
|
18
19
|
id: PouchDB.Core.DocumentId,
|
|
@@ -27,21 +28,24 @@ export default class UpdateQueue extends Loggable {
|
|
|
27
28
|
return this.applyUpdates<T>(id);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
constructor(
|
|
31
|
+
constructor(readDB: PouchDB.Database, writeDB?: PouchDB.Database) {
|
|
31
32
|
super();
|
|
32
33
|
// PouchDB.debug.enable('*');
|
|
33
|
-
this.
|
|
34
|
+
this.readDB = readDB;
|
|
35
|
+
this.writeDB = writeDB || readDB; // Default to readDB if writeDB not provided
|
|
34
36
|
logger.debug(`UpdateQ initialized...`);
|
|
35
|
-
void this.
|
|
37
|
+
void this.readDB.info().then((i) => {
|
|
36
38
|
logger.debug(`db info: ${JSON.stringify(i)}`);
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
private async applyUpdates<T extends PouchDB.Core.Document<object>>(
|
|
42
|
+
private async applyUpdates<T extends PouchDB.Core.Document<object>>(
|
|
43
|
+
id: string
|
|
44
|
+
): Promise<T & PouchDB.Core.GetMeta & PouchDB.Core.RevisionIdMeta> {
|
|
41
45
|
logger.debug(`Applying updates on doc: ${id}`);
|
|
42
46
|
if (this.inprogressUpdates[id]) {
|
|
43
47
|
// console.log(`Updates in progress...`);
|
|
44
|
-
await this.
|
|
48
|
+
await this.readDB.info(); // stall for a round trip
|
|
45
49
|
// console.log(`Retrying...`);
|
|
46
50
|
return this.applyUpdates<T>(id);
|
|
47
51
|
} else {
|
|
@@ -49,7 +53,7 @@ export default class UpdateQueue extends Loggable {
|
|
|
49
53
|
this.inprogressUpdates[id] = true;
|
|
50
54
|
|
|
51
55
|
try {
|
|
52
|
-
let doc = await this.
|
|
56
|
+
let doc = await this.readDB.get<T>(id);
|
|
53
57
|
logger.debug(`Retrieved doc: ${id}`);
|
|
54
58
|
while (this.pendingUpdates[id].length !== 0) {
|
|
55
59
|
const update = this.pendingUpdates[id].splice(0, 1)[0];
|
|
@@ -66,7 +70,7 @@ export default class UpdateQueue extends Loggable {
|
|
|
66
70
|
// console.log(`${k}: ${typeof k}`);
|
|
67
71
|
// }
|
|
68
72
|
// console.log(`Applied updates to doc: ${JSON.stringify(doc)}`);
|
|
69
|
-
await this.
|
|
73
|
+
await this.writeDB.put<T>(doc);
|
|
70
74
|
logger.debug(`Put doc: ${id}`);
|
|
71
75
|
|
|
72
76
|
if (this.pendingUpdates[id].length === 0) {
|
|
@@ -4,20 +4,18 @@ import {
|
|
|
4
4
|
UserCourseSettings,
|
|
5
5
|
UsrCrsDataInterface,
|
|
6
6
|
} from '@db/core';
|
|
7
|
+
|
|
7
8
|
import moment, { Moment } from 'moment';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import { User } from './userDB';
|
|
9
|
+
|
|
10
|
+
import { UserDBInterface } from '@db/core';
|
|
11
11
|
import { logger } from '../../util/logger';
|
|
12
12
|
|
|
13
13
|
export class UsrCrsData implements UsrCrsDataInterface {
|
|
14
|
-
private user:
|
|
15
|
-
private course: CourseDB;
|
|
14
|
+
private user: UserDBInterface;
|
|
16
15
|
private _courseId: string;
|
|
17
16
|
|
|
18
|
-
constructor(user:
|
|
17
|
+
constructor(user: UserDBInterface, courseId: string) {
|
|
19
18
|
this.user = user;
|
|
20
|
-
this.course = new CourseDB(courseId, async () => this.user);
|
|
21
19
|
this._courseId = courseId;
|
|
22
20
|
}
|
|
23
21
|
|
|
@@ -47,32 +45,24 @@ export class UsrCrsData implements UsrCrsDataInterface {
|
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
public updateCourseSettings(updates: UserCourseSetting[]): void {
|
|
50
|
-
|
|
48
|
+
// TODO: Add updateCourseSettings method to UserDBInterface
|
|
49
|
+
// For now, we'll need to cast to access the concrete implementation
|
|
50
|
+
if ('updateCourseSettings' in this.user) {
|
|
51
|
+
void (this.user as any).updateCourseSettings(this._courseId, updates);
|
|
52
|
+
}
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
private async getReviewstoDate(targetDate: Moment) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const reviews = await this.user.remote().allDocs<ScheduledCard>({
|
|
57
|
-
startkey: keys.startkey,
|
|
58
|
-
endkey: keys.endkey,
|
|
59
|
-
include_docs: true,
|
|
60
|
-
});
|
|
56
|
+
// Use the interface method instead of direct database access
|
|
57
|
+
const allReviews = await this.user.getPendingReviews(this._courseId);
|
|
61
58
|
|
|
62
59
|
logger.debug(
|
|
63
60
|
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
64
61
|
);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (this._courseId === undefined || r.doc!.courseId === this._courseId) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
.map((r) => r.doc!);
|
|
62
|
+
|
|
63
|
+
return allReviews.filter((review: ScheduledCard) => {
|
|
64
|
+
const reviewTime = moment.utc(review.reviewTime);
|
|
65
|
+
return targetDate.isAfter(reviewTime);
|
|
66
|
+
});
|
|
77
67
|
}
|
|
78
68
|
}
|
|
@@ -17,6 +17,11 @@ export class NoOpSyncStrategy implements SyncStrategy {
|
|
|
17
17
|
return getLocalUserDB(username);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
getWriteDB(username: string): PouchDB.Database {
|
|
21
|
+
// In static mode, always write to local database
|
|
22
|
+
return getLocalUserDB(username);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
startSync(_localDB: PouchDB.Database, _remoteDB: PouchDB.Database): void {
|
|
21
26
|
// No-op - in static mode, local and remote are the same database instance
|
|
22
27
|
// PouchDB sync with itself is harmless and efficient
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { StaticCourseManifest, ChunkMetadata } from '../../util/packer/types';
|
|
4
4
|
import { logger } from '../../util/logger';
|
|
5
|
-
import { DocType } from '@db/core';
|
|
5
|
+
import { DocType, DocTypePrefixes } from '@db/core';
|
|
6
6
|
|
|
7
7
|
// Browser-compatible path utilities
|
|
8
8
|
const pathUtils = {
|
|
@@ -151,57 +151,38 @@ export class StaticDataUnpacker {
|
|
|
151
151
|
return (await this.loadIndex('tags')) as TagsIndex;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
private getDocTypeFromId(id: string): DocType | undefined {
|
|
155
|
+
for (const docTypeKey in DocTypePrefixes) {
|
|
156
|
+
const prefix = DocTypePrefixes[docTypeKey as DocType];
|
|
157
|
+
if (id.startsWith(`${prefix}-`)) {
|
|
158
|
+
return docTypeKey as DocType;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
154
164
|
/**
|
|
155
165
|
* Find which chunk contains a specific document ID
|
|
156
166
|
*/
|
|
157
167
|
private async findChunkForDocument(docId: string): Promise<ChunkMetadata | undefined> {
|
|
158
|
-
|
|
159
|
-
let expectedDocType: DocType | undefined = undefined;
|
|
160
|
-
|
|
161
|
-
// Check for ID prefixes matching any DocType enum value
|
|
162
|
-
for (const docType of Object.values(DocType)) {
|
|
163
|
-
if (docId.startsWith(`${docType}-`)) {
|
|
164
|
-
expectedDocType = docType;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
+
const expectedDocType = this.getDocTypeFromId(docId);
|
|
168
169
|
|
|
169
|
-
if (expectedDocType
|
|
170
|
-
// Use chunk filtering by docType for documents with recognized prefixes
|
|
170
|
+
if (expectedDocType) {
|
|
171
171
|
const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
|
|
172
172
|
|
|
173
173
|
for (const chunk of typeChunks) {
|
|
174
174
|
if (docId >= chunk.startKey && docId <= chunk.endKey) {
|
|
175
|
-
// Verify document actually exists in chunk
|
|
176
175
|
const exists = await this.verifyDocumentInChunk(docId, chunk);
|
|
177
176
|
if (exists) {
|
|
178
177
|
return chunk;
|
|
179
178
|
}
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
|
-
|
|
183
|
-
return undefined;
|
|
184
181
|
} else {
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const displayableChunks = this.manifest.chunks.filter(
|
|
190
|
-
(c) => c.docType === 'DISPLAYABLE_DATA'
|
|
191
|
-
);
|
|
192
|
-
for (const chunk of displayableChunks) {
|
|
193
|
-
if (docId >= chunk.startKey && docId <= chunk.endKey) {
|
|
194
|
-
// Verify document actually exists in chunk
|
|
195
|
-
const exists = await this.verifyDocumentInChunk(docId, chunk);
|
|
196
|
-
if (exists) {
|
|
197
|
-
return chunk;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Then try CARD chunks (for legacy card IDs without prefixes)
|
|
203
|
-
const cardChunks = this.manifest.chunks.filter((c) => c.docType === 'CARD');
|
|
204
|
-
for (const chunk of cardChunks) {
|
|
182
|
+
// Fallback for documents without recognized prefixes (e.g., CourseConfig, or old documents)
|
|
183
|
+
// This part remains for backward compatibility and non-prefixed documents.
|
|
184
|
+
// It's less efficient but necessary if not all document types are prefixed.
|
|
185
|
+
for (const chunk of this.manifest.chunks) {
|
|
205
186
|
if (docId >= chunk.startKey && docId <= chunk.endKey) {
|
|
206
187
|
// Verify document actually exists in chunk
|
|
207
188
|
const exists = await this.verifyDocumentInChunk(docId, chunk);
|
|
@@ -227,6 +208,7 @@ export class StaticDataUnpacker {
|
|
|
227
208
|
|
|
228
209
|
return undefined;
|
|
229
210
|
}
|
|
211
|
+
return undefined;
|
|
230
212
|
}
|
|
231
213
|
|
|
232
214
|
/**
|
|
@@ -15,6 +15,7 @@ import { DataLayerResult } from '../../core/types/db';
|
|
|
15
15
|
import { ContentNavigationStrategyData } from '../../core/types/contentNavigationStrategy';
|
|
16
16
|
import { ScheduledCard } from '../../core/types/user';
|
|
17
17
|
import { Navigators } from '../../core/navigators';
|
|
18
|
+
import { logger } from '../../util/logger';
|
|
18
19
|
|
|
19
20
|
export class StaticCourseDB implements CourseDBInterface {
|
|
20
21
|
constructor(
|
|
@@ -41,10 +42,15 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
async getCourseInfo(): Promise<CourseInfo> {
|
|
44
|
-
//
|
|
45
|
+
// Count only cards, not all documents
|
|
46
|
+
// Use chunks metadata to count card documents specifically
|
|
47
|
+
const cardCount = this.manifest.chunks
|
|
48
|
+
.filter((chunk) => chunk.docType === DocType.CARD)
|
|
49
|
+
.reduce((total, chunk) => total + chunk.documentCount, 0);
|
|
50
|
+
|
|
45
51
|
return {
|
|
46
|
-
cardCount
|
|
47
|
-
registeredUsers: 0,
|
|
52
|
+
cardCount,
|
|
53
|
+
registeredUsers: 0, // Always 0 in static mode
|
|
48
54
|
};
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -158,13 +164,61 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
158
164
|
}));
|
|
159
165
|
}
|
|
160
166
|
|
|
161
|
-
async getAppliedTags(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
rows
|
|
167
|
-
|
|
167
|
+
async getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>> {
|
|
168
|
+
try {
|
|
169
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
170
|
+
const cardTags = tagsIndex.byCard[cardId] || [];
|
|
171
|
+
|
|
172
|
+
const rows = await Promise.all(
|
|
173
|
+
cardTags.map(async (tagName) => {
|
|
174
|
+
const tagId = `${DocType.TAG}-${tagName}`;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Try to get the full tag document
|
|
178
|
+
const tagDoc = await this.unpacker.getDocument(tagId);
|
|
179
|
+
return {
|
|
180
|
+
id: tagId,
|
|
181
|
+
key: cardId,
|
|
182
|
+
value: {
|
|
183
|
+
name: tagDoc.name,
|
|
184
|
+
snippet: tagDoc.snippet,
|
|
185
|
+
count: tagDoc.taggedCards?.length || 0,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error && (error as PouchDB.Core.Error).status === 404) {
|
|
190
|
+
logger.warn(`Tag document not found for ${tagName}, creating stub`);
|
|
191
|
+
} else {
|
|
192
|
+
logger.error(`Error getting tag document for ${tagName}:`, error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
// If tag document not found, create a minimal stub
|
|
196
|
+
return {
|
|
197
|
+
id: tagId,
|
|
198
|
+
key: cardId,
|
|
199
|
+
value: {
|
|
200
|
+
name: tagName,
|
|
201
|
+
snippet: `Tag: ${tagName}`,
|
|
202
|
+
count: tagsIndex.byTag[tagName]?.length || 0,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
total_rows: rows.length,
|
|
211
|
+
offset: 0,
|
|
212
|
+
rows,
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
logger.error(`Error getting applied tags for card ${cardId}:`, error);
|
|
216
|
+
return {
|
|
217
|
+
total_rows: 0,
|
|
218
|
+
offset: 0,
|
|
219
|
+
rows: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
168
222
|
}
|
|
169
223
|
|
|
170
224
|
async addTagToCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response> {
|
|
@@ -188,12 +242,76 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
188
242
|
}
|
|
189
243
|
|
|
190
244
|
async getCourseTagStubs(): Promise<PouchDB.Core.AllDocsResponse<Tag>> {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
245
|
+
try {
|
|
246
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
247
|
+
|
|
248
|
+
if (!tagsIndex || !tagsIndex.byTag) {
|
|
249
|
+
logger.warn('Tags index not found or empty');
|
|
250
|
+
return {
|
|
251
|
+
total_rows: 0,
|
|
252
|
+
offset: 0,
|
|
253
|
+
rows: [],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Create tag stubs from the index
|
|
258
|
+
const tagNames = Object.keys(tagsIndex.byTag);
|
|
259
|
+
const rows = await Promise.all(
|
|
260
|
+
tagNames.map(async (tagName) => {
|
|
261
|
+
const cardIds = tagsIndex.byTag[tagName] || [];
|
|
262
|
+
const tagId = `${DocType.TAG}-${tagName}`;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Try to get the full tag document
|
|
266
|
+
const tagDoc = await this.unpacker.getDocument(tagId);
|
|
267
|
+
return {
|
|
268
|
+
id: tagId,
|
|
269
|
+
key: tagId,
|
|
270
|
+
value: { rev: '1-static' },
|
|
271
|
+
doc: tagDoc,
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// If tag document not found, create a minimal stub
|
|
275
|
+
if (error && (error as PouchDB.Core.Error).status === 404) {
|
|
276
|
+
logger.warn(`Tag document not found for ${tagName}, creating stub`);
|
|
277
|
+
const stubDoc = {
|
|
278
|
+
_id: tagId,
|
|
279
|
+
_rev: '1-static',
|
|
280
|
+
course: this.courseId,
|
|
281
|
+
docType: DocType.TAG,
|
|
282
|
+
name: tagName,
|
|
283
|
+
snippet: `Tag: ${tagName}`,
|
|
284
|
+
wiki: '',
|
|
285
|
+
taggedCards: cardIds,
|
|
286
|
+
author: 'system',
|
|
287
|
+
};
|
|
288
|
+
return {
|
|
289
|
+
id: tagId,
|
|
290
|
+
key: tagId,
|
|
291
|
+
value: { rev: '1-static' },
|
|
292
|
+
doc: stubDoc,
|
|
293
|
+
};
|
|
294
|
+
} else {
|
|
295
|
+
logger.error(`Error getting tag document for ${tagName}:`, error);
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
total_rows: rows.length,
|
|
304
|
+
offset: 0,
|
|
305
|
+
rows,
|
|
306
|
+
};
|
|
307
|
+
} catch (error) {
|
|
308
|
+
logger.error('Failed to get course tag stubs:', error);
|
|
309
|
+
return {
|
|
310
|
+
total_rows: 0,
|
|
311
|
+
offset: 0,
|
|
312
|
+
rows: [],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
197
315
|
}
|
|
198
316
|
|
|
199
317
|
async addNote(
|
|
@@ -256,7 +374,7 @@ export class StaticCourseDB implements CourseDBInterface {
|
|
|
256
374
|
}
|
|
257
375
|
|
|
258
376
|
// Attachment helper methods (internal use, not part of interface)
|
|
259
|
-
|
|
377
|
+
|
|
260
378
|
/**
|
|
261
379
|
* Get attachment URL for a document and attachment name
|
|
262
380
|
* Internal helper method for static attachment serving
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Test suite for cross-platform data directory utilities
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { getAppDataDirectory, getDbPath, ensureAppDataDirectory } from './dataDirectory';
|
|
6
|
+
|
|
7
|
+
describe('dataDirectory utilities', () => {
|
|
8
|
+
const originalHomedir = os.homedir;
|
|
9
|
+
const testHome = '/test/home';
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Mock os.homedir for consistent testing
|
|
13
|
+
(os as any).homedir = () => testHome;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
// Restore original function
|
|
18
|
+
os.homedir = originalHomedir;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('getAppDataDirectory', () => {
|
|
22
|
+
it('should return correct path using home directory', () => {
|
|
23
|
+
const result = getAppDataDirectory();
|
|
24
|
+
expect(result).toBe(path.join(testHome, '.tuilder'));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getDbPath', () => {
|
|
29
|
+
it('should return correct database path', () => {
|
|
30
|
+
const dbName = 'userdb-testuser';
|
|
31
|
+
const result = getDbPath(dbName);
|
|
32
|
+
expect(result).toBe(path.join(testHome, '.tuilder', dbName));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle special characters in username', () => {
|
|
36
|
+
const dbName = 'userdb-test@user.com';
|
|
37
|
+
const result = getDbPath(dbName);
|
|
38
|
+
expect(result).toBe(path.join(testHome, '.tuilder', dbName));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('ensureAppDataDirectory', () => {
|
|
43
|
+
it('should handle path creation gracefully', async () => {
|
|
44
|
+
// Note: This test doesn't actually create directories since we're testing logic
|
|
45
|
+
// In a real test environment, you'd want to use a temp directory
|
|
46
|
+
expect(typeof ensureAppDataDirectory).toBe('function');
|
|
47
|
+
|
|
48
|
+
// Test the function exists and returns a promise
|
|
49
|
+
const result = ensureAppDataDirectory();
|
|
50
|
+
expect(result).toBeInstanceOf(Promise);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Cross-platform data directory utilities for PouchDB
|
|
2
|
+
// Provides OS-appropriate application data directories
|
|
3
|
+
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import { logger } from './tuiLogger';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the application data directory for the current platform
|
|
11
|
+
* Uses ~/.tuilder as requested by user for simplicity
|
|
12
|
+
*/
|
|
13
|
+
export function getAppDataDirectory(): string {
|
|
14
|
+
return path.join(os.homedir(), '.tuilder');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure the application data directory exists
|
|
19
|
+
* Creates directory recursively if it doesn't exist
|
|
20
|
+
*/
|
|
21
|
+
export async function ensureAppDataDirectory(): Promise<string> {
|
|
22
|
+
const appDataDir = getAppDataDirectory();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await fs.promises.mkdir(appDataDir, { recursive: true });
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
if (err.code !== 'EEXIST') {
|
|
28
|
+
throw new Error(`Failed to create app data directory ${appDataDir}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return appDataDir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the full path for a PouchDB database file
|
|
37
|
+
* @param dbName - The database name (e.g., 'userdb-Colin')
|
|
38
|
+
*/
|
|
39
|
+
export function getDbPath(dbName: string): string {
|
|
40
|
+
return path.join(getAppDataDirectory(), dbName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize data directory for PouchDB usage
|
|
45
|
+
* Should be called once at application startup
|
|
46
|
+
*/
|
|
47
|
+
export async function initializeDataDirectory(): Promise<void> {
|
|
48
|
+
await ensureAppDataDirectory();
|
|
49
|
+
|
|
50
|
+
// Log initialization
|
|
51
|
+
logger.info(`PouchDB data directory initialized: ${getAppDataDirectory()}`);
|
|
52
|
+
}
|