@toiroakr/lines-db 0.7.0 → 0.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/CHANGELOG.md +16 -0
- package/bin/cli.js +91 -34
- package/dist/index.cjs +39 -7
- package/dist/index.d.cts +9 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +118 -55
- package/src/database.ts +71 -11
- package/src/index.ts +1 -0
- package/src/schema-extensions.ts +1 -4
- package/src/type-generator.ts +4 -3
- package/src/types.ts +9 -0
package/src/cli.ts
CHANGED
|
@@ -106,72 +106,135 @@ program
|
|
|
106
106
|
await db.close();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
//
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
// Directory validation: display per-table results
|
|
110
|
+
if (!tableName) {
|
|
111
|
+
const formatter = new ErrorFormatter({ verbose: options.verbose });
|
|
112
|
+
|
|
113
|
+
for (const tableResult of result.tableResults) {
|
|
114
|
+
if (tableResult.valid && tableResult.warnings.length === 0) {
|
|
115
|
+
// Success
|
|
116
|
+
console.log(
|
|
117
|
+
styleText('green', `✓ ${tableResult.tableName} (${tableResult.rowCount} records)`),
|
|
118
|
+
);
|
|
119
|
+
} else if (tableResult.valid && tableResult.warnings.length > 0) {
|
|
120
|
+
// Warnings
|
|
121
|
+
for (const warning of tableResult.warnings) {
|
|
122
|
+
console.warn(styleText('yellow', `⚠ ${warning}`));
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Errors
|
|
126
|
+
const fileErrors = tableResult.errors;
|
|
127
|
+
console.error(formatter.formatErrorHeader(fileErrors.length, fileErrors[0]?.file));
|
|
128
|
+
console.error('');
|
|
129
|
+
|
|
130
|
+
const validationErrors = fileErrors.filter(
|
|
131
|
+
(e) => e.type !== 'foreignKey' || !e.foreignKeyError,
|
|
132
|
+
);
|
|
133
|
+
const foreignKeyErrors = fileErrors.filter(
|
|
134
|
+
(e) => e.type === 'foreignKey' && e.foreignKeyError,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (validationErrors.length > 0) {
|
|
138
|
+
console.error(
|
|
139
|
+
formatter.formatValidationErrors(
|
|
140
|
+
validationErrors.map((e) => ({
|
|
141
|
+
file: e.file,
|
|
142
|
+
rowIndex: e.rowIndex,
|
|
143
|
+
issues: e.issues,
|
|
144
|
+
})),
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const error of foreignKeyErrors) {
|
|
150
|
+
if (error.foreignKeyError) {
|
|
151
|
+
console.error(
|
|
152
|
+
formatter.formatForeignKeyError({
|
|
153
|
+
file: error.file,
|
|
154
|
+
rowIndex: error.rowIndex,
|
|
155
|
+
column: error.foreignKeyError.column,
|
|
156
|
+
value: error.foreignKeyError.value,
|
|
157
|
+
referencedTable: error.foreignKeyError.referencedTable,
|
|
158
|
+
referencedColumn: error.foreignKeyError.referencedColumn,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.error('');
|
|
165
|
+
}
|
|
113
166
|
}
|
|
114
|
-
console.log('');
|
|
115
|
-
}
|
|
116
167
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
168
|
+
if (result.valid) {
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log(styleText('green', '✓ All records are valid'));
|
|
171
|
+
process.exit(0);
|
|
172
|
+
} else {
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
120
175
|
} else {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
fileErrors.push(error);
|
|
128
|
-
errorsByFile.set(error.file, fileErrors);
|
|
176
|
+
// Single file validation: existing behavior
|
|
177
|
+
if (result.warnings.length > 0) {
|
|
178
|
+
for (const warning of result.warnings) {
|
|
179
|
+
console.warn(styleText('yellow', `⚠ ${warning}`));
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
129
182
|
}
|
|
130
183
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
file: e.file,
|
|
149
|
-
rowIndex: e.rowIndex,
|
|
150
|
-
issues: e.issues,
|
|
151
|
-
})),
|
|
184
|
+
if (result.valid) {
|
|
185
|
+
console.log(styleText('green', '✓ All records are valid'));
|
|
186
|
+
process.exit(0);
|
|
187
|
+
} else {
|
|
188
|
+
const formatter = new ErrorFormatter({ verbose: options.verbose });
|
|
189
|
+
|
|
190
|
+
for (const [, fileErrors] of result.errors.reduce((map, error) => {
|
|
191
|
+
const errors = map.get(error.file) || [];
|
|
192
|
+
errors.push(error);
|
|
193
|
+
map.set(error.file, errors);
|
|
194
|
+
return map;
|
|
195
|
+
}, new Map<string, typeof result.errors>())) {
|
|
196
|
+
console.error(formatter.formatErrorHeader(fileErrors.length, fileErrors[0]?.file));
|
|
197
|
+
console.error('');
|
|
198
|
+
|
|
199
|
+
const validationErrors = fileErrors.filter(
|
|
200
|
+
(e) => e.type !== 'foreignKey' || !e.foreignKeyError,
|
|
152
201
|
);
|
|
153
|
-
|
|
154
|
-
|
|
202
|
+
const foreignKeyErrors = fileErrors.filter(
|
|
203
|
+
(e) => e.type === 'foreignKey' && e.foreignKeyError,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (validationErrors.length > 0) {
|
|
207
|
+
console.error(
|
|
208
|
+
formatter.formatValidationErrors(
|
|
209
|
+
validationErrors.map((e) => ({
|
|
210
|
+
file: e.file,
|
|
211
|
+
rowIndex: e.rowIndex,
|
|
212
|
+
issues: e.issues,
|
|
213
|
+
})),
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
155
217
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
218
|
+
for (const error of foreignKeyErrors) {
|
|
219
|
+
if (error.foreignKeyError) {
|
|
220
|
+
console.error(
|
|
221
|
+
formatter.formatForeignKeyError({
|
|
222
|
+
file: error.file,
|
|
223
|
+
rowIndex: error.rowIndex,
|
|
224
|
+
column: error.foreignKeyError.column,
|
|
225
|
+
value: error.foreignKeyError.value,
|
|
226
|
+
referencedTable: error.foreignKeyError.referencedTable,
|
|
227
|
+
referencedColumn: error.foreignKeyError.referencedColumn,
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
168
231
|
}
|
|
232
|
+
|
|
233
|
+
console.error('');
|
|
169
234
|
}
|
|
170
235
|
|
|
171
|
-
|
|
236
|
+
process.exit(1);
|
|
172
237
|
}
|
|
173
|
-
|
|
174
|
-
process.exit(1);
|
|
175
238
|
}
|
|
176
239
|
} catch (error) {
|
|
177
240
|
console.error('Error:', error instanceof Error ? error.message : String(error));
|
package/src/database.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
WhereCondition,
|
|
19
19
|
ValidationResult,
|
|
20
20
|
ValidationErrorDetail,
|
|
21
|
+
TableValidationResult,
|
|
21
22
|
ForeignKeyDefinition,
|
|
22
23
|
} from './types.js';
|
|
23
24
|
import type { BiDirectionalSchema } from './schema.js';
|
|
@@ -58,6 +59,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
58
59
|
}): Promise<ValidationResult> {
|
|
59
60
|
const allErrors: ValidationErrorDetail[] = [];
|
|
60
61
|
const allWarnings: string[] = [];
|
|
62
|
+
const allRowCounts = new Map<string, number>();
|
|
61
63
|
const tableName = options?.tableName;
|
|
62
64
|
const detailedValidate = options?.detailedValidate ?? false;
|
|
63
65
|
const transform = options?.transform;
|
|
@@ -87,7 +89,11 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
87
89
|
if (!attemptedTables.has(tableNameToLoad)) {
|
|
88
90
|
// Only apply transform to the specified table
|
|
89
91
|
const tableTransform = tableNameToLoad === tableName ? transform : undefined;
|
|
90
|
-
const {
|
|
92
|
+
const {
|
|
93
|
+
errors,
|
|
94
|
+
warnings,
|
|
95
|
+
rowCounts: tableRowCounts,
|
|
96
|
+
} = await this.loadTableWithDependencies(
|
|
91
97
|
tableNameToLoad,
|
|
92
98
|
loadedTables,
|
|
93
99
|
loadingTables,
|
|
@@ -97,13 +103,30 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
97
103
|
);
|
|
98
104
|
allErrors.push(...errors);
|
|
99
105
|
allWarnings.push(...warnings);
|
|
106
|
+
for (const [k, v] of tableRowCounts) {
|
|
107
|
+
allRowCounts.set(k, v);
|
|
108
|
+
}
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
// Build per-table results
|
|
113
|
+
const tableResults: TableValidationResult[] = tablesToLoad.map((name) => {
|
|
114
|
+
const tableErrors = allErrors.filter((e) => e.tableName === name);
|
|
115
|
+
const tableWarnings = allWarnings.filter((w) => w.includes(`'${name}'`));
|
|
116
|
+
return {
|
|
117
|
+
tableName: name,
|
|
118
|
+
valid: tableErrors.length === 0,
|
|
119
|
+
rowCount: allRowCounts.get(name) ?? 0,
|
|
120
|
+
errors: tableErrors,
|
|
121
|
+
warnings: tableWarnings,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
103
125
|
return {
|
|
104
126
|
valid: allErrors.length === 0,
|
|
105
127
|
errors: allErrors,
|
|
106
128
|
warnings: allWarnings,
|
|
129
|
+
tableResults,
|
|
107
130
|
};
|
|
108
131
|
}
|
|
109
132
|
|
|
@@ -117,13 +140,18 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
117
140
|
attemptedTables: Set<string>,
|
|
118
141
|
detailedValidate: boolean,
|
|
119
142
|
transform?: (row: JsonObject) => JsonObject,
|
|
120
|
-
): Promise<{
|
|
143
|
+
): Promise<{
|
|
144
|
+
errors: ValidationErrorDetail[];
|
|
145
|
+
warnings: string[];
|
|
146
|
+
rowCounts: Map<string, number>;
|
|
147
|
+
}> {
|
|
121
148
|
const errors: ValidationErrorDetail[] = [];
|
|
122
149
|
const warnings: string[] = [];
|
|
150
|
+
const rowCounts = new Map<string, number>();
|
|
123
151
|
|
|
124
152
|
// Skip if already attempted (loaded or not)
|
|
125
153
|
if (attemptedTables.has(tableName)) {
|
|
126
|
-
return { errors, warnings };
|
|
154
|
+
return { errors, warnings, rowCounts };
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
// Mark as attempted
|
|
@@ -190,6 +218,9 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
190
218
|
);
|
|
191
219
|
errors.push(...depResult.errors);
|
|
192
220
|
warnings.push(...depResult.warnings);
|
|
221
|
+
for (const [k, v] of depResult.rowCounts) {
|
|
222
|
+
rowCounts.set(k, v);
|
|
223
|
+
}
|
|
193
224
|
} else {
|
|
194
225
|
throw new Error(
|
|
195
226
|
`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`,
|
|
@@ -199,14 +230,39 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
199
230
|
}
|
|
200
231
|
}
|
|
201
232
|
|
|
233
|
+
// Determine which FK dependencies failed validation (attempted but not loaded)
|
|
234
|
+
const failedDependencies = new Set<string>();
|
|
235
|
+
if (foreignKeys && foreignKeys.length > 0) {
|
|
236
|
+
for (const fk of foreignKeys) {
|
|
237
|
+
const referencedTable = fk.references.table;
|
|
238
|
+
if (referencedTable === tableName) continue;
|
|
239
|
+
if (attemptedTables.has(referencedTable) && !loadedTables.has(referencedTable)) {
|
|
240
|
+
failedDependencies.add(referencedTable);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (failedDependencies.size > 0) {
|
|
244
|
+
for (const dep of failedDependencies) {
|
|
245
|
+
warnings.push(
|
|
246
|
+
`Skipping foreign key validation for table '${tableName}': referenced table '${dep}' has validation errors`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
202
252
|
// Now load this table
|
|
203
|
-
const {
|
|
253
|
+
const {
|
|
254
|
+
loaded,
|
|
255
|
+
rowCount,
|
|
256
|
+
errors: loadErrors,
|
|
257
|
+
} = await this.loadTable(
|
|
204
258
|
tableName,
|
|
205
259
|
tableConfig,
|
|
206
260
|
detailedValidate,
|
|
207
261
|
transform,
|
|
262
|
+
failedDependencies,
|
|
208
263
|
);
|
|
209
264
|
errors.push(...loadErrors);
|
|
265
|
+
rowCounts.set(tableName, rowCount);
|
|
210
266
|
|
|
211
267
|
if (loaded) {
|
|
212
268
|
loadedTables.add(tableName);
|
|
@@ -220,7 +276,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
220
276
|
loadingTables.delete(tableName);
|
|
221
277
|
}
|
|
222
278
|
|
|
223
|
-
return { errors, warnings };
|
|
279
|
+
return { errors, warnings, rowCounts };
|
|
224
280
|
}
|
|
225
281
|
|
|
226
282
|
/**
|
|
@@ -232,7 +288,8 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
232
288
|
config: TableConfig,
|
|
233
289
|
detailedValidate: boolean,
|
|
234
290
|
transform?: (row: JsonObject) => JsonObject,
|
|
235
|
-
|
|
291
|
+
failedDependencies?: Set<string>,
|
|
292
|
+
): Promise<{ loaded: boolean; rowCount: number; errors: ValidationErrorDetail[] }> {
|
|
236
293
|
// Read JSONL file
|
|
237
294
|
let data = await JsonlReader.read(config.jsonlPath);
|
|
238
295
|
|
|
@@ -350,7 +407,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
350
407
|
|
|
351
408
|
if (validationErrors.length > 0) {
|
|
352
409
|
// Return errors instead of throwing
|
|
353
|
-
return { loaded: false, errors: validationErrorDetails };
|
|
410
|
+
return { loaded: false, rowCount: data.length, errors: validationErrorDetails };
|
|
354
411
|
}
|
|
355
412
|
|
|
356
413
|
// Determine schema - infer from validated data if auto-inference is enabled
|
|
@@ -375,7 +432,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
375
432
|
}
|
|
376
433
|
} else if (config.autoInferSchema !== false) {
|
|
377
434
|
if (validatedData.length === 0) {
|
|
378
|
-
return { loaded: false, errors: [] };
|
|
435
|
+
return { loaded: false, rowCount: 0, errors: [] };
|
|
379
436
|
}
|
|
380
437
|
// Use inferred schema
|
|
381
438
|
schema = inferredSchema!;
|
|
@@ -406,7 +463,10 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
406
463
|
}
|
|
407
464
|
}
|
|
408
465
|
if (foreignKeys) {
|
|
409
|
-
schema.foreignKeys =
|
|
466
|
+
schema.foreignKeys =
|
|
467
|
+
failedDependencies && failedDependencies.size > 0
|
|
468
|
+
? foreignKeys.filter((fk) => !failedDependencies.has(fk.references.table))
|
|
469
|
+
: foreignKeys;
|
|
410
470
|
}
|
|
411
471
|
if (indexes) {
|
|
412
472
|
schema.indexes = indexes;
|
|
@@ -438,13 +498,13 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
438
498
|
config.jsonlPath,
|
|
439
499
|
);
|
|
440
500
|
if (insertErrors.length > 0) {
|
|
441
|
-
return { loaded: false, errors: insertErrors };
|
|
501
|
+
return { loaded: false, rowCount: data.length, errors: insertErrors };
|
|
442
502
|
}
|
|
443
503
|
} else {
|
|
444
504
|
this.insertData(tableName, schema, validatedData);
|
|
445
505
|
}
|
|
446
506
|
|
|
447
|
-
return { loaded: true, errors: [] };
|
|
507
|
+
return { loaded: true, rowCount: data.length, errors: [] };
|
|
448
508
|
}
|
|
449
509
|
|
|
450
510
|
/**
|
package/src/index.ts
CHANGED
package/src/schema-extensions.ts
CHANGED
|
@@ -21,10 +21,7 @@ const SCHEMA_TO_JS_IMPORT_MAP: Record<string, string> = {
|
|
|
21
21
|
* Try each supported schema extension and return the full path of the first
|
|
22
22
|
* one that exists on disk. Returns undefined if none is found.
|
|
23
23
|
*/
|
|
24
|
-
export async function findSchemaFile(
|
|
25
|
-
dir: string,
|
|
26
|
-
tableName: string,
|
|
27
|
-
): Promise<string | undefined> {
|
|
24
|
+
export async function findSchemaFile(dir: string, tableName: string): Promise<string | undefined> {
|
|
28
25
|
for (const ext of SCHEMA_EXTENSIONS) {
|
|
29
26
|
const candidate = join(dir, `${tableName}${ext}`);
|
|
30
27
|
try {
|
package/src/type-generator.ts
CHANGED
|
@@ -30,7 +30,9 @@ export class TypeGenerator {
|
|
|
30
30
|
? this.dataDir
|
|
31
31
|
: join(this.projectRoot, this.dataDir);
|
|
32
32
|
this.outputFile = options.output
|
|
33
|
-
?
|
|
33
|
+
? isAbsolute(options.output)
|
|
34
|
+
? options.output
|
|
35
|
+
: join(this.projectRoot, options.output)
|
|
34
36
|
: join(this.dataDirPath, 'db.ts');
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -109,8 +111,7 @@ export class TypeGenerator {
|
|
|
109
111
|
|
|
110
112
|
// Calculate relative path from output file to schema file
|
|
111
113
|
let relativePath = rewriteExtensionForImport(
|
|
112
|
-
relative(join(this.outputFile, '..'), table.schemaFile)
|
|
113
|
-
.replace(/\\/g, '/'), // Convert Windows paths to Unix-style
|
|
114
|
+
relative(join(this.outputFile, '..'), table.schemaFile).replace(/\\/g, '/'), // Convert Windows paths to Unix-style
|
|
114
115
|
);
|
|
115
116
|
|
|
116
117
|
// Ensure relative path starts with './' or '../'
|
package/src/types.ts
CHANGED
|
@@ -18,10 +18,19 @@ export type InferInput<T> = T extends StandardSchemaV1<infer I, unknown> ? I : n
|
|
|
18
18
|
export type InferOutput<T> = T extends StandardSchemaV1<unknown, infer O> ? O : never;
|
|
19
19
|
|
|
20
20
|
// Validation result types
|
|
21
|
+
export interface TableValidationResult {
|
|
22
|
+
tableName: string;
|
|
23
|
+
valid: boolean;
|
|
24
|
+
rowCount: number;
|
|
25
|
+
errors: ValidationErrorDetail[];
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export interface ValidationResult {
|
|
22
30
|
valid: boolean;
|
|
23
31
|
errors: ValidationErrorDetail[];
|
|
24
32
|
warnings: string[];
|
|
33
|
+
tableResults: TableValidationResult[];
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
export interface ValidationErrorDetail {
|