@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.
Files changed (70) 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 +825 -762
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +812 -750
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BZmLyBVw.d.mts → dataLayerProvider-BInqI_RF.d.mts} +1 -1
  10. package/dist/{dataLayerProvider-BuntXkCs.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 +2261 -2081
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2274 -2095
  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 +524 -1064
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +515 -1058
  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 +200 -9
  26. package/dist/index.d.ts +200 -9
  27. package/dist/index.js +4123 -2820
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +4119 -2830
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-D6SnlHPm.d.ts → types-BefDGkKa.d.ts} +1 -1
  32. package/dist/{types-DPRvCrIk.d.mts → types-DC-ckZug.d.mts} +1 -1
  33. package/dist/{types-legacy-WPe8CtO-.d.mts → types-legacy-Birv-Jx6.d.mts} +2 -2
  34. package/dist/{types-legacy-WPe8CtO-.d.ts → types-legacy-Birv-Jx6.d.ts} +2 -2
  35. package/dist/{userDB-D9EuWTp1.d.ts → userDB-C33Hzjgn.d.mts} +11 -4
  36. package/dist/{userDB-31gsvxyd.d.mts → userDB-DusL7OXe.d.ts} +11 -4
  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/factory.ts +25 -0
  48. package/src/impl/common/BaseUserDB.ts +62 -28
  49. package/src/impl/common/SyncStrategy.ts +7 -0
  50. package/src/impl/common/index.ts +0 -1
  51. package/src/impl/common/userDBHelpers.ts +15 -5
  52. package/src/impl/couch/CouchDBSyncStrategy.ts +10 -0
  53. package/src/impl/couch/courseAPI.ts +7 -6
  54. package/src/impl/couch/courseLookupDB.ts +24 -0
  55. package/src/impl/couch/index.ts +10 -5
  56. package/src/impl/couch/updateQueue.ts +12 -8
  57. package/src/impl/couch/user-course-relDB.ts +17 -27
  58. package/src/impl/static/NoOpSyncStrategy.ts +5 -0
  59. package/src/impl/static/StaticDataUnpacker.ts +18 -36
  60. package/src/impl/static/courseDB.ts +135 -17
  61. package/src/util/dataDirectory.test.ts +53 -0
  62. package/src/util/dataDirectory.ts +52 -0
  63. package/src/util/index.ts +3 -0
  64. package/src/util/migrator/FileSystemAdapter.ts +79 -0
  65. package/src/util/migrator/StaticToCouchDBMigrator.ts +713 -0
  66. package/src/util/migrator/index.ts +18 -0
  67. package/src/util/migrator/types.ts +84 -0
  68. package/src/util/migrator/validation.ts +517 -0
  69. package/src/util/packer/CouchDBToStaticPacker.ts +92 -2
  70. package/src/util/tuiLogger.ts +139 -0
