@vue-skuilder/db 0.1.7 → 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 +131 -118
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +128 -115
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BbW9EnZK.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
- package/dist/{dataLayerProvider-6stCgDME.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 +1365 -1252
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +1359 -1246
- 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 +253 -843
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +250 -842
- 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 +10 -55
- package/dist/index.d.ts +10 -55
- package/dist/index.js +343 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +340 -167
- package/dist/index.mjs.map +1 -1
- package/dist/{types-BvzcRAys.d.ts → types-BefDGkKa.d.ts} +1 -1
- package/dist/{types-CQQ80R5N.d.mts → types-DC-ckZug.d.mts} +1 -1
- package/dist/{types-legacy-CtrmkOLu.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
- package/dist/{types-legacy-CtrmkOLu.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
- package/dist/{userDB-DUY63VMN.d.ts → userDB-C33Hzjgn.d.mts} +10 -3
- package/dist/{userDB-7fM4tpgr.d.mts → userDB-DusL7OXe.d.ts} +10 -3
- 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/impl/common/BaseUserDB.ts +33 -22
- package/src/impl/common/SyncStrategy.ts +7 -0
- package/src/impl/common/index.ts +0 -1
- package/src/impl/common/userDBHelpers.ts +4 -4
- package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
- package/src/impl/couch/courseAPI.ts +7 -6
- 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/migrator/FileSystemAdapter.ts +20 -0
- package/src/util/migrator/StaticToCouchDBMigrator.ts +6 -0
- package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
|
@@ -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
|
|
@@ -26,11 +26,31 @@ export interface FileSystemAdapter {
|
|
|
26
26
|
*/
|
|
27
27
|
stat(filePath: string): Promise<FileStats>;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Write text data to a file
|
|
31
|
+
*/
|
|
32
|
+
writeFile(filePath: string, data: string | Buffer): Promise<void>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Write JSON data to a file with formatting
|
|
36
|
+
*/
|
|
37
|
+
writeJson(filePath: string, data: any, options?: { spaces?: number }): Promise<void>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure a directory exists, creating it and parent directories if needed
|
|
41
|
+
*/
|
|
42
|
+
ensureDir(dirPath: string): Promise<void>;
|
|
43
|
+
|
|
29
44
|
/**
|
|
30
45
|
* Join path segments into a complete path
|
|
31
46
|
*/
|
|
32
47
|
joinPath(...segments: string[]): string;
|
|
33
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Get the directory name of a path
|
|
51
|
+
*/
|
|
52
|
+
dirname(filePath: string): string;
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* Check if a path is absolute
|
|
36
56
|
*/
|
|
@@ -425,6 +425,8 @@ export class StaticToCouchDBMigrator {
|
|
|
425
425
|
const cleanDoc = { ...doc };
|
|
426
426
|
// Remove _rev if present (CouchDB will assign new revision)
|
|
427
427
|
delete cleanDoc._rev;
|
|
428
|
+
// Remove _attachments - these are uploaded separately in Phase 5
|
|
429
|
+
delete cleanDoc._attachments;
|
|
428
430
|
|
|
429
431
|
return cleanDoc;
|
|
430
432
|
});
|
|
@@ -575,10 +577,14 @@ export class StaticToCouchDBMigrator {
|
|
|
575
577
|
}
|
|
576
578
|
}
|
|
577
579
|
|
|
580
|
+
// Get current document revision (needed for putAttachment)
|
|
581
|
+
const doc = await db.get(docId);
|
|
582
|
+
|
|
578
583
|
// Upload to CouchDB
|
|
579
584
|
await db.putAttachment(
|
|
580
585
|
docId,
|
|
581
586
|
attachmentName,
|
|
587
|
+
doc._rev,
|
|
582
588
|
attachmentData as any, // PouchDB accepts both ArrayBuffer and Buffer
|
|
583
589
|
attachmentMeta.content_type
|
|
584
590
|
);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
StaticCourseManifest,
|
|
15
15
|
AttachmentData,
|
|
16
16
|
} from './types';
|
|
17
|
+
import { FileSystemAdapter } from '../migrator/FileSystemAdapter';
|
|
17
18
|
|
|
18
19
|
export class CouchDBToStaticPacker {
|
|
19
20
|
private config: PackerConfig;
|
|
@@ -86,6 +87,94 @@ export class CouchDBToStaticPacker {
|
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Pack a CouchDB course database and write the static files to disk
|
|
92
|
+
*/
|
|
93
|
+
async packCourseToFiles(
|
|
94
|
+
sourceDB: PouchDB.Database,
|
|
95
|
+
courseId: string,
|
|
96
|
+
outputDir: string,
|
|
97
|
+
fsAdapter: FileSystemAdapter
|
|
98
|
+
): Promise<{
|
|
99
|
+
manifest: StaticCourseManifest;
|
|
100
|
+
filesWritten: number;
|
|
101
|
+
attachmentsFound: number;
|
|
102
|
+
}> {
|
|
103
|
+
logger.info(`Packing course ${courseId} to files in ${outputDir}`);
|
|
104
|
+
|
|
105
|
+
// First, pack the course data
|
|
106
|
+
const packedData = await this.packCourse(sourceDB, courseId);
|
|
107
|
+
|
|
108
|
+
// Write the files using the FileSystemAdapter
|
|
109
|
+
const filesWritten = await this.writePackedDataToFiles(packedData, outputDir, fsAdapter);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
manifest: packedData.manifest,
|
|
113
|
+
filesWritten,
|
|
114
|
+
attachmentsFound: packedData.attachments ? packedData.attachments.size : 0,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Write packed course data to files using FileSystemAdapter
|
|
120
|
+
*/
|
|
121
|
+
private async writePackedDataToFiles(
|
|
122
|
+
packedData: PackedCourseData,
|
|
123
|
+
outputDir: string,
|
|
124
|
+
fsAdapter: FileSystemAdapter
|
|
125
|
+
): Promise<number> {
|
|
126
|
+
let totalFiles = 0;
|
|
127
|
+
|
|
128
|
+
// Ensure output directory exists
|
|
129
|
+
await fsAdapter.ensureDir(outputDir);
|
|
130
|
+
|
|
131
|
+
// Write manifest
|
|
132
|
+
const manifestPath = fsAdapter.joinPath(outputDir, 'manifest.json');
|
|
133
|
+
await fsAdapter.writeJson(manifestPath, packedData.manifest, { spaces: 2 });
|
|
134
|
+
totalFiles++;
|
|
135
|
+
logger.info(`Wrote manifest: ${manifestPath}`);
|
|
136
|
+
|
|
137
|
+
// Create subdirectories
|
|
138
|
+
const chunksDir = fsAdapter.joinPath(outputDir, 'chunks');
|
|
139
|
+
const indicesDir = fsAdapter.joinPath(outputDir, 'indices');
|
|
140
|
+
await fsAdapter.ensureDir(chunksDir);
|
|
141
|
+
await fsAdapter.ensureDir(indicesDir);
|
|
142
|
+
|
|
143
|
+
// Write chunks
|
|
144
|
+
for (const [chunkId, chunkData] of packedData.chunks) {
|
|
145
|
+
const chunkPath = fsAdapter.joinPath(chunksDir, `${chunkId}.json`);
|
|
146
|
+
await fsAdapter.writeJson(chunkPath, chunkData);
|
|
147
|
+
totalFiles++;
|
|
148
|
+
}
|
|
149
|
+
logger.info(`Wrote ${packedData.chunks.size} chunk files`);
|
|
150
|
+
|
|
151
|
+
// Write indices
|
|
152
|
+
for (const [indexName, indexData] of packedData.indices) {
|
|
153
|
+
const indexPath = fsAdapter.joinPath(indicesDir, `${indexName}.json`);
|
|
154
|
+
await fsAdapter.writeJson(indexPath, indexData, { spaces: 2 });
|
|
155
|
+
totalFiles++;
|
|
156
|
+
}
|
|
157
|
+
logger.info(`Wrote ${packedData.indices.size} index files`);
|
|
158
|
+
|
|
159
|
+
// Write attachments
|
|
160
|
+
if (packedData.attachments && packedData.attachments.size > 0) {
|
|
161
|
+
for (const [attachmentPath, attachmentData] of packedData.attachments) {
|
|
162
|
+
const fullAttachmentPath = fsAdapter.joinPath(outputDir, attachmentPath);
|
|
163
|
+
|
|
164
|
+
// Ensure attachment directory exists
|
|
165
|
+
const attachmentDir = fsAdapter.dirname(fullAttachmentPath);
|
|
166
|
+
await fsAdapter.ensureDir(attachmentDir);
|
|
167
|
+
|
|
168
|
+
// Write binary file
|
|
169
|
+
await fsAdapter.writeFile(fullAttachmentPath, attachmentData.buffer);
|
|
170
|
+
totalFiles++;
|
|
171
|
+
}
|
|
172
|
+
logger.info(`Wrote ${packedData.attachments.size} attachment files`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return totalFiles;
|
|
176
|
+
}
|
|
177
|
+
|
|
89
178
|
private async extractCourseConfig(db: PouchDB.Database): Promise<CourseConfig> {
|
|
90
179
|
try {
|
|
91
180
|
return await db.get<CourseConfig>('CourseConfig');
|
|
@@ -322,11 +411,12 @@ export class CouchDBToStaticPacker {
|
|
|
322
411
|
|
|
323
412
|
try {
|
|
324
413
|
const designDocId = designDoc._id; // e.g., "_design/elo"
|
|
325
|
-
const
|
|
414
|
+
const designDocName = designDocId.replace('_design/', ''); // Extract just "elo"
|
|
415
|
+
const viewPath = `${designDocName}/${viewName}`;
|
|
326
416
|
|
|
327
417
|
logger.info(`Querying CouchDB view: ${viewPath}`);
|
|
328
418
|
|
|
329
|
-
// Query the view directly from CouchDB
|
|
419
|
+
// Query the view directly from CouchDB using PouchDB format: "designDocName/viewName"
|
|
330
420
|
const viewResults = await this.sourceDB.query(viewPath, {
|
|
331
421
|
include_docs: false,
|
|
332
422
|
});
|