@toiroakr/lines-db 0.2.1 → 0.4.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 +222 -171
- package/dist/index.cjs +143 -92
- package/dist/index.d.cts +23 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +23 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +143 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +5 -2
- package/src/database.ts +221 -77
- package/src/jsonl-reader.ts +1 -1
- package/src/schema.ts +6 -6
- package/src/sqlite-adapter.ts +4 -0
- package/src/types.ts +2 -2
- package/src/validator.ts +70 -72
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ function createDatabase(path = ":memory:") {
|
|
|
29
29
|
function createNodeDatabase(path) {
|
|
30
30
|
const { DatabaseSync } = __require("node:sqlite");
|
|
31
31
|
const db = new DatabaseSync(path);
|
|
32
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
32
33
|
return {
|
|
33
34
|
prepare(sql) {
|
|
34
35
|
const stmt = db.prepare(sql);
|
|
@@ -124,7 +125,7 @@ var JsonlReader = class {
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
static inferType(value) {
|
|
127
|
-
if (value === null) return "NULL";
|
|
128
|
+
if (value === null || value === void 0) return "NULL";
|
|
128
129
|
if (typeof value === "number") return Number.isInteger(value) ? "INTEGER" : "REAL";
|
|
129
130
|
if (typeof value === "string") return "TEXT";
|
|
130
131
|
if (typeof value === "boolean") return "INTEGER";
|
|
@@ -260,9 +261,9 @@ var DirectoryScanner = class {
|
|
|
260
261
|
* const schema = defineSchema(
|
|
261
262
|
* v.object({ id: v.number(), customerId: v.number() }),
|
|
262
263
|
* {
|
|
263
|
-
* primaryKey:
|
|
264
|
+
* primaryKey: 'id',
|
|
264
265
|
* foreignKeys: [
|
|
265
|
-
* {
|
|
266
|
+
* { column: 'customerId', references: { table: 'users', column: 'id' } }
|
|
266
267
|
* ]
|
|
267
268
|
* }
|
|
268
269
|
* );
|
|
@@ -308,57 +309,78 @@ var LinesDB = class LinesDB {
|
|
|
308
309
|
}
|
|
309
310
|
/**
|
|
310
311
|
* Initialize database by loading all JSONL files
|
|
312
|
+
* Uses dependency resolution to ensure foreign key references are loaded in correct order
|
|
311
313
|
*/
|
|
312
314
|
async initialize() {
|
|
313
315
|
this.tables = await DirectoryScanner.scanDirectory(this.config.dataDir);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
const loadedTables = /* @__PURE__ */ new Set();
|
|
317
|
+
const loadingTables = /* @__PURE__ */ new Set();
|
|
318
|
+
for (const [tableName] of this.tables) if (!loadedTables.has(tableName)) try {
|
|
319
|
+
await this.loadTableWithDependencies(tableName, loadedTables, loadingTables);
|
|
316
320
|
} catch (error) {
|
|
317
321
|
console.warn(`Warning: Failed to load table '${tableName}':`, error instanceof Error ? error.message : String(error));
|
|
318
322
|
this.tables.delete(tableName);
|
|
323
|
+
this.schemas.delete(tableName);
|
|
324
|
+
this.validationSchemas.delete(tableName);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Load a table and its dependencies recursively
|
|
329
|
+
*/
|
|
330
|
+
async loadTableWithDependencies(tableName, loadedTables, loadingTables) {
|
|
331
|
+
if (loadedTables.has(tableName)) return;
|
|
332
|
+
if (loadingTables.has(tableName)) throw new Error(`Circular dependency detected for table '${tableName}'`);
|
|
333
|
+
const tableConfig = this.tables.get(tableName);
|
|
334
|
+
if (!tableConfig) throw new Error(`Table configuration not found for '${tableName}'`);
|
|
335
|
+
loadingTables.add(tableName);
|
|
336
|
+
try {
|
|
337
|
+
let foreignKeys;
|
|
338
|
+
try {
|
|
339
|
+
const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
|
|
340
|
+
const schemaModule = await import(`${pathToFileURL$1(tableConfig.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
|
|
341
|
+
foreignKeys = (schemaModule.schema || schemaModule.default)?.foreignKeys || schemaModule.foreignKeys;
|
|
342
|
+
} catch {}
|
|
343
|
+
if (foreignKeys && foreignKeys.length > 0) for (const fk of foreignKeys) {
|
|
344
|
+
const referencedTable = fk.references.table;
|
|
345
|
+
if (!loadedTables.has(referencedTable)) if (this.tables.has(referencedTable)) await this.loadTableWithDependencies(referencedTable, loadedTables, loadingTables);
|
|
346
|
+
else throw new Error(`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`);
|
|
347
|
+
}
|
|
348
|
+
if (await this.loadTable(tableName, tableConfig)) loadedTables.add(tableName);
|
|
349
|
+
else this.tables.delete(tableName);
|
|
350
|
+
} finally {
|
|
351
|
+
loadingTables.delete(tableName);
|
|
319
352
|
}
|
|
320
353
|
}
|
|
321
354
|
/**
|
|
322
355
|
* Load a single table from JSONL file
|
|
356
|
+
* @returns true if table was loaded, false if skipped
|
|
323
357
|
*/
|
|
324
358
|
async loadTable(tableName, config) {
|
|
325
359
|
const data = await JsonlReader.read(config.jsonlPath);
|
|
326
|
-
if (data.length === 0) {
|
|
327
|
-
console.warn(`Warning: Table ${tableName} has no data`);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
360
|
let validationSchema = config.validationSchema;
|
|
361
|
+
const schemaMetadata = {};
|
|
331
362
|
if (!validationSchema) try {
|
|
332
363
|
validationSchema = await SchemaLoader.loadSchema(config.jsonlPath);
|
|
333
|
-
} catch (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
364
|
+
} catch (_error) {}
|
|
365
|
+
if (!config.validationSchema) try {
|
|
366
|
+
const { pathToFileURL: pathToFileURL$1 } = await import("node:url");
|
|
367
|
+
const schemaModule = await import(`${pathToFileURL$1(config.jsonlPath.replace(".jsonl", ".schema.ts")).href}?t=${Date.now()}`);
|
|
368
|
+
const schemaExport = schemaModule.schema || schemaModule.default;
|
|
369
|
+
if (schemaExport?.primaryKey) schemaMetadata.primaryKey = schemaExport.primaryKey;
|
|
370
|
+
else if (schemaModule.primaryKey) schemaMetadata.primaryKey = schemaModule.primaryKey;
|
|
371
|
+
if (schemaExport?.foreignKeys) schemaMetadata.foreignKeys = schemaExport.foreignKeys;
|
|
372
|
+
else if (schemaModule.foreignKeys) schemaMetadata.foreignKeys = schemaModule.foreignKeys;
|
|
373
|
+
if (schemaExport?.indexes) schemaMetadata.indexes = schemaExport.indexes;
|
|
374
|
+
else if (schemaModule.indexes) schemaMetadata.indexes = schemaModule.indexes;
|
|
375
|
+
} catch (_error) {}
|
|
341
376
|
this.validationSchemas.set(tableName, validationSchema);
|
|
342
|
-
let schema;
|
|
343
|
-
if (config.schema) schema = config.schema;
|
|
344
|
-
else if (config.autoInferSchema !== false) schema = JsonlReader.inferSchema(tableName, data);
|
|
345
|
-
else throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
|
|
346
|
-
if (validationSchema) {
|
|
347
|
-
const biSchema = validationSchema;
|
|
348
|
-
if (biSchema.primaryKey && !schema.columns.some((col) => col.primaryKey)) for (const pkColumn of biSchema.primaryKey) {
|
|
349
|
-
const col = schema.columns.find((c) => c.name === pkColumn);
|
|
350
|
-
if (col) col.primaryKey = true;
|
|
351
|
-
}
|
|
352
|
-
if (biSchema.foreignKeys) schema.foreignKeys = biSchema.foreignKeys;
|
|
353
|
-
if (biSchema.indexes) schema.indexes = biSchema.indexes;
|
|
354
|
-
}
|
|
355
|
-
this.schemas.set(tableName, schema);
|
|
356
|
-
this.createTable(schema);
|
|
357
377
|
const validationErrors = [];
|
|
378
|
+
const validatedData = [];
|
|
358
379
|
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
359
380
|
const row = data[rowIndex];
|
|
360
381
|
try {
|
|
361
|
-
this.
|
|
382
|
+
const validatedRow = this.validateAndTransform(tableName, row);
|
|
383
|
+
validatedData.push(validatedRow);
|
|
362
384
|
} catch (error) {
|
|
363
385
|
if (error instanceof Error && error.name === "ValidationError") validationErrors.push({
|
|
364
386
|
rowIndex,
|
|
@@ -375,13 +397,34 @@ var LinesDB = class LinesDB {
|
|
|
375
397
|
enhancedError.issues = validationErrors[0].error.issues;
|
|
376
398
|
throw enhancedError;
|
|
377
399
|
}
|
|
378
|
-
|
|
400
|
+
let schema;
|
|
401
|
+
if (config.schema) schema = config.schema;
|
|
402
|
+
else if (config.autoInferSchema !== false) {
|
|
403
|
+
if (validatedData.length === 0) return false;
|
|
404
|
+
schema = JsonlReader.inferSchema(tableName, validatedData);
|
|
405
|
+
} else throw new Error(`No schema provided for table ${tableName} and autoInferSchema is disabled`);
|
|
406
|
+
const biSchema = validationSchema;
|
|
407
|
+
const primaryKey = biSchema?.primaryKey || schemaMetadata.primaryKey;
|
|
408
|
+
const foreignKeys = biSchema?.foreignKeys || schemaMetadata.foreignKeys;
|
|
409
|
+
const indexes = biSchema?.indexes || schemaMetadata.indexes;
|
|
410
|
+
if (primaryKey && !schema.columns.some((col) => col.primaryKey)) {
|
|
411
|
+
const col = schema.columns.find((c) => c.name === primaryKey);
|
|
412
|
+
if (col) col.primaryKey = true;
|
|
413
|
+
} else if (!primaryKey && !schema.columns.some((col) => col.primaryKey)) {
|
|
414
|
+
const idColumn = schema.columns.find((c) => c.name === "id");
|
|
415
|
+
if (idColumn) idColumn.primaryKey = true;
|
|
416
|
+
}
|
|
417
|
+
if (foreignKeys) schema.foreignKeys = foreignKeys;
|
|
418
|
+
if (indexes) schema.indexes = indexes;
|
|
419
|
+
this.schemas.set(tableName, schema);
|
|
420
|
+
this.createTable(schema);
|
|
421
|
+
this.insertData(tableName, schema, validatedData);
|
|
422
|
+
return true;
|
|
379
423
|
}
|
|
380
424
|
/**
|
|
381
425
|
* Create table in SQLite with constraints and indexes
|
|
382
426
|
*/
|
|
383
427
|
createTable(schema) {
|
|
384
|
-
this.db.exec("PRAGMA foreign_keys = ON");
|
|
385
428
|
const quotedTableName = this.quoteTableName(schema.name);
|
|
386
429
|
const columnDefs = schema.columns.map((col) => {
|
|
387
430
|
const sqlType = col.type === "JSON" ? "TEXT" : col.type;
|
|
@@ -393,7 +436,7 @@ var LinesDB = class LinesDB {
|
|
|
393
436
|
});
|
|
394
437
|
const foreignKeyDefs = [];
|
|
395
438
|
if (schema.foreignKeys && schema.foreignKeys.length > 0) for (const fk of schema.foreignKeys) {
|
|
396
|
-
const fkParts = [`FOREIGN KEY (${
|
|
439
|
+
const fkParts = [`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`, `REFERENCES ${this.quoteTableName(fk.references.table)}(${this.quoteIdentifier(fk.references.column)})`];
|
|
397
440
|
if (fk.onDelete) fkParts.push(`ON DELETE ${fk.onDelete}`);
|
|
398
441
|
if (fk.onUpdate) fkParts.push(`ON UPDATE ${fk.onUpdate}`);
|
|
399
442
|
foreignKeyDefs.push(fkParts.join(" "));
|
|
@@ -508,17 +551,12 @@ var LinesDB = class LinesDB {
|
|
|
508
551
|
return deserializedRow;
|
|
509
552
|
}
|
|
510
553
|
/**
|
|
511
|
-
* Validate data using StandardSchema
|
|
554
|
+
* Validate data using StandardSchema and return the transformed value
|
|
512
555
|
* Note: Only synchronous validation is supported
|
|
513
556
|
*/
|
|
514
|
-
|
|
557
|
+
validateAndTransform(tableName, data) {
|
|
515
558
|
const schema = this.validationSchemas.get(tableName);
|
|
516
|
-
|
|
517
|
-
if (!schema) {
|
|
518
|
-
console.log(`[LinesDB] No validation schema found for table '${tableName}', skipping validation`);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
console.log(`[LinesDB] Validating data:`, JSON.stringify(data));
|
|
559
|
+
if (!schema) return data;
|
|
522
560
|
const result = schema["~standard"].validate(data);
|
|
523
561
|
if (result instanceof Promise) throw new Error("Asynchronous validation is not supported. Please use synchronous validation schemas.");
|
|
524
562
|
if (result.issues && result.issues.length > 0) {
|
|
@@ -535,6 +573,17 @@ var LinesDB = class LinesDB {
|
|
|
535
573
|
error.issues = result.issues;
|
|
536
574
|
throw error;
|
|
537
575
|
}
|
|
576
|
+
const transformedValue = "value" in result ? result.value : data;
|
|
577
|
+
const normalizedValue = {};
|
|
578
|
+
for (const [key, value] of Object.entries(transformedValue)) normalizedValue[key] = value === void 0 ? null : value;
|
|
579
|
+
return normalizedValue;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Validate data using StandardSchema (without returning transformed value)
|
|
583
|
+
* Note: Only synchronous validation is supported
|
|
584
|
+
*/
|
|
585
|
+
validateData(tableName, data) {
|
|
586
|
+
this.validateAndTransform(tableName, data);
|
|
538
587
|
}
|
|
539
588
|
/**
|
|
540
589
|
* Insert a row into a table with validation
|
|
@@ -857,7 +906,7 @@ var LinesDB = class LinesDB {
|
|
|
857
906
|
await this.sync();
|
|
858
907
|
return result;
|
|
859
908
|
} catch (error) {
|
|
860
|
-
this.db.exec("ROLLBACK");
|
|
909
|
+
if (this.inTransaction) this.db.exec("ROLLBACK");
|
|
861
910
|
this.inTransaction = false;
|
|
862
911
|
throw error;
|
|
863
912
|
}
|
|
@@ -1025,9 +1074,9 @@ var Validator = class {
|
|
|
1025
1074
|
allErrors.push(...result.errors);
|
|
1026
1075
|
allWarnings.push(...result.warnings);
|
|
1027
1076
|
}
|
|
1028
|
-
if (filesWithSchema.length > 0) {
|
|
1029
|
-
const
|
|
1030
|
-
allErrors.push(...
|
|
1077
|
+
if (filesWithSchema.length > 0 && allErrors.length === 0) {
|
|
1078
|
+
const dbErrors = await this.validateWithDatabase(dirPath, filesWithSchema);
|
|
1079
|
+
allErrors.push(...dbErrors);
|
|
1031
1080
|
}
|
|
1032
1081
|
return {
|
|
1033
1082
|
valid: allErrors.length === 0,
|
|
@@ -1036,53 +1085,50 @@ var Validator = class {
|
|
|
1036
1085
|
};
|
|
1037
1086
|
}
|
|
1038
1087
|
/**
|
|
1039
|
-
* Validate
|
|
1088
|
+
* Validate by loading data into an actual database
|
|
1089
|
+
* This catches constraint violations (unique, primary key, foreign key, etc.)
|
|
1040
1090
|
*/
|
|
1041
|
-
async
|
|
1091
|
+
async validateWithDatabase(dirPath, jsonlFiles) {
|
|
1042
1092
|
const errors = [];
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
const row = data[i];
|
|
1069
|
-
const foreignKeyValues = fk.columns.map((col) => row[col]);
|
|
1070
|
-
const compositeKey = JSON.stringify(foreignKeyValues);
|
|
1071
|
-
if (!referencedValues.has(compositeKey)) errors.push({
|
|
1072
|
-
file,
|
|
1073
|
-
tableName,
|
|
1074
|
-
rowIndex: i,
|
|
1075
|
-
issues: [],
|
|
1076
|
-
type: "foreignKey",
|
|
1077
|
-
foreignKeyError: {
|
|
1078
|
-
column: fk.columns.join(", "),
|
|
1079
|
-
value: foreignKeyValues.length === 1 ? foreignKeyValues[0] : foreignKeyValues,
|
|
1080
|
-
referencedTable,
|
|
1081
|
-
referencedColumn: fk.references.columns.join(", ")
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1093
|
+
const warnMessages = [];
|
|
1094
|
+
const originalWarn = console.warn;
|
|
1095
|
+
console.warn = (...args) => {
|
|
1096
|
+
const message = args.map((arg) => String(arg)).join(" ");
|
|
1097
|
+
warnMessages.push(message);
|
|
1098
|
+
originalWarn(...args);
|
|
1099
|
+
};
|
|
1100
|
+
try {
|
|
1101
|
+
const db = LinesDB.create({ dataDir: dirPath });
|
|
1102
|
+
await db.initialize();
|
|
1103
|
+
await db.close();
|
|
1104
|
+
for (const message of warnMessages) if (message.includes("Failed to load table")) {
|
|
1105
|
+
const tableNameMatch = message.match(/Failed to load table '([^']+)'/);
|
|
1106
|
+
const tableName = tableNameMatch ? tableNameMatch[1] : "unknown";
|
|
1107
|
+
const file = jsonlFiles.find((f) => basename(f, ".jsonl") === tableName);
|
|
1108
|
+
errors.push({
|
|
1109
|
+
file: file || `${dirPath}/${tableName}.jsonl`,
|
|
1110
|
+
tableName,
|
|
1111
|
+
rowIndex: 0,
|
|
1112
|
+
issues: [{
|
|
1113
|
+
message: message.replace(/^Warning:\s*/, ""),
|
|
1114
|
+
path: []
|
|
1115
|
+
}],
|
|
1116
|
+
type: "schema"
|
|
1117
|
+
});
|
|
1085
1118
|
}
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
errors.push({
|
|
1121
|
+
file: dirPath,
|
|
1122
|
+
tableName: "database",
|
|
1123
|
+
rowIndex: 0,
|
|
1124
|
+
issues: [{
|
|
1125
|
+
message: `Database initialization failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1126
|
+
path: []
|
|
1127
|
+
}],
|
|
1128
|
+
type: "schema"
|
|
1129
|
+
});
|
|
1130
|
+
} finally {
|
|
1131
|
+
console.warn = originalWarn;
|
|
1086
1132
|
}
|
|
1087
1133
|
return errors;
|
|
1088
1134
|
}
|
|
@@ -1106,6 +1152,11 @@ var Validator = class {
|
|
|
1106
1152
|
type: "schema"
|
|
1107
1153
|
});
|
|
1108
1154
|
}
|
|
1155
|
+
if (errors.length === 0) {
|
|
1156
|
+
const dirPath = dirname(filePath);
|
|
1157
|
+
const dbErrors = await this.validateWithDatabase(dirPath, [filePath]);
|
|
1158
|
+
errors.push(...dbErrors);
|
|
1159
|
+
}
|
|
1109
1160
|
return {
|
|
1110
1161
|
valid: errors.length === 0,
|
|
1111
1162
|
errors,
|