@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/src/cli.ts CHANGED
@@ -106,72 +106,135 @@ program
106
106
  await db.close();
107
107
  }
108
108
 
109
- // Display warnings if any
110
- if (result.warnings.length > 0) {
111
- for (const warning of result.warnings) {
112
- console.warn(styleText('yellow', `⚠ ${warning}`));
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
- if (result.valid) {
118
- console.log('✓ All records are valid');
119
- process.exit(0);
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
- const formatter = new ErrorFormatter({ verbose: options.verbose });
122
-
123
- // Group errors by file for header
124
- const errorsByFile = new Map<string, typeof result.errors>();
125
- for (const error of result.errors) {
126
- const fileErrors = errorsByFile.get(error.file) || [];
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
- // Format and display errors
132
- for (const [file, fileErrors] of errorsByFile) {
133
- console.error(formatter.formatErrorHeader(fileErrors.length, file));
134
- console.error('');
135
-
136
- // Separate validation errors and foreign key errors
137
- const validationErrors = fileErrors.filter(
138
- (e) => e.type !== 'foreignKey' || !e.foreignKeyError,
139
- );
140
- const foreignKeyErrors = fileErrors.filter(
141
- (e) => e.type === 'foreignKey' && e.foreignKeyError,
142
- );
143
-
144
- // Format validation errors
145
- if (validationErrors.length > 0) {
146
- const formattedValidation = formatter.formatValidationErrors(
147
- validationErrors.map((e) => ({
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
- console.error(formattedValidation);
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
- // Format foreign key errors
157
- for (const error of foreignKeyErrors) {
158
- if (error.foreignKeyError) {
159
- const formattedFk = formatter.formatForeignKeyError({
160
- file: error.file,
161
- rowIndex: error.rowIndex,
162
- column: error.foreignKeyError.column,
163
- value: error.foreignKeyError.value,
164
- referencedTable: error.foreignKeyError.referencedTable,
165
- referencedColumn: error.foreignKeyError.referencedColumn,
166
- });
167
- console.error(formattedFk);
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
- console.error('');
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 { errors, warnings } = await this.loadTableWithDependencies(
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<{ errors: ValidationErrorDetail[]; warnings: string[] }> {
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 { loaded, errors: loadErrors } = await this.loadTable(
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
- ): Promise<{ loaded: boolean; errors: ValidationErrorDetail[] }> {
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 = 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
@@ -32,6 +32,7 @@ export type {
32
32
  StandardSchemaIssue,
33
33
  ValidationError,
34
34
  ValidationResult,
35
+ TableValidationResult,
35
36
  ValidationErrorDetail,
36
37
  InferInput,
37
38
  InferOutput,
@@ -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 {
@@ -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
- ? (isAbsolute(options.output) ? options.output : join(this.projectRoot, options.output))
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 {