@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.
Files changed (61) hide show
  1. package/dist/{SyncStrategy-DnJRj-Xp.d.mts → SyncStrategy-CyATpyLQ.d.mts} +6 -0
  2. package/dist/{SyncStrategy-DnJRj-Xp.d.ts → SyncStrategy-CyATpyLQ.d.ts} +6 -0
  3. package/dist/core/index.d.mts +5 -5
  4. package/dist/core/index.d.ts +5 -5
  5. package/dist/core/index.js +131 -118
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +128 -115
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BbW9EnZK.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
  10. package/dist/{dataLayerProvider-6stCgDME.d.ts → dataLayerProvider-DqtNroSh.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.mts +6 -6
  12. package/dist/impl/couch/index.d.ts +6 -6
  13. package/dist/impl/couch/index.js +1365 -1252
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +1359 -1246
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.mts +8 -6
  18. package/dist/impl/static/index.d.ts +8 -6
  19. package/dist/impl/static/index.js +253 -843
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +250 -842
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index-CLL31bEy.d.ts +137 -0
  24. package/dist/index-CUNnL38E.d.mts +137 -0
  25. package/dist/index.d.mts +10 -55
  26. package/dist/index.d.ts +10 -55
  27. package/dist/index.js +343 -170
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +340 -167
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-BvzcRAys.d.ts → types-BefDGkKa.d.ts} +1 -1
  32. package/dist/{types-CQQ80R5N.d.mts → types-DC-ckZug.d.mts} +1 -1
  33. package/dist/{types-legacy-CtrmkOLu.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
  34. package/dist/{types-legacy-CtrmkOLu.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
  35. package/dist/{userDB-DUY63VMN.d.ts → userDB-C33Hzjgn.d.mts} +10 -3
  36. package/dist/{userDB-7fM4tpgr.d.mts → userDB-DusL7OXe.d.ts} +10 -3
  37. package/dist/util/packer/index.d.mts +3 -63
  38. package/dist/util/packer/index.d.ts +3 -63
  39. package/dist/util/packer/index.js +53 -1
  40. package/dist/util/packer/index.js.map +1 -1
  41. package/dist/util/packer/index.mjs +53 -1
  42. package/dist/util/packer/index.mjs.map +1 -1
  43. package/package.json +7 -4
  44. package/src/core/types/types-legacy.ts +13 -1
  45. package/src/core/types/user.ts +9 -2
  46. package/src/core/util/index.ts +5 -4
  47. package/src/impl/common/BaseUserDB.ts +33 -22
  48. package/src/impl/common/SyncStrategy.ts +7 -0
  49. package/src/impl/common/index.ts +0 -1
  50. package/src/impl/common/userDBHelpers.ts +4 -4
  51. package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
  52. package/src/impl/couch/courseAPI.ts +7 -6
  53. package/src/impl/couch/index.ts +10 -5
  54. package/src/impl/couch/updateQueue.ts +12 -8
  55. package/src/impl/couch/user-course-relDB.ts +17 -27
  56. package/src/impl/static/NoOpSyncStrategy.ts +5 -0
  57. package/src/impl/static/StaticDataUnpacker.ts +18 -36
  58. package/src/impl/static/courseDB.ts +135 -17
  59. package/src/util/migrator/FileSystemAdapter.ts +20 -0
  60. package/src/util/migrator/StaticToCouchDBMigrator.ts +6 -0
  61. 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 db: PouchDB.Database;
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(db: PouchDB.Database) {
31
+ constructor(readDB: PouchDB.Database, writeDB?: PouchDB.Database) {
31
32
  super();
32
33
  // PouchDB.debug.enable('*');
33
- this.db = db;
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.db.info().then((i) => {
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>>(id: string): Promise<T> {
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.db.info(); // stall for a round trip
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.db.get<T>(id);
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.db.put<T>(doc);
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
- import { getStartAndEndKeys, REVIEW_PREFIX, REVIEW_TIME_FORMAT } from '.';
9
- import { CourseDB } from './courseDB';
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: User;
15
- private course: CourseDB;
14
+ private user: UserDBInterface;
16
15
  private _courseId: string;
17
16
 
18
- constructor(user: User, courseId: string) {
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
- void this.user.updateCourseSettings(this._courseId, updates);
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
- const keys = getStartAndEndKeys(REVIEW_PREFIX);
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
- return reviews.rows
66
- .filter((r) => {
67
- if (r.id.startsWith(REVIEW_PREFIX)) {
68
- const date = moment.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
69
- if (targetDate.isAfter(date)) {
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
- // Determine document type from ID pattern by checking all DocType enum members
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 !== undefined) {
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
- // Fall back to trying all chunk types with strict verification
186
- // Since card IDs and displayable data IDs can overlap in range, we need to verify actual existence
187
-
188
- // First try DISPLAYABLE_DATA chunks (most likely for documents without prefixes)
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
- // This would need to be pre-computed in the manifest
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: 0, // Would come from manifest
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(_cardId: string): Promise<PouchDB.Query.Response<TagStub>> {
162
- // Would need to query the tag index
163
- return {
164
- total_rows: 0,
165
- offset: 0,
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
- // Would query all tag documents
192
- return {
193
- total_rows: 0,
194
- offset: 0,
195
- rows: [],
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 viewPath = `${designDocId}/${viewName}`;
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
  });