@toiroakr/lines-db 0.6.1 → 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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # @toiroakr/lines-db
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c408b92: feat: display per-table validation results for directory validation
8
+
9
+ The `validate` command now shows individual results per table when validating a directory, including record counts for successful tables (e.g., `✓ users (3 records)`).
10
+ - Added `TableValidationResult` type and `tableResults` field to `ValidationResult`
11
+ - Each table result includes `tableName`, `valid`, `rowCount`, `errors`, and `warnings`
12
+
13
+ ### Patch Changes
14
+
15
+ - e61a4ee: fix: gracefully handle foreign key validation when referenced table has errors
16
+
17
+ When validating a directory, if a table had validation errors, any table referencing it via foreign key would crash with a misleading `no such table` SQLite error. Now, foreign key constraints to failed tables are skipped with a clear warning (e.g., `⚠ Skipping foreign key validation for table 'child': referenced table 'parent' has validation errors`), and the child table's own schema validation still runs normally.
18
+
19
+ ## 0.7.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 4597383: feat: support .mts and .cts schema file extensions
24
+
25
+ Schema files are now auto-detected with the following priority: `.schema.ts` > `.schema.mts` > `.schema.cts`. Mixed extensions within a single project are supported.
26
+ - Added `--output` option to `generate` command for specifying the output file path (e.g., `--output ./data/db.mts`)
27
+ - Import paths are correctly rewritten: `.ts`→`.js`, `.mts`→`.mjs`, `.cts`→`.cjs`
28
+ - New exported utilities: `findSchemaFile`, `isSchemaFile`, `extractTableNameFromSchemaFile`, `rewriteExtensionForImport`, `SCHEMA_EXTENSIONS`
29
+
3
30
  ## 0.6.1
4
31
 
5
32
  ### Patch Changes
package/bin/cli.js CHANGED
@@ -10,6 +10,57 @@ import { runInNewContext } from "node:vm";
10
10
  //#region rolldown:runtime
11
11
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
12
12
 
13
+ //#endregion
14
+ //#region src/schema-extensions.ts
15
+ /**
16
+ * Supported schema file extensions, in priority order.
17
+ * The first match wins when discovering schema files.
18
+ */
19
+ const SCHEMA_EXTENSIONS = [
20
+ ".schema.ts",
21
+ ".schema.mts",
22
+ ".schema.cts"
23
+ ];
24
+ /**
25
+ * Map from schema extensions to their JavaScript import counterparts.
26
+ */
27
+ const SCHEMA_TO_JS_IMPORT_MAP = {
28
+ ".schema.ts": ".schema.js",
29
+ ".schema.mts": ".schema.mjs",
30
+ ".schema.cts": ".schema.cjs"
31
+ };
32
+ /**
33
+ * Try each supported schema extension and return the full path of the first
34
+ * one that exists on disk. Returns undefined if none is found.
35
+ */
36
+ async function findSchemaFile(dir, tableName) {
37
+ for (const ext of SCHEMA_EXTENSIONS) {
38
+ const candidate = join(dir, `${tableName}${ext}`);
39
+ try {
40
+ await access(candidate);
41
+ return candidate;
42
+ } catch {}
43
+ }
44
+ }
45
+ /**
46
+ * Synchronously find a schema file among directory entries.
47
+ * Returns the full path of the first match, or undefined.
48
+ */
49
+ function findSchemaFileInEntries(dataDirPath, tableName, entries) {
50
+ for (const ext of SCHEMA_EXTENSIONS) {
51
+ const candidateName = `${tableName}${ext}`;
52
+ if (entries.some((e) => e.isFile() && e.name === candidateName)) return join(dataDirPath, candidateName);
53
+ }
54
+ }
55
+ /**
56
+ * Rewrite a TypeScript path to its JavaScript counterpart for ESM imports.
57
+ * ".schema.ts" -> ".schema.js", ".schema.mts" -> ".schema.mjs", ".schema.cts" -> ".schema.cjs"
58
+ */
59
+ function rewriteExtensionForImport(filePath) {
60
+ for (const [tsExt, jsExt] of Object.entries(SCHEMA_TO_JS_IMPORT_MAP)) if (filePath.endsWith(tsExt)) return filePath.slice(0, -tsExt.length) + jsExt;
61
+ return filePath;
62
+ }
63
+
13
64
  //#endregion