@@ -0,0 +1,18 @@
1
+ // packages/db/src/util/migrator/index.ts
2
+
3
+ export { StaticToCouchDBMigrator } from './StaticToCouchDBMigrator';
4
+ export { validateStaticCourse, validateMigration } from './validation';
5
+ export type { FileSystemAdapter, FileStats } from './FileSystemAdapter';
6
+ export { FileSystemError } from './FileSystemAdapter';
7
+ export type {
8
+ MigrationOptions,
9
+ MigrationResult,
10
+ ValidationResult,
11
+ ValidationIssue,
12
+ DocumentCounts,
13
+ RestoreProgress,
14
+ StaticCourseValidation,
15
+ AggregatedDocument,
16
+ AttachmentUploadResult,
17
+ DEFAULT_MIGRATION_OPTIONS
18
+ } from './types';
@@ -0,0 +1,84 @@
1
+ // packages/db/src/util/migrator/types.ts
2
+
3
+ export interface MigrationOptions {
4
+ chunkBatchSize: number;
5
+ validateRoundTrip: boolean;
6
+ cleanupOnFailure: boolean;
7
+ timeout: number; // milliseconds
8
+ }
9
+
10
+ export interface MigrationResult {
11
+ success: boolean;
12
+ documentsRestored: number;
13
+ attachmentsRestored: number;
14
+ designDocsRestored: number;
15
+ courseConfigRestored: number;
16
+ errors: string[];
17
+ warnings: string[];
18
+ migrationTime: number;
19
+ tempDatabaseName?: string;
20
+ }
21
+
22
+ export interface ValidationResult {
23
+ valid: boolean;
24
+ documentCountMatch: boolean;
25
+ attachmentIntegrity: boolean;
26
+ viewFunctionality: boolean;
27
+ issues: ValidationIssue[];
28
+ }
29
+
30
+ export interface ValidationIssue {
31
+ type: 'error' | 'warning';
32
+ category: 'documents' | 'attachments' | 'views' | 'metadata' | 'course_config';
33
+ message: string;
34
+ details?: any;
35
+ }
36
+
37
+ export interface DocumentCounts {
38
+ [docType: string]: number;
39
+ }
40
+
41
+ export interface RestoreProgress {
42
+ phase: 'manifest' | 'design_docs' | 'course_config' | 'documents' | 'attachments' | 'validation';
43
+ current: number;
44
+ total: number;
45
+ message: string;
46
+ }
47
+
48
+ export interface StaticCourseValidation {
49
+ valid: boolean;
50
+ manifestExists: boolean;
51
+ chunksExist: boolean;
52
+ attachmentsExist: boolean;
53
+ errors: string[];
54
+ warnings: string[];
55
+ courseId?: string;
56
+ courseName?: string;
57
+ }
58
+
59
+ export interface AggregatedDocument {
60
+ _id: string;
61
+ _attachments?: Record<string, any>;
62
+ docType: string;
63
+ [key: string]: any;
64
+ }
65
+
66
+ export interface RestoreResults {
67
+ restored: number;
68
+ errors: string[];
69
+ warnings: string[];
70
+ }
71
+
72
+ export interface AttachmentUploadResult {
73
+ success: boolean;
74
+ attachmentName: string;
75
+ docId: string;
76
+ error?: string;
77
+ }
78
+
79
+ export const DEFAULT_MIGRATION_OPTIONS: MigrationOptions = {
80
+ chunkBatchSize: 100,
81
+ validateRoundTrip: false,
82
+ cleanupOnFailure: true,
83
+ timeout: 300000, // 5 minutes
84
+ };
@@ -0,0 +1,517 @@
1
+ // packages/db/src/util/migrator/validation.ts
2
+
3
+ import { logger } from '../logger';
4
+ import { StaticCourseValidation, ValidationResult, DocumentCounts, ValidationIssue } from './types';
5
+ import { StaticCourseManifest } from '../packer/types';
6
+ import { FileSystemAdapter, FileSystemError } from './FileSystemAdapter';
7
+
8
+ // Check if we're in Node.js environment and fs is available
9
+ let nodeFS: any = null;
10
+ try {
11
+ if (typeof window === 'undefined' && typeof process !== 'undefined' && process.versions?.node) {
12
+ nodeFS = eval('require')('fs');
13
+ nodeFS.promises = nodeFS.promises || eval('require')('fs').promises;
14
+ }
15
+ } catch {
16
+ // fs not available
17
+ }
18
+
19
+ /**
20
+ * Validate that a static course directory contains all required files
21
+ */
22
+ export async function validateStaticCourse(
23
+ staticPath: string,
24
+ fs?: FileSystemAdapter
25
+ ): Promise<StaticCourseValidation> {
26
+ const validation: StaticCourseValidation = {
27
+ valid: true,
28
+ manifestExists: false,
29
+ chunksExist: false,
30
+ attachmentsExist: false,
31
+ errors: [],
32
+ warnings: [],
33
+ };
34
+
35
+ try {
36
+ // Check if path exists and is directory
37
+ if (fs) {
38
+ // Use injected file system adapter (preferred)
39
+ const stats = await fs.stat(staticPath);
40
+ if (!stats.isDirectory()) {
41
+ validation.errors.push(`Path is not a directory: ${staticPath}`);
42
+ validation.valid = false;
43
+ return validation;
44
+ }
45
+ } else if (!nodeFS) {
46
+ // Fallback validation failed
47
+ validation.errors.push('File system access not available - validation skipped');
48
+ validation.valid = false;
49
+ return validation;
50
+ } else {
51
+ // Legacy fallback
52
+ const stats = await nodeFS.promises.stat(staticPath);
53
+ if (!stats.isDirectory()) {
54
+ validation.errors.push(`Path is not a directory: ${staticPath}`);
55
+ validation.valid = false;
56
+ return validation;
57
+ }
58
+ }
59
+
60
+ // Check for manifest.json
61
+ let manifestPath: string = `${staticPath}/manifest.json`;
62
+ try {
63
+ if (fs) {
64
+ // Use injected file system adapter (preferred)
65
+ manifestPath = fs.joinPath(staticPath, 'manifest.json');
66
+ if (await fs.exists(manifestPath)) {
67
+ validation.manifestExists = true;
68
+
69
+ // Parse manifest to get course info
70
+ const manifestContent = await fs.readFile(manifestPath);
71
+ const manifest: StaticCourseManifest = JSON.parse(manifestContent);
72
+ validation.courseId = manifest.courseId;
73
+ validation.courseName = manifest.courseName;
74
+
75
+ // Validate manifest structure
76
+ if (
77
+ !manifest.version ||
78
+ !manifest.courseId ||
79
+ !manifest.chunks ||
80
+ !Array.isArray(manifest.chunks)
81
+ ) {
82
+ validation.errors.push('Invalid manifest structure');
83
+ validation.valid = false;
84
+ }
85
+ } else {
86
+ validation.errors.push(`Manifest not found: ${manifestPath}`);
87
+ validation.valid = false;
88
+ }
89
+ } else {
90
+ // Legacy fallback
91
+ manifestPath = `${staticPath}/manifest.json`;
92
+ await nodeFS.promises.access(manifestPath);
93
+ validation.manifestExists = true;
94
+
95
+ // Parse manifest to get course info
96
+ const manifestContent = await nodeFS.promises.readFile(manifestPath, 'utf8');
97
+ const manifest: StaticCourseManifest = JSON.parse(manifestContent);
98
+ validation.courseId = manifest.courseId;
99
+ validation.courseName = manifest.courseName;
100
+
101
+ // Validate manifest structure
102
+ if (
103
+ !manifest.version ||
104
+ !manifest.courseId ||
105
+ !manifest.chunks ||
106
+ !Array.isArray(manifest.chunks)
107
+ ) {
108
+ validation.errors.push('Invalid manifest structure');
109
+ validation.valid = false;
110
+ }
111
+ }
112
+ } catch (error) {
113
+ const errorMessage =
114
+ error instanceof FileSystemError
115
+ ? error.message
116
+ : `Manifest not found or invalid: ${manifestPath}`;
117
+ validation.errors.push(errorMessage);
118
+ validation.valid = false;
119
+ }
120
+
121
+ // Check for chunks directory
122
+ let chunksPath: string = `${staticPath}/chunks`;
123
+ try {
124
+ if (fs) {
125
+ // Use injected file system adapter (preferred)
126
+ chunksPath = fs.joinPath(staticPath, 'chunks');
127
+ if (await fs.exists(chunksPath)) {
128
+ const chunksStats = await fs.stat(chunksPath);
129
+ if (chunksStats.isDirectory()) {
130
+ validation.chunksExist = true;
131
+ } else {
132
+ validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
133
+ validation.valid = false;
134
+ }
135
+ } else {
136
+ validation.errors.push(`Chunks directory not found: ${chunksPath}`);
137
+ validation.valid = false;
138
+ }
139
+ } else {
140
+ // Legacy fallback
141
+ chunksPath = `${staticPath}/chunks`;
142
+ const chunksStats = await nodeFS.promises.stat(chunksPath);
143
+ if (chunksStats.isDirectory()) {
144
+ validation.chunksExist = true;
145
+ } else {
146
+ validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
147
+ validation.valid = false;
148
+ }
149
+ }
150
+ } catch (error) {
151
+ const errorMessage =
152
+ error instanceof FileSystemError
153
+ ? error.message
154
+ : `Chunks directory not found: ${chunksPath}`;
155
+ validation.errors.push(errorMessage);
156
+ validation.valid = false;
157
+ }
158
+
159
+ // Check for attachments directory (optional - course might not have attachments)
160
+ let attachmentsPath: string;
161
+ try {
162
+ if (fs) {
163
+ // Use injected file system adapter (preferred)
164
+ attachmentsPath = fs.joinPath(staticPath, 'attachments');
165
+ if (await fs.exists(attachmentsPath)) {
166
+ const attachmentsStats = await fs.stat(attachmentsPath);
167
+ if (attachmentsStats.isDirectory()) {
168
+ validation.attachmentsExist = true;
169
+ }
170
+ } else {
171
+ // Attachments directory is optional
172
+ validation.warnings.push(
173
+ `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
174
+ );
175
+ }
176
+ } else {
177
+ // Legacy fallback
178
+ attachmentsPath = `${staticPath}/attachments`;
179
+ const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
180
+ if (attachmentsStats.isDirectory()) {
181
+ validation.attachmentsExist = true;
182
+ }
183
+ }
184
+ } catch (error) {
185
+ // Attachments directory is optional
186
+ attachmentsPath = attachmentsPath! || `${staticPath}/attachments`;
187
+ const warningMessage =
188
+ error instanceof FileSystemError
189
+ ? error.message
190
+ : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
191
+ validation.warnings.push(warningMessage);
192
+ }
193
+ } catch (error) {
194
+ validation.errors.push(
195
+ `Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
196
+ );
197
+ validation.valid = false;
198
+ }
199
+
200
+ return validation;
201
+ }
202
+
203
+ /**
204
+ * Validate the result of a migration by checking document counts and integrity
205
+ */
206
+ export async function validateMigration(
207
+ targetDB: PouchDB.Database,
208
+ expectedCounts: DocumentCounts,
209
+ manifest: StaticCourseManifest
210
+ ): Promise<ValidationResult> {
211
+ const validation: ValidationResult = {
212
+ valid: true,
213
+ documentCountMatch: false,
214
+ attachmentIntegrity: false,
215
+ viewFunctionality: false,
216
+ issues: [],
217
+ };
218
+
219
+ try {
220
+ logger.info('Starting migration validation...');
221
+
222
+ // 1. Validate document counts
223
+ const actualCounts = await getActualDocumentCounts(targetDB);
224
+ validation.documentCountMatch = compareDocumentCounts(
225
+ expectedCounts,
226
+ actualCounts,
227
+ validation.issues
228
+ );
229
+
230
+ // 2. Validate CourseConfig document
231
+ await validateCourseConfig(targetDB, manifest, validation.issues);
232
+
233
+ // 3. Validate design documents and views
234
+ validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
235
+
236
+ // 4. Validate attachment integrity (sample check)
237
+ validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
238
+
239
+ // Overall validation result
240
+ validation.valid =
241
+ validation.documentCountMatch &&
242
+ validation.viewFunctionality &&
243
+ validation.attachmentIntegrity;
244
+
245
+ logger.info(`Migration validation completed. Valid: ${validation.valid}`);
246
+ if (validation.issues.length > 0) {
247
+ logger.info(`Validation issues: ${validation.issues.length}`);
248
+ validation.issues.forEach((issue) => {
249
+ if (issue.type === 'error') {
250
+ logger.error(`${issue.category}: ${issue.message}`);
251
+ } else {
252
+ logger.warn(`${issue.category}: ${issue.message}`);
253
+ }
254
+ });
255
+ }
256
+ } catch (error) {
257
+ validation.valid = false;
258
+ validation.issues.push({
259
+ type: 'error',
260
+ category: 'metadata',
261
+ message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`,
262
+ });
263
+ }
264
+
265
+ return validation;
266
+ }
267
+
268
+ /**
269
+ * Get actual document counts by type from the database
270
+ */
271
+ async function getActualDocumentCounts(db: PouchDB.Database): Promise<DocumentCounts> {
272
+ const counts: DocumentCounts = {};
273
+
274
+ try {
275
+ const allDocs = await db.allDocs({ include_docs: true });
276
+
277
+ for (const row of allDocs.rows) {
278
+ if (row.id.startsWith('_design/')) {
279
+ // Count design documents separately
280
+ counts['_design'] = (counts['_design'] || 0) + 1;
281
+ continue;
282
+ }
283
+
284
+ const doc = row.doc as any;
285
+ if (doc && doc.docType) {
286
+ counts[doc.docType] = (counts[doc.docType] || 0) + 1;
287
+ } else {
288
+ // Documents without docType
289
+ counts['unknown'] = (counts['unknown'] || 0) + 1;
290
+ }
291
+ }
292
+ } catch (error) {
293
+ logger.error('Failed to get actual document counts:', error);
294
+ }
295
+
296
+ return counts;
297
+ }
298
+
299
+ /**
300
+ * Compare expected vs actual document counts
301
+ */
302
+ function compareDocumentCounts(
303
+ expected: DocumentCounts,
304
+ actual: DocumentCounts,
305
+ issues: ValidationIssue[]
306
+ ): boolean {
307
+ let countsMatch = true;
308
+
309
+ // Check each expected document type
310
+ for (const [docType, expectedCount] of Object.entries(expected)) {
311
+ const actualCount = actual[docType] || 0;
312
+
313
+ if (actualCount !== expectedCount) {
314
+ countsMatch = false;
315
+ issues.push({
316
+ type: 'error',
317
+ category: 'documents',
318
+ message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`,
319
+ });
320
+ }
321
+ }
322
+
323
+ // Check for unexpected document types
324
+ for (const [docType, actualCount] of Object.entries(actual)) {
325
+ if (!expected[docType] && docType !== '_design') {
326
+ issues.push({
327
+ type: 'warning',
328
+ category: 'documents',
329
+ message: `Unexpected document type found: ${docType} (${actualCount} documents)`,
330
+ });
331
+ }
332
+ }
333
+
334
+ return countsMatch;
335
+ }
336
+
337
+ /**
338
+ * Validate that CourseConfig document exists and is properly structured
339
+ */
340
+ async function validateCourseConfig(
341
+ db: PouchDB.Database,
342
+ manifest: StaticCourseManifest,
343
+ issues: ValidationIssue[]
344
+ ): Promise<void> {
345
+ try {
346
+ // Check if CourseConfig document exists
347
+ const courseConfig = await db.get('CourseConfig');
348
+ if (!courseConfig) {
349
+ issues.push({
350
+ type: 'error',
351
+ category: 'course_config',
352
+ message: 'CourseConfig document not found after migration',
353
+ });
354
+ return;
355
+ }
356
+
357
+ // Verify courseID field is present
358
+ if (!(courseConfig as any).courseID) {
359
+ issues.push({
360
+ type: 'warning',
361
+ category: 'course_config',
362
+ message: 'CourseConfig document missing courseID field',
363
+ });
364
+ }
365
+
366
+ // Verify courseID matches manifest
367
+ if ((courseConfig as any).courseID !== manifest.courseId) {
368
+ issues.push({
369
+ type: 'warning',
370
+ category: 'course_config',
371
+ message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${(courseConfig as any).courseID}`,
372
+ });
373
+ }
374
+
375
+ logger.debug('CourseConfig document validation passed');
376
+ } catch (error) {
377
+ if ((error as any).status === 404) {
378
+ issues.push({
379
+ type: 'error',
380
+ category: 'course_config',
381
+ message: 'CourseConfig document not found in database',
382
+ });
383
+ } else {
384
+ issues.push({
385
+ type: 'error',
386
+ category: 'course_config',
387
+ message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`,
388
+ });
389
+ }
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Validate that design documents and views are working correctly
395
+ */
396
+ async function validateViews(
397
+ db: PouchDB.Database,
398
+ manifest: StaticCourseManifest,
399
+ issues: ValidationIssue[]
400
+ ): Promise<boolean> {
401
+ let viewsValid = true;
402
+
403
+ try {
404
+ // Check that design documents exist
405
+ for (const designDoc of manifest.designDocs) {
406
+ try {
407
+ const doc = await db.get(designDoc._id);
408
+ if (!doc) {
409
+ viewsValid = false;
410
+ issues.push({
411
+ type: 'error',
412
+ category: 'views',
413
+ message: `Design document not found: ${designDoc._id}`,
414
+ });
415
+ continue;
416
+ }
417
+
418
+ // Test each view in the design document
419
+ for (const viewName of Object.keys(designDoc.views)) {
420
+ try {
421
+ const viewPath = `${designDoc._id}/${viewName}`;
422
+ await db.query(viewPath, { limit: 1 });
423
+ // If we get here, the view is accessible (even if it returns no results)
424
+ } catch (viewError) {
425
+ viewsValid = false;
426
+ issues.push({
427
+ type: 'error',
428
+ category: 'views',
429
+ message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`,
430
+ });
431
+ }
432
+ }
433
+ } catch (error) {
434
+ viewsValid = false;
435
+ issues.push({
436
+ type: 'error',
437
+ category: 'views',
438
+ message: `Failed to validate design document ${designDoc._id}: ${error}`,
439
+ });
440
+ }
441
+ }
442
+ } catch (error) {
443
+ viewsValid = false;
444
+ issues.push({
445
+ type: 'error',
446
+ category: 'views',
447
+ message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`,
448
+ });
449
+ }
450
+
451
+ return viewsValid;
452
+ }
453
+
454
+ /**
455
+ * Validate attachment integrity by checking a sample of attachments
456
+ */
457
+ async function validateAttachmentIntegrity(
458
+ db: PouchDB.Database,
459
+ issues: ValidationIssue[]
460
+ ): Promise<boolean> {
461
+ let attachmentsValid = true;
462
+
463
+ try {
464
+ // Get documents with attachments (sample check)
465
+ const allDocs = await db.allDocs({
466
+ include_docs: true,
467
+ limit: 10, // Sample first 10 documents for performance
468
+ });
469
+
470
+ let attachmentCount = 0;
471
+ let validAttachments = 0;
472
+
473
+ for (const row of allDocs.rows) {
474
+ const doc = row.doc as any;
475
+ if (doc && doc._attachments) {
476
+ for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
477
+ attachmentCount++;
478
+
479
+ try {
480
+ // Try to access the attachment
481
+ const attachment = await db.getAttachment(doc._id, attachmentName);
482
+ if (attachment) {
483
+ validAttachments++;
484
+ }
485
+ } catch (attachmentError) {
486
+ attachmentsValid = false;
487
+ issues.push({
488
+ type: 'error',
489
+ category: 'attachments',
490
+ message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`,
491
+ });
492
+ }
493
+ }
494
+ }
495
+ }
496
+
497
+ if (attachmentCount === 0) {
498
+ // No attachments found - this is OK
499
+ issues.push({
500
+ type: 'warning',
501
+ category: 'attachments',
502
+ message: 'No attachments found in sampled documents',
503
+ });
504
+ } else {
505
+ logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
506
+ }
507
+ } catch (error) {
508
+ attachmentsValid = false;
509
+ issues.push({
510
+ type: 'error',
511
+ category: 'attachments',
512
+ message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`,
513
+ });
514
+ }
515
+
516
+ return attachmentsValid;
517
+ }