@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 +27 -0
- package/bin/cli.js +163 -59
- package/dist/index.cjs +126 -31
- package/dist/index.d.cts +38 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +38 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +121 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +121 -57
- package/src/database.test.ts +28 -0
- package/src/database.ts +89 -18
- package/src/index.ts +9 -0
- package/src/schema-extensions.test.ts +155 -0
- package/src/schema-extensions.ts +86 -0
- package/src/schema-loader.test.ts +90 -0
- package/src/schema-loader.ts +10 -18
- package/src/type-generator.ts +12 -10
- package/src/types.ts +9 -0
package/src/cli.ts
CHANGED
|
@@ -62,9 +62,10 @@ program
|
|
|
62
62
|
.command('generate')
|
|
63
63
|
.description('Generate TypeScript type definitions from schema files')
|
|
64
64
|
.argument('<dataDir>', 'Directory containing JSONL and schema files')
|
|
65
|
-
.
|
|
65
|
+
.option('-o, --output <path>', 'Output file path (default: db.ts in dataDir)')
|
|
66
|
+
.action(async (dataDir: string, options: { output?: string }) => {
|
|
66
67
|
try {
|
|
67
|
-
const generator = new TypeGenerator({ dataDir });
|
|
68
|
+
const generator = new TypeGenerator({ dataDir, output: options.output });
|
|
68
69
|
await generator.generate();
|
|
69
70
|
console.log('Type generation completed successfully!');
|
|
70
71
|
} catch (error) {
|
|
@@ -105,72 +106,135 @@ program
|
|
|
105
106
|
await db.close();
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
//
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
166
|
}
|
|
113
|
-
console.log('');
|
|
114
|
-
}
|
|
115
167
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
175
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
fileErrors.push(error);
|
|
127
|
-
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('');
|
|
128
182
|
}
|
|
129
183
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
file: e.file,
|
|
148
|
-
rowIndex: e.rowIndex,
|
|
149
|
-
issues: e.issues,
|
|
150
|
-
})),
|
|
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,
|
|
151
201
|
);
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
217
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
231
|
}
|
|
232
|
+
|
|
233
|
+
console.error('');
|
|
168
234
|
}
|
|
169
235
|
|
|
170
|
-
|
|
236
|
+
process.exit(1);
|
|
171
237
|
}
|
|
172
|
-
|
|
173
|
-
process.exit(1);
|
|
174
238
|
}
|
|
175
239
|
} catch (error) {
|
|
176
240
|
console.error('Error:', error instanceof Error ? error.message : String(error));
|
package/src/database.test.ts
CHANGED
|
@@ -47,6 +47,34 @@ export const schema = defineSchema(rawSchema);
|
|
|
47
47
|
await db.close();
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
it('should load table with .schema.mts schema file', async () => {
|
|
51
|
+
await writeFile(join(testDir, 'users.jsonl'), '{"id":1,"name":"Alice"}\n');
|
|
52
|
+
await writeFile(join(testDir, 'users.schema.mts'), GENERIC_SCHEMA_SOURCE);
|
|
53
|
+
|
|
54
|
+
const config: DatabaseConfig = { dataDir: testDir };
|
|
55
|
+
const db = LinesDB.create(config);
|
|
56
|
+
await db.initialize();
|
|
57
|
+
|
|
58
|
+
const tables = db.getTableNames();
|
|
59
|
+
expect(tables).toContain('users');
|
|
60
|
+
|
|
61
|
+
await db.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should load table with .schema.cts schema file', async () => {
|
|
65
|
+
await writeFile(join(testDir, 'users.jsonl'), '{"id":1,"name":"Alice"}\n');
|
|
66
|
+
await writeFile(join(testDir, 'users.schema.cts'), GENERIC_SCHEMA_SOURCE);
|
|
67
|
+
|
|
68
|
+
const config: DatabaseConfig = { dataDir: testDir };
|
|
69
|
+
const db = LinesDB.create(config);
|
|
70
|
+
await db.initialize();
|
|
71
|
+
|
|
72
|
+
const tables = db.getTableNames();
|
|
73
|
+
expect(tables).toContain('users');
|
|
74
|
+
|
|
75
|
+
await db.close();
|
|
76
|
+
});
|
|
77
|
+
|
|
50
78
|
it('should load multiple tables', async () => {
|
|
51
79
|
await writeTable('users', '{"id":1}\n');
|
|
52
80
|
await writeTable('products', '{"id":1}\n');
|
package/src/database.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { JsonlWriter } from './jsonl-writer.js';
|
|
|
4
4
|
import { SchemaLoader } from './schema-loader.js';
|
|
5
5
|
import { DirectoryScanner } from './directory-scanner.js';
|
|
6
6
|
import { hasBackward } from './schema.js';
|
|
7
|
+
import { findSchemaFile } from './schema-extensions.js';
|
|
8
|
+
import { dirname, basename } from 'node:path';
|
|
7
9
|
import type {
|
|
8
10
|
DatabaseConfig,
|
|
9
11
|
TableSchema,
|
|
@@ -16,6 +18,7 @@ import type {
|
|
|
16
18
|
WhereCondition,
|
|
17
19
|
ValidationResult,
|
|
18
20
|
ValidationErrorDetail,
|
|
21
|
+
TableValidationResult,
|
|
19
22
|
ForeignKeyDefinition,
|
|
20
23
|
} from './types.js';
|
|
21
24
|
import type { BiDirectionalSchema } from './schema.js';
|
|
@@ -56,6 +59,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
56
59
|
}): Promise<ValidationResult> {
|
|
57
60
|
const allErrors: ValidationErrorDetail[] = [];
|
|
58
61
|
const allWarnings: string[] = [];
|
|
62
|
+
const allRowCounts = new Map<string, number>();
|
|
59
63
|
const tableName = options?.tableName;
|
|
60
64
|
const detailedValidate = options?.detailedValidate ?? false;
|
|
61
65
|
const transform = options?.transform;
|
|
@@ -85,7 +89,11 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
85
89
|
if (!attemptedTables.has(tableNameToLoad)) {
|
|
86
90
|
// Only apply transform to the specified table
|
|
87
91
|
const tableTransform = tableNameToLoad === tableName ? transform : undefined;
|
|
88
|
-
const {
|
|
92
|
+
const {
|
|
93
|
+
errors,
|
|
94
|
+
warnings,
|
|
95
|
+
rowCounts: tableRowCounts,
|
|
96
|
+
} = await this.loadTableWithDependencies(
|
|
89
97
|
tableNameToLoad,
|
|
90
98
|
loadedTables,
|
|
91
99
|
loadingTables,
|
|
@@ -95,13 +103,30 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
95
103
|
);
|
|
96
104
|
allErrors.push(...errors);
|
|
97
105
|
allWarnings.push(...warnings);
|
|
106
|
+
for (const [k, v] of tableRowCounts) {
|
|
107
|
+
allRowCounts.set(k, v);
|
|
108
|
+
}
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
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
|
+
|
|
101
125
|
return {
|
|
102
126
|
valid: allErrors.length === 0,
|
|
103
127
|
errors: allErrors,
|
|
104
128
|
warnings: allWarnings,
|
|
129
|
+
tableResults,
|
|
105
130
|
};
|
|
106
131
|
}
|
|
107
132
|
|
|
@@ -115,13 +140,18 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
115
140
|
attemptedTables: Set<string>,
|
|
116
141
|
detailedValidate: boolean,
|
|
117
142
|
transform?: (row: JsonObject) => JsonObject,
|
|
118
|
-
): Promise<{
|
|
143
|
+
): Promise<{
|
|
144
|
+
errors: ValidationErrorDetail[];
|
|
145
|
+
warnings: string[];
|
|
146
|
+
rowCounts: Map<string, number>;
|
|
147
|
+
}> {
|
|
119
148
|
const errors: ValidationErrorDetail[] = [];
|
|
120
149
|
const warnings: string[] = [];
|
|
150
|
+
const rowCounts = new Map<string, number>();
|
|
121
151
|
|
|
122
152
|
// Skip if already attempted (loaded or not)
|
|
123
153
|
if (attemptedTables.has(tableName)) {
|
|
124
|
-
return { errors, warnings };
|
|
154
|
+
return { errors, warnings, rowCounts };
|
|
125
155
|
}
|
|
126
156
|
|
|
127
157
|
// Mark as attempted
|
|
@@ -148,13 +178,18 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
148
178
|
|
|
149
179
|
try {
|
|
150
180
|
const { pathToFileURL } = await import('node:url');
|
|
151
|
-
const schemaPath =
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
const schemaPath = await findSchemaFile(
|
|
182
|
+
dirname(tableConfig.jsonlPath),
|
|
183
|
+
basename(tableConfig.jsonlPath, '.jsonl'),
|
|
184
|
+
);
|
|
185
|
+
if (schemaPath) {
|
|
186
|
+
const schemaUrl = pathToFileURL(schemaPath).href;
|
|
187
|
+
const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
|
|
154
188
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
// Try to get foreign keys from exported 'schema' or directly from module
|
|
190
|
+
const schemaExport = schemaModule.schema || schemaModule.default;
|
|
191
|
+
foreignKeys = schemaExport?.foreignKeys || schemaModule.foreignKeys;
|
|
192
|
+
}
|
|
158
193
|
} catch {
|
|
159
194
|
// Schema file not found - will continue without validation
|
|
160
195
|
}
|
|
@@ -183,6 +218,9 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
183
218
|
);
|
|
184
219
|
errors.push(...depResult.errors);
|
|
185
220
|
warnings.push(...depResult.warnings);
|
|
221
|
+
for (const [k, v] of depResult.rowCounts) {
|
|
222
|
+
rowCounts.set(k, v);
|
|
223
|
+
}
|
|
186
224
|
} else {
|
|
187
225
|
throw new Error(
|
|
188
226
|
`Foreign key reference to non-existent table '${referencedTable}' in table '${tableName}'`,
|
|
@@ -192,14 +230,39 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
192
230
|
}
|
|
193
231
|
}
|
|
194
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
|
+
|
|
195
252
|
// Now load this table
|
|
196
|
-
const {
|
|
253
|
+
const {
|
|
254
|
+
loaded,
|
|
255
|
+
rowCount,
|
|
256
|
+
errors: loadErrors,
|
|
257
|
+
} = await this.loadTable(
|
|
197
258
|
tableName,
|
|
198
259
|
tableConfig,
|
|
199
260
|
detailedValidate,
|
|
200
261
|
transform,
|
|
262
|
+
failedDependencies,
|
|
201
263
|
);
|
|
202
264
|
errors.push(...loadErrors);
|
|
265
|
+
rowCounts.set(tableName, rowCount);
|
|
203
266
|
|
|
204
267
|
if (loaded) {
|
|
205
268
|
loadedTables.add(tableName);
|
|
@@ -213,7 +276,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
213
276
|
loadingTables.delete(tableName);
|
|
214
277
|
}
|
|
215
278
|
|
|
216
|
-
return { errors, warnings };
|
|
279
|
+
return { errors, warnings, rowCounts };
|
|
217
280
|
}
|
|
218
281
|
|
|
219
282
|
/**
|
|
@@ -225,7 +288,8 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
225
288
|
config: TableConfig,
|
|
226
289
|
detailedValidate: boolean,
|
|
227
290
|
transform?: (row: JsonObject) => JsonObject,
|
|
228
|
-
|
|
291
|
+
failedDependencies?: Set<string>,
|
|
292
|
+
): Promise<{ loaded: boolean; rowCount: number; errors: ValidationErrorDetail[] }> {
|
|
229
293
|
// Read JSONL file
|
|
230
294
|
let data = await JsonlReader.read(config.jsonlPath);
|
|
231
295
|
|
|
@@ -256,7 +320,11 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
256
320
|
// Only load if not already provided via config
|
|
257
321
|
try {
|
|
258
322
|
const { pathToFileURL } = await import('node:url');
|
|
259
|
-
const schemaPath =
|
|
323
|
+
const schemaPath = await findSchemaFile(
|
|
324
|
+
dirname(config.jsonlPath),
|
|
325
|
+
basename(config.jsonlPath, '.jsonl'),
|
|
326
|
+
);
|
|
327
|
+
if (!schemaPath) throw new Error('Schema file not found');
|
|
260
328
|
const schemaUrl = pathToFileURL(schemaPath).href;
|
|
261
329
|
const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
|
|
262
330
|
|
|
@@ -339,7 +407,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
339
407
|
|
|
340
408
|
if (validationErrors.length > 0) {
|
|
341
409
|
// Return errors instead of throwing
|
|
342
|
-
return { loaded: false, errors: validationErrorDetails };
|
|
410
|
+
return { loaded: false, rowCount: data.length, errors: validationErrorDetails };
|
|
343
411
|
}
|
|
344
412
|
|
|
345
413
|
// Determine schema - infer from validated data if auto-inference is enabled
|
|
@@ -364,7 +432,7 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
364
432
|
}
|
|
365
433
|
} else if (config.autoInferSchema !== false) {
|
|
366
434
|
if (validatedData.length === 0) {
|
|
367
|
-
return { loaded: false, errors: [] };
|
|
435
|
+
return { loaded: false, rowCount: 0, errors: [] };
|
|
368
436
|
}
|
|
369
437
|
// Use inferred schema
|
|
370
438
|
schema = inferredSchema!;
|
|
@@ -395,7 +463,10 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
395
463
|
}
|
|
396
464
|
}
|
|
397
465
|
if (foreignKeys) {
|
|
398
|
-
schema.foreignKeys =
|
|
466
|
+
schema.foreignKeys =
|
|
467
|
+
failedDependencies && failedDependencies.size > 0
|
|
468
|
+
? foreignKeys.filter((fk) => !failedDependencies.has(fk.references.table))
|
|
469
|
+
: foreignKeys;
|
|
399
470
|
}
|
|
400
471
|
if (indexes) {
|
|
401
472
|
schema.indexes = indexes;
|
|
@@ -427,13 +498,13 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
427
498
|
config.jsonlPath,
|
|
428
499
|
);
|
|
429
500
|
if (insertErrors.length > 0) {
|
|
430
|
-
return { loaded: false, errors: insertErrors };
|
|
501
|
+
return { loaded: false, rowCount: data.length, errors: insertErrors };
|
|
431
502
|
}
|
|
432
503
|
} else {
|
|
433
504
|
this.insertData(tableName, schema, validatedData);
|
|
434
505
|
}
|
|
435
506
|
|
|
436
|
-
return { loaded: true, errors: [] };
|
|
507
|
+
return { loaded: true, rowCount: data.length, errors: [] };
|
|
437
508
|
}
|
|
438
509
|
|
|
439
510
|
/**
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,14 @@ export { defineSchema, hasBackward } from './schema.js';
|
|
|
7
7
|
export { TypeGenerator } from './type-generator.js';
|
|
8
8
|
export { ensureTableRowsValid } from './jsonl-migration.js';
|
|
9
9
|
export type { TableValidationOptions } from './jsonl-migration.js';
|
|
10
|
+
export {
|
|
11
|
+
SCHEMA_EXTENSIONS,
|
|
12
|
+
findSchemaFile,
|
|
13
|
+
isSchemaFile,
|
|
14
|
+
extractTableNameFromSchemaFile,
|
|
15
|
+
rewriteExtensionForImport,
|
|
16
|
+
} from './schema-extensions.js';
|
|
17
|
+
export type { SchemaExtension } from './schema-extensions.js';
|
|
10
18
|
export { detectRuntime, RUNTIME } from './runtime.js';
|
|
11
19
|
export type { RuntimeEnvironment } from './runtime.js';
|
|
12
20
|
export type { SQLiteDatabase, SQLiteStatement } from './sqlite-adapter.js';
|
|
@@ -24,6 +32,7 @@ export type {
|
|
|
24
32
|
StandardSchemaIssue,
|
|
25
33
|
ValidationError,
|
|
26
34
|
ValidationResult,
|
|
35
|
+
TableValidationResult,
|
|
27
36
|
ValidationErrorDetail,
|
|
28
37
|
InferInput,
|
|
29
38
|
InferOutput,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isSchemaFile,
|
|
4
|
+
extractTableNameFromSchemaFile,
|
|
5
|
+
rewriteExtensionForImport,
|
|
6
|
+
findSchemaFile,
|
|
7
|
+
findSchemaFileInEntries,
|
|
8
|
+
SCHEMA_EXTENSIONS,
|
|
9
|
+
} from './schema-extensions.js';
|
|
10
|
+
import { writeFile, mkdir, rm } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
describe('schema-extensions', () => {
|
|
15
|
+
describe('isSchemaFile', () => {
|
|
16
|
+
it('should recognize .schema.ts', () => {
|
|
17
|
+
expect(isSchemaFile('users.schema.ts')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should recognize .schema.mts', () => {
|
|
21
|
+
expect(isSchemaFile('users.schema.mts')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should recognize .schema.cts', () => {
|
|
25
|
+
expect(isSchemaFile('users.schema.cts')).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should reject non-schema files', () => {
|
|
29
|
+
expect(isSchemaFile('users.ts')).toBe(false);
|
|
30
|
+
expect(isSchemaFile('db.ts')).toBe(false);
|
|
31
|
+
expect(isSchemaFile('schema.ts')).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('extractTableNameFromSchemaFile', () => {
|
|
36
|
+
it('should extract from .schema.ts', () => {
|
|
37
|
+
expect(extractTableNameFromSchemaFile('users.schema.ts')).toBe('users');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should extract from .schema.mts', () => {
|
|
41
|
+
expect(extractTableNameFromSchemaFile('users.schema.mts')).toBe('users');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should extract from .schema.cts', () => {
|
|
45
|
+
expect(extractTableNameFromSchemaFile('users.schema.cts')).toBe('users');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return null for non-schema files', () => {
|
|
49
|
+
expect(extractTableNameFromSchemaFile('users.ts')).toBeNull();
|
|
50
|
+
expect(extractTableNameFromSchemaFile('db.ts')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('rewriteExtensionForImport', () => {
|
|
55
|
+
it('.schema.ts -> .schema.js', () => {
|
|
56
|
+
expect(rewriteExtensionForImport('./users.schema.ts')).toBe('./users.schema.js');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('.schema.mts -> .schema.mjs', () => {
|
|
60
|
+
expect(rewriteExtensionForImport('./users.schema.mts')).toBe('./users.schema.mjs');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('.schema.cts -> .schema.cjs', () => {
|
|
64
|
+
expect(rewriteExtensionForImport('./users.schema.cts')).toBe('./users.schema.cjs');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should not modify paths without matching extensions', () => {
|
|
68
|
+
expect(rewriteExtensionForImport('./users.js')).toBe('./users.js');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('findSchemaFile', () => {
|
|
73
|
+
let testDir: string;
|
|
74
|
+
|
|
75
|
+
beforeEach(async () => {
|
|
76
|
+
testDir = join(tmpdir(), `schema-ext-test-${Date.now()}`);
|
|
77
|
+
await mkdir(testDir, { recursive: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
await rm(testDir, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should find .schema.ts file', async () => {
|
|
85
|
+
await writeFile(join(testDir, 'users.schema.ts'), 'export const schema = {};');
|
|
86
|
+
const result = await findSchemaFile(testDir, 'users');
|
|
87
|
+
expect(result).toBe(join(testDir, 'users.schema.ts'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should find .schema.mts file', async () => {
|
|
91
|
+
await writeFile(join(testDir, 'users.schema.mts'), 'export const schema = {};');
|
|
92
|
+
const result = await findSchemaFile(testDir, 'users');
|
|
93
|
+
expect(result).toBe(join(testDir, 'users.schema.mts'));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should find .schema.cts file', async () => {
|
|
97
|
+
await writeFile(join(testDir, 'users.schema.cts'), 'export const schema = {};');
|
|
98
|
+
const result = await findSchemaFile(testDir, 'users');
|
|
99
|
+
expect(result).toBe(join(testDir, 'users.schema.cts'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should prefer .schema.ts over .schema.mts', async () => {
|
|
103
|
+
await writeFile(join(testDir, 'users.schema.ts'), 'export const schema = {};');
|
|
104
|
+
await writeFile(join(testDir, 'users.schema.mts'), 'export const schema = {};');
|
|
105
|
+
const result = await findSchemaFile(testDir, 'users');
|
|
106
|
+
expect(result).toBe(join(testDir, 'users.schema.ts'));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return undefined when no schema file exists', async () => {
|
|
110
|
+
const result = await findSchemaFile(testDir, 'users');
|
|
111
|
+
expect(result).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('findSchemaFileInEntries', () => {
|
|
116
|
+
it('should find .schema.ts in entries', () => {
|
|
117
|
+
const entries = [
|
|
118
|
+
{ isFile: () => true, name: 'users.jsonl' },
|
|
119
|
+
{ isFile: () => true, name: 'users.schema.ts' },
|
|
120
|
+
];
|
|
121
|
+
const result = findSchemaFileInEntries('/data', 'users', entries);
|
|
122
|
+
expect(result).toBe(join('/data', 'users.schema.ts'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should find .schema.mts in entries', () => {
|
|
126
|
+
const entries = [
|
|
127
|
+
{ isFile: () => true, name: 'users.jsonl' },
|
|
128
|
+
{ isFile: () => true, name: 'users.schema.mts' },
|
|
129
|
+
];
|
|
130
|
+
const result = findSchemaFileInEntries('/data', 'users', entries);
|
|
131
|
+
expect(result).toBe(join('/data', 'users.schema.mts'));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should prefer .schema.ts over .schema.mts in entries', () => {
|
|
135
|
+
const entries = [
|
|
136
|
+
{ isFile: () => true, name: 'users.schema.ts' },
|
|
137
|
+
{ isFile: () => true, name: 'users.schema.mts' },
|
|
138
|
+
];
|
|
139
|
+
const result = findSchemaFileInEntries('/data', 'users', entries);
|
|
140
|
+
expect(result).toBe(join('/data', 'users.schema.ts'));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return undefined when no schema file in entries', () => {
|
|
144
|
+
const entries = [{ isFile: () => true, name: 'users.jsonl' }];
|
|
145
|
+
const result = findSchemaFileInEntries('/data', 'users', entries);
|
|
146
|
+
expect(result).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('SCHEMA_EXTENSIONS', () => {
|
|
151
|
+
it('should have correct priority order', () => {
|
|
152
|
+
expect(SCHEMA_EXTENSIONS).toEqual(['.schema.ts', '.schema.mts', '.schema.cts']);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|