@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/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
- .action(async (dataDir: string) => {
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
- // Display warnings if any
109
- if (result.warnings.length > 0) {
110
- for (const warning of result.warnings) {
111
- 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
+ }
112
166
  }
113
- console.log('');
114
- }
115
167
 
116
- if (result.valid) {
117
- console.log('✓ All records are valid');
118
- 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
+ }
119
175
  } else {
120
- const formatter = new ErrorFormatter({ verbose: options.verbose });
121
-
122
- // Group errors by file for header
123
- const errorsByFile = new Map<string, typeof result.errors>();
124
- for (const error of result.errors) {
125
- const fileErrors = errorsByFile.get(error.file) || [];
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
- // Format and display errors
131
- for (const [file, fileErrors] of errorsByFile) {
132
- console.error(formatter.formatErrorHeader(fileErrors.length, file));
133
- console.error('');
134
-
135
- // Separate validation errors and foreign key errors
136
- const validationErrors = fileErrors.filter(
137
- (e) => e.type !== 'foreignKey' || !e.foreignKeyError,
138
- );
139
- const foreignKeyErrors = fileErrors.filter(
140
- (e) => e.type === 'foreignKey' && e.foreignKeyError,
141
- );
142
-
143
- // Format validation errors
144
- if (validationErrors.length > 0) {
145
- const formattedValidation = formatter.formatValidationErrors(
146
- validationErrors.map((e) => ({
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
- console.error(formattedValidation);
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
- // Format foreign key errors
156
- for (const error of foreignKeyErrors) {
157
- if (error.foreignKeyError) {
158
- const formattedFk = formatter.formatForeignKeyError({
159
- file: error.file,
160
- rowIndex: error.rowIndex,
161
- column: error.foreignKeyError.column,
162
- value: error.foreignKeyError.value,
163
- referencedTable: error.foreignKeyError.referencedTable,
164
- referencedColumn: error.foreignKeyError.referencedColumn,
165
- });
166
- 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
+ }
167
231
  }
232
+
233
+ console.error('');
168
234
  }
169
235
 
170
- console.error('');
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));
@@ -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 { errors, warnings } = await this.loadTableWithDependencies(
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<{ errors: ValidationErrorDetail[]; warnings: string[] }> {
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 = tableConfig.jsonlPath.replace('.jsonl', '.schema.ts');
152
- const schemaUrl = pathToFileURL(schemaPath).href;
153
- const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
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
- // Try to get foreign keys from exported 'schema' or directly from module
156
- const schemaExport = schemaModule.schema || schemaModule.default;
157
- foreignKeys = schemaExport?.foreignKeys || schemaModule.foreignKeys;
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 { loaded, errors: loadErrors } = await this.loadTable(
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
- ): Promise<{ loaded: boolean; errors: ValidationErrorDetail[] }> {
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 = config.jsonlPath.replace('.jsonl', '.schema.ts');
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 = 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
+ });