14
65
  //#region src/type-generator.ts
15
66
  var TypeGenerator = class {
@@ -22,7 +73,7 @@ var TypeGenerator = class {
22
73
  this.projectRoot = envProjectRoot !== void 0 ? envProjectRoot : options.projectRoot || process.cwd();
23
74
  this.dataDir = options.dataDir;
24
75
  this.dataDirPath = isAbsolute(this.dataDir) ? this.dataDir : join(this.projectRoot, this.dataDir);
25
- this.outputFile = join(this.dataDirPath, "db.ts");
76
+ this.outputFile = options.output ? isAbsolute(options.output) ? options.output : join(this.projectRoot, options.output) : join(this.dataDirPath, "db.ts");
26
77
  }
27
78
  /**
28
79
  * Generate types file from JSONL files and their optional schema files.
@@ -45,12 +96,10 @@ var TypeGenerator = class {
45
96
  const tables = [];
46
97
  for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".jsonl")) {
47
98
  const tableName = basename(entry.name, ".jsonl");
48
- const schemaFileName = `${tableName}.schema.ts`;
49
- const schemaFilePath = join(this.dataDirPath, schemaFileName);
50
- const hasSchema = entries.some((e) => e.isFile() && e.name === schemaFileName);
99
+ const schemaFilePath = findSchemaFileInEntries(this.dataDirPath, tableName, entries);
51
100
  tables.push({
52
101
  tableName,
53
- schemaFile: hasSchema ? schemaFilePath : void 0
102
+ schemaFile: schemaFilePath
54
103
  });
55
104
  }
56
105
  return tables;
@@ -71,7 +120,7 @@ var TypeGenerator = class {
71
120
  if (table.schemaFile) {
72
121
  const schemaIdentifier = this.createSchemaIdentifier(table.tableName, usedAliases);
73
122
  usedAliases.add(schemaIdentifier);
74
- let relativePath = relative(join(this.outputFile, ".."), table.schemaFile).replace(/\\/g, "/").replace(".ts", ".js");
123
+ let relativePath = rewriteExtensionForImport(relative(join(this.outputFile, ".."), table.schemaFile).replace(/\\/g, "/"));
75
124
  if (!relativePath.startsWith(".")) relativePath = "./" + relativePath;
76
125
  imports.push(`import { schema as ${schemaIdentifier} } from '${relativePath}';`);
77
126
  tableEntries.push(` ${tableKey}: InferOutput<typeof ${schemaIdentifier}>;`);
@@ -279,27 +328,17 @@ var SchemaLoader = class {
279
328
  * Check if a schema file exists for a table
280
329
  */
281
330
  static async hasSchema(jsonlPath) {
282
- const schemaPath = join(dirname(jsonlPath), `${basename(jsonlPath, ".jsonl")}.schema.ts`);
283
- try {
284
- await access(schemaPath);
285
- return true;
286
- } catch {
287
- return false;
288
- }
331
+ return await findSchemaFile(dirname(jsonlPath), basename(jsonlPath, ".jsonl")) !== void 0;
289
332
  }
290
333
  /**
291
334
  * Load a validation schema file for a table
292
- * Requires ${tableName}.schema.ts to exist alongside the JSONL file
335
+ * Requires ${tableName}.schema.{ts,mts,cts} to exist alongside the JSONL file
293
336
  */
294
337
  static async loadSchema(jsonlPath) {
295
338
  const dir = dirname(jsonlPath);
296
339
  const tableName = basename(jsonlPath, ".jsonl");
297
- const schemaPath = join(dir, `${tableName}.schema.ts`);
298
- try {
299
- await access(schemaPath);
300
- } catch (error) {
301
- throw new Error(`Schema file not found for table '${tableName}'. Expected: ${schemaPath}`, { cause: error instanceof Error ? error : void 0 });
302
- }
340
+ const schemaPath = await findSchemaFile(dir, tableName);
341
+ if (!schemaPath) throw new Error(`Schema file not found for table '${tableName}'. Expected one of: ${SCHEMA_EXTENSIONS.map((ext) => `${tableName}${ext}`).join(", ")}`);
303
342
  try {
304
343
  const module = await import(`${pathToFileURL(schemaPath).href}?t=${Date.now()}`);
305
344
  const schema = module.default || module.schema;
@@ -384,6 +423,7 @@ var LinesDB = class LinesDB {
384
423
  async initialize(options) {
385
424
  const allErrors = [];
386
425
  const allWarnings = [];
426
+ const allRowCounts = /* @__PURE__ */ new Map();
387
427
  const tableName = options?.tableName;
388
428
  const detailedValidate = options?.detailedValidate ?? false;
389
429
  const transform = options?.transform;
@@ -395,14 +435,27 @@ var LinesDB = class LinesDB {
395
435
  const attemptedTables = /* @__PURE__ */ new Set();
396
436
  for (const tableNameToLoad of tablesToLoad) if (!attemptedTables.has(tableNameToLoad)) {
397
437
  const tableTransform = tableNameToLoad === tableName ? transform : void 0;
398
- const { errors, warnings } = await this.loadTableWithDependencies(tableNameToLoad, loadedTables, loadingTables, attemptedTables, detailedValidate, tableTransform);
438
+ const { errors, warnings, rowCounts: tableRowCounts } = await this.loadTableWithDependencies(tableNameToLoad, loadedTables, loadingTables, attemptedTables, detailedValidate, tableTransform);
399
439
  allErrors.push(...errors);
400
440
  allWarnings.push(...warnings);
441
+ for (const [k, v] of tableRowCounts) allRowCounts.set(k, v);
401
442
  }
443
+ const tableResults = tablesToLoad.map((name) => {
444
+ const tableErrors = allErrors.filter((e) => e.tableName === name);
445
+ const tableWarnings = allWarnings.filter((w) => w.includes(`'${name}'`));
446
+ return {
447
+ tableName: name,
448
+ valid: tableErrors.length === 0,
449
+ rowCount: allRowCounts.get(name) ?? 0,
450
+ errors: tableErrors,
451
+ warnings: tableWarnings
452
+ };
453
+ });
402
454
  return {
403
455
  valid: allErrors.length === 0,
404
456
  errors: allErrors,
405
- warnings: allWarnings
457
+ warnings: allWarnings,
458
+ tableResults
406
459
  };
407
460
  }
408
461
  /**
@@ -411,9 +464,11 @@ var LinesDB = class LinesDB {
411
464
  async loadTableWithDependencies(tableName, loadedTables, loadingTables, attemptedTables, detailedValidate, transform) {
412
465
  const errors = [];
413
466
  const warnings = [];
467
+ const rowCounts = /* @__PURE__ */ new Map();
414
468
  if (attemptedTables.has(tableName)) return {
415
469
  errors,
416
- warnings
470
+ warnings,
471
+ rowCounts
417
472
  };
418
473
  attemptedTables.add(tableName);
419
474
  if (loadingTables.has(tableName)) throw new Error(`Circular dependency detected for table '${tableName}'`);
@@ -424,8 +479,11 @@ var LinesDB = class LinesDB {
424
479
  let foreignKeys;
425
480
  try {
426
481
  const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
427
- const schemaModule = await import(`${pathToFileURL$1(tableConfig.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
428
- foreignKeys = (schemaModule.schema || schemaModule.default)?.foreignKeys || schemaModule.foreignKeys;
482
+ const schemaPath = await findSchemaFile(dirname(tableConfig.jsonlPath), basename(tableConfig.jsonlPath, ".jsonl"));
483
+ if (schemaPath) {
484
+ const schemaModule = await import(`${pathToFileURL$1(schemaPath).href}?t=${Date.now()}`);
485
+ foreignKeys = (schemaModule.schema || schemaModule.default)?.foreignKeys || schemaModule.foreignKeys;
486
+ }
429
487
  } catch {}
430
488
  if (foreignKeys && foreignKeys.length > 0) for (const fk of foreignKeys) {
431
489
  const referencedTable = fk.references.table;
@@ -434,10 +492,21 @@ var LinesDB = class LinesDB {
434
492
  const depResult = await this.loadTableWithDependencies(referencedTable, loadedTables, loadingTables, attemptedTables, detailedValidate, void 0);
435
493
  errors.push(...depResult.errors);
436
494
  warnings.push(...depResult.warnings);
495
+ for (const [k, v] of depResult.rowCounts) rowCounts.set(k, v);
437
496
  } else throw new Error(`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`);
438
497
  }
439
- const { loaded, errors: loadErrors } = await this.loadTable(tableName, tableConfig, detailedValidate, transform);
498
+ const failedDependencies = /* @__PURE__ */ new Set();
499
+ if (foreignKeys && foreignKeys.length > 0) {
500
+ for (const fk of foreignKeys) {
501
+ const referencedTable = fk.references.table;
502
+ if (referencedTable === tableName) continue;
503
+ if (attemptedTables.has(referencedTable) && !loadedTables.has(referencedTable)) failedDependencies.add(referencedTable);
504
+ }
505
+ if (failedDependencies.size > 0) for (const dep of failedDependencies) warnings.push(`Skipping foreign key validation for table '${tableName}': referenced table '${dep}' has validation errors`);
506
+ }
507
+ const { loaded, rowCount, errors: loadErrors } = await this.loadTable(tableName, tableConfig, detailedValidate, transform, failedDependencies);
440
508
  errors.push(...loadErrors);
509
+ rowCounts.set(tableName, rowCount);
441
510
  if (loaded) loadedTables.add(tableName);
442
511
  else {
443
512
  warnings.push(`Table '${tableName}' was not loaded (no data or skipped)`);
@@ -448,14 +517,15 @@ var LinesDB = class LinesDB {
448
517
  }
449
518
  return {
450
519
  errors,
451
- warnings
520
+ warnings,
521
+ rowCounts
452
522
  };
453
523
  }
454
524
  /**
455
525
  * Load a single table from JSONL file
456
526
  * @returns Object with loaded status and validation errors
457
527
  */
458
- async loadTable(tableName, config, detailedValidate, transform) {
528
+ async loadTable(tableName, config, detailedValidate, transform, failedDependencies) {
459
529
  let data = await JsonlReader.read(config.jsonlPath);
460
530
  if (transform) data = data.map((row) => transform(row));
461
531
  let validationSchema = config.validationSchema;
@@ -465,7 +535,9 @@ var LinesDB = class LinesDB {
465
535
  } catch (_error) {}
466
536
  if (!config.validationSchema) try {
467
537
  const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
468
- const schemaModule = await import(`${pathToFileURL$1(config.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
538
+ const schemaPath = await findSchemaFile(dirname(config.jsonlPath), basename(config.jsonlPath, ".jsonl"));
539
+ if (!schemaPath) throw new Error("Schema file not found");
540
+ const schemaModule = await import(`${pathToFileURL$1(schemaPath).href}?t=${Date.now()}`);
469
541
  const schemaExport = schemaModule.schema || schemaModule.default;
470
542
  if (schemaExport?.primaryKey) schemaMetadata.primaryKey = schemaExport.primaryKey;
471
543
  else if (schemaModule.primaryKey) schemaMetadata.primaryKey = schemaModule.primaryKey;
@@ -508,6 +580,7 @@ var LinesDB = class LinesDB {
508
580
  }));
509
581
  if (validationErrors.length > 0) return {
510
582
  loaded: false,
583
+ rowCount: data.length,
511
584
  errors: validationErrorDetails
512
585
  };
513
586
  let schema;
@@ -522,6 +595,7 @@ var LinesDB = class LinesDB {
522
595
  } else if (config.autoInferSchema !== false) {
523
596
  if (validatedData.length === 0) return {
524
597
  loaded: false,
598
+ rowCount: 0,
525
599
  errors: []
526
600
  };
527
601
  schema = inferredSchema;
@@ -537,7 +611,7 @@ var LinesDB = class LinesDB {
537
611
  const idColumn = schema.columns.find((c) => c.name === "id");
538
612
  if (idColumn) idColumn.primaryKey = true;
539
613
  }
540
- if (foreignKeys) schema.foreignKeys = foreignKeys;
614
+ if (foreignKeys) schema.foreignKeys = failedDependencies && failedDependencies.size > 0 ? foreignKeys.filter((fk) => !failedDependencies.has(fk.references.table)) : foreignKeys;
541
615
  if (indexes) {
542
616
  schema.indexes = indexes;
543
617
  for (const index of indexes) if (index.unique && index.columns.length === 1) {
@@ -551,11 +625,13 @@ var LinesDB = class LinesDB {
551
625
  const insertErrors = this.insertDataWithDetailedValidation(tableName, schema, validatedData, config.jsonlPath);
552
626
  if (insertErrors.length > 0) return {
553
627
  loaded: false,
628
+ rowCount: data.length,
554
629
  errors: insertErrors
555
630
  };
556
631
  } else this.insertData(tableName, schema, validatedData);
557
632
  return {
558
633
  loaded: true,
634
+ rowCount: data.length,
559
635
  errors: []
560
636
  };
561
637
  }
@@ -1277,9 +1353,12 @@ function runInSandbox(expression, context = {}) {
1277
1353
  }
1278
1354
  const program = new Command();
1279
1355
  program.name("@toiroakr/lines-db").description("Database utilities for JSONL files").version("1.0.0");
1280
- program.command("generate").description("Generate TypeScript type definitions from schema files").argument("<dataDir>", "Directory containing JSONL and schema files").action(async (dataDir) => {
1356
+ program.command("generate").description("Generate TypeScript type definitions from schema files").argument("<dataDir>", "Directory containing JSONL and schema files").option("-o, --output <path>", "Output file path (default: db.ts in dataDir)").action(async (dataDir, options) => {
1281
1357
  try {
1282
- await new TypeGenerator({ dataDir }).generate();
1358
+ await new TypeGenerator({
1359
+ dataDir,
1360
+ output: options.output
1361
+ }).generate();
1283
1362
  console.log("Type generation completed successfully!");
1284
1363
  } catch (error) {
1285
1364
  console.error("Error:", error instanceof Error ? error.message : String(error));
@@ -1306,48 +1385,73 @@ program.command("validate").description("Validate JSONL file(s) against schema")
1306
1385
  } finally {
1307
1386
  await db.close();
1308
1387
  }
1309
- if (result.warnings.length > 0) {
1310
- for (const warning of result.warnings) console.warn(styleText("yellow", `⚠ ${warning}`));
1311
- console.log("");
1312
- }
1313
- if (result.valid) {
1314
- console.log("✓ All records are valid");
1315
- process.exit(0);
1316
- } else {
1388
+ if (!tableName) {
1317
1389
  const formatter = new ErrorFormatter({ verbose: options.verbose });
1318
- const errorsByFile = /* @__PURE__ */ new Map();
1319
- for (const error of result.errors) {
1320
- const fileErrors = errorsByFile.get(error.file) || [];
1321
- fileErrors.push(error);
1322
- errorsByFile.set(error.file, fileErrors);
1323
- }
1324
- for (const [file, fileErrors] of errorsByFile) {
1325
- console.error(formatter.formatErrorHeader(fileErrors.length, file));
1390
+ for (const tableResult of result.tableResults) if (tableResult.valid && tableResult.warnings.length === 0) console.log(styleText("green", `✓ ${tableResult.tableName} (${tableResult.rowCount} records)`));
1391
+ else if (tableResult.valid && tableResult.warnings.length > 0) for (const warning of tableResult.warnings) console.warn(styleText("yellow", `⚠ ${warning}`));
1392
+ else {
1393
+ const fileErrors = tableResult.errors;
1394
+ console.error(formatter.formatErrorHeader(fileErrors.length, fileErrors[0]?.file));
1326
1395
  console.error("");
1327
1396
  const validationErrors = fileErrors.filter((e) => e.type !== "foreignKey" || !e.foreignKeyError);
1328
1397
  const foreignKeyErrors = fileErrors.filter((e) => e.type === "foreignKey" && e.foreignKeyError);
1329
- if (validationErrors.length > 0) {
1330
- const formattedValidation = formatter.formatValidationErrors(validationErrors.map((e) => ({
1398
+ if (validationErrors.length > 0) console.error(formatter.formatValidationErrors(validationErrors.map((e) => ({
1399
+ file: e.file,
1400
+ rowIndex: e.rowIndex,
1401
+ issues: e.issues
1402
+ }))));
1403
+ for (const error of foreignKeyErrors) if (error.foreignKeyError) console.error(formatter.formatForeignKeyError({
1404
+ file: error.file,
1405
+ rowIndex: error.rowIndex,
1406
+ column: error.foreignKeyError.column,
1407
+ value: error.foreignKeyError.value,
1408
+ referencedTable: error.foreignKeyError.referencedTable,
1409
+ referencedColumn: error.foreignKeyError.referencedColumn
1410
+ }));
1411
+ console.error("");
1412
+ }
1413
+ if (result.valid) {
1414
+ console.log("");
1415
+ console.log(styleText("green", "✓ All records are valid"));
1416
+ process.exit(0);
1417
+ } else process.exit(1);
1418
+ } else {
1419
+ if (result.warnings.length > 0) {
1420
+ for (const warning of result.warnings) console.warn(styleText("yellow", `⚠ ${warning}`));
1421
+ console.log("");
1422
+ }
1423
+ if (result.valid) {
1424
+ console.log(styleText("green", "✓ All records are valid"));
1425
+ process.exit(0);
1426
+ } else {
1427
+ const formatter = new ErrorFormatter({ verbose: options.verbose });
1428
+ for (const [, fileErrors] of result.errors.reduce((map, error) => {
1429
+ const errors = map.get(error.file) || [];
1430
+ errors.push(error);
1431
+ map.set(error.file, errors);
1432
+ return map;
1433
+ }, /* @__PURE__ */ new Map())) {
1434
+ console.error(formatter.formatErrorHeader(fileErrors.length, fileErrors[0]?.file));
1435
+ console.error("");
1436
+ const validationErrors = fileErrors.filter((e) => e.type !== "foreignKey" || !e.foreignKeyError);
1437
+ const foreignKeyErrors = fileErrors.filter((e) => e.type === "foreignKey" && e.foreignKeyError);
1438
+ if (validationErrors.length > 0) console.error(formatter.formatValidationErrors(validationErrors.map((e) => ({
1331
1439
  file: e.file,
1332
1440
  rowIndex: e.rowIndex,
1333
1441
  issues: e.issues
1334
- })));
1335
- console.error(formattedValidation);
1336
- }
1337
- for (const error of foreignKeyErrors) if (error.foreignKeyError) {
1338
- const formattedFk = formatter.formatForeignKeyError({
1442
+ }))));
1443
+ for (const error of foreignKeyErrors) if (error.foreignKeyError) console.error(formatter.formatForeignKeyError({
1339
1444
  file: error.file,
1340
1445
  rowIndex: error.rowIndex,
1341
1446
  column: error.foreignKeyError.column,
1342
1447
  value: error.foreignKeyError.value,
1343
1448
  referencedTable: error.foreignKeyError.referencedTable,
1344
1449
  referencedColumn: error.foreignKeyError.referencedColumn
1345
- });
1346
- console.error(formattedFk);
1450
+ }));
1451
+ console.error("");
1347
1452
  }
1348
- console.error("");
1453
+ process.exit(1);
1349
1454
  }
1350
- process.exit(1);
1351
1455
  }
1352
1456
  } catch (error) {
1353
1457
  console.error("Error:", error instanceof Error ? error.message : String(error));