@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
|
@@ -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
|
+
}
|