@toiroakr/lines-db 0.6.0 → 0.7.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 +17 -0
- package/bin/cli.js +94 -28
- package/dist/index.cjs +109 -27
- package/dist/index.d.cts +30 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +30 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -2
- package/src/database.test.ts +28 -0
- package/src/database.ts +64 -8
- package/src/index.ts +8 -0
- package/src/schema-extensions.test.ts +155 -0
- package/src/schema-extensions.ts +89 -0
- package/src/schema-loader.test.ts +90 -0
- package/src/schema-loader.ts +10 -18
- package/src/type-generator.ts +11 -10
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,
|
|
@@ -148,13 +150,18 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
148
150
|
|
|
149
151
|
try {
|
|
150
152
|
const { pathToFileURL } = await import('node:url');
|
|
151
|
-
const schemaPath =
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const schemaPath = await findSchemaFile(
|
|
154
|
+
dirname(tableConfig.jsonlPath),
|
|
155
|
+
basename(tableConfig.jsonlPath, '.jsonl'),
|
|
156
|
+
);
|
|
157
|
+
if (schemaPath) {
|
|
158
|
+
const schemaUrl = pathToFileURL(schemaPath).href;
|
|
159
|
+
const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
|
|
154
160
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
// Try to get foreign keys from exported 'schema' or directly from module
|
|
162
|
+
const schemaExport = schemaModule.schema || schemaModule.default;
|
|
163
|
+
foreignKeys = schemaExport?.foreignKeys || schemaModule.foreignKeys;
|
|
164
|
+
}
|
|
158
165
|
} catch {
|
|
159
166
|
// Schema file not found - will continue without validation
|
|
160
167
|
}
|
|
@@ -256,7 +263,11 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
256
263
|
// Only load if not already provided via config
|
|
257
264
|
try {
|
|
258
265
|
const { pathToFileURL } = await import('node:url');
|
|
259
|
-
const schemaPath =
|
|
266
|
+
const schemaPath = await findSchemaFile(
|
|
267
|
+
dirname(config.jsonlPath),
|
|
268
|
+
basename(config.jsonlPath, '.jsonl'),
|
|
269
|
+
);
|
|
270
|
+
if (!schemaPath) throw new Error('Schema file not found');
|
|
260
271
|
const schemaUrl = pathToFileURL(schemaPath).href;
|
|
261
272
|
const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
|
|
262
273
|
|
|
@@ -280,8 +291,23 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
280
291
|
} else if (schemaModule.indexes) {
|
|
281
292
|
schemaMetadata.indexes = schemaModule.indexes;
|
|
282
293
|
}
|
|
294
|
+
|
|
295
|
+
// Debug: log loaded metadata
|
|
296
|
+
if (process.env.DEBUG_LINES_DB) {
|
|
297
|
+
console.log(`[lines-db] Schema metadata for ${tableName}:`);
|
|
298
|
+
console.log(` primaryKey: ${schemaMetadata.primaryKey}`);
|
|
299
|
+
console.log(` foreignKeys: ${JSON.stringify(schemaMetadata.foreignKeys)}`);
|
|
300
|
+
console.log(` indexes: ${JSON.stringify(schemaMetadata.indexes)}`);
|
|
301
|
+
}
|
|
283
302
|
} catch (_error) {
|
|
284
303
|
// Schema file not found - this is OK
|
|
304
|
+
// Debug: log error for investigation
|
|
305
|
+
if (process.env.DEBUG_LINES_DB) {
|
|
306
|
+
console.warn(
|
|
307
|
+
`[lines-db] Failed to load schema metadata for ${tableName}:`,
|
|
308
|
+
_error instanceof Error ? _error.message : String(_error),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
285
311
|
}
|
|
286
312
|
}
|
|
287
313
|
|
|
@@ -384,6 +410,18 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
384
410
|
}
|
|
385
411
|
if (indexes) {
|
|
386
412
|
schema.indexes = indexes;
|
|
413
|
+
|
|
414
|
+
// Apply unique constraint from single-column unique indexes to column definitions
|
|
415
|
+
// This is required for foreign key references, as SQLite requires the referenced column
|
|
416
|
+
// to have a UNIQUE constraint in the table definition (not just an index)
|
|
417
|
+
for (const index of indexes) {
|
|
418
|
+
if (index.unique && index.columns.length === 1) {
|
|
419
|
+
const col = schema.columns.find((c) => c.name === index.columns[0]);
|
|
420
|
+
if (col && !col.unique && !col.primaryKey) {
|
|
421
|
+
col.unique = true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
387
425
|
}
|
|
388
426
|
|
|
389
427
|
this.schemas.set(tableName, schema);
|
|
@@ -419,13 +457,31 @@ export class LinesDB<Tables extends TableDefs> {
|
|
|
419
457
|
// Quote table name to handle special characters
|
|
420
458
|
const quotedTableName = this.quoteTableName(schema.name);
|
|
421
459
|
|
|
460
|
+
// Build a set of columns that should have UNIQUE constraint
|
|
461
|
+
// This includes columns marked as unique in schema AND single-column unique indexes
|
|
462
|
+
// The latter is required for foreign key references, as SQLite requires the referenced column
|
|
463
|
+
// to have a UNIQUE constraint in the table definition (not just a separately created index)
|
|
464
|
+
const uniqueColumns = new Set<string>();
|
|
465
|
+
for (const col of schema.columns) {
|
|
466
|
+
if (col.unique) {
|
|
467
|
+
uniqueColumns.add(col.name);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (schema.indexes) {
|
|
471
|
+
for (const index of schema.indexes) {
|
|
472
|
+
if (index.unique && index.columns.length === 1) {
|
|
473
|
+
uniqueColumns.add(index.columns[0]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
422
478
|
const columnDefs = schema.columns.map((col) => {
|
|
423
479
|
// JSON type is stored as TEXT in SQLite
|
|
424
480
|
const sqlType = col.type === 'JSON' ? 'TEXT' : col.type;
|
|
425
481
|
const parts = [this.quoteIdentifier(col.name), sqlType];
|
|
426
482
|
if (col.primaryKey) parts.push('PRIMARY KEY');
|
|
427
483
|
if (col.notNull) parts.push('NOT NULL');
|
|
428
|
-
if (col.
|
|
484
|
+
if (uniqueColumns.has(col.name) && !col.primaryKey) parts.push('UNIQUE');
|
|
429
485
|
return parts.join(' ');
|
|
430
486
|
});
|
|
431
487
|
|
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';
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Supported schema file extensions, in priority order.
|
|
6
|
+
* The first match wins when discovering schema files.
|
|
7
|
+
*/
|
|
8
|
+
export const SCHEMA_EXTENSIONS = ['.schema.ts', '.schema.mts', '.schema.cts'] as const;
|
|
9
|
+
export type SchemaExtension = (typeof SCHEMA_EXTENSIONS)[number];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map from schema extensions to their JavaScript import counterparts.
|
|
13
|
+
*/
|
|
14
|
+
const SCHEMA_TO_JS_IMPORT_MAP: Record<string, string> = {
|
|
15
|
+
'.schema.ts': '.schema.js',
|
|
16
|
+
'.schema.mts': '.schema.mjs',
|
|
17
|
+
'.schema.cts': '.schema.cjs',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Try each supported schema extension and return the full path of the first
|
|
22
|
+
* one that exists on disk. Returns undefined if none is found.
|
|
23
|
+
*/
|
|
24
|
+
export async function findSchemaFile(
|
|
25
|
+
dir: string,
|
|
26
|
+
tableName: string,
|
|
27
|
+
): Promise<string | undefined> {
|
|
28
|
+
for (const ext of SCHEMA_EXTENSIONS) {
|
|
29
|
+
const candidate = join(dir, `${tableName}${ext}`);
|
|
30
|
+
try {
|
|
31
|
+
await access(candidate);
|
|
32
|
+
return candidate;
|
|
33
|
+
} catch {
|
|
34
|
+
// Continue to next extension
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Synchronously find a schema file among directory entries.
|
|
42
|
+
* Returns the full path of the first match, or undefined.
|
|
43
|
+
*/
|
|
44
|
+
export function findSchemaFileInEntries(
|
|
45
|
+
dataDirPath: string,
|
|
46
|
+
tableName: string,
|
|
47
|
+
entries: { isFile(): boolean; name: string }[],
|
|
48
|
+
): string | undefined {
|
|
49
|
+
for (const ext of SCHEMA_EXTENSIONS) {
|
|
50
|
+
const candidateName = `${tableName}${ext}`;
|
|
51
|
+
if (entries.some((e) => e.isFile() && e.name === candidateName)) {
|
|
52
|
+
return join(dataDirPath, candidateName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a filename matches any supported schema file pattern.
|
|
60
|
+
*/
|
|
61
|
+
export function isSchemaFile(fileName: string): boolean {
|
|
62
|
+
return SCHEMA_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract table name from a schema filename.
|
|
67
|
+
* e.g., "users.schema.ts" -> "users", "users.schema.mts" -> "users"
|
|
68
|
+
*/
|
|
69
|
+
export function extractTableNameFromSchemaFile(fileName: string): string | null {
|
|
70
|
+
for (const ext of SCHEMA_EXTENSIONS) {
|
|
71
|
+
if (fileName.endsWith(ext)) {
|
|
72
|
+
return fileName.slice(0, -ext.length);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rewrite a TypeScript path to its JavaScript counterpart for ESM imports.
|
|
80
|
+
* ".schema.ts" -> ".schema.js", ".schema.mts" -> ".schema.mjs", ".schema.cts" -> ".schema.cjs"
|
|
81
|
+
*/
|
|
82
|
+
export function rewriteExtensionForImport(filePath: string): string {
|
|
83
|
+
for (const [tsExt, jsExt] of Object.entries(SCHEMA_TO_JS_IMPORT_MAP)) {
|
|
84
|
+
if (filePath.endsWith(tsExt)) {
|
|
85
|
+
return filePath.slice(0, -tsExt.length) + jsExt;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return filePath;
|
|
89
|
+
}
|
|
@@ -65,6 +65,87 @@ describe('SchemaLoader', () => {
|
|
|
65
65
|
expect(schema['~standard'].vendor).toBe('default-export');
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it('should load schema from .mts file', async () => {
|
|
69
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
70
|
+
const schemaPath = join(testDir, 'users.schema.mts');
|
|
71
|
+
|
|
72
|
+
await writeFile(jsonlPath, '{"id":1}\n');
|
|
73
|
+
await writeFile(
|
|
74
|
+
schemaPath,
|
|
75
|
+
`
|
|
76
|
+
export const schema = {
|
|
77
|
+
'~standard': {
|
|
78
|
+
version: 1,
|
|
79
|
+
vendor: 'mts-test',
|
|
80
|
+
validate: () => ({ value: {} })
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const schema = await SchemaLoader.loadSchema(jsonlPath);
|
|
87
|
+
|
|
88
|
+
expect(schema).toBeDefined();
|
|
89
|
+
expect(schema['~standard'].vendor).toBe('mts-test');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should load schema from .cts file', async () => {
|
|
93
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
94
|
+
const schemaPath = join(testDir, 'users.schema.cts');
|
|
95
|
+
|
|
96
|
+
await writeFile(jsonlPath, '{"id":1}\n');
|
|
97
|
+
await writeFile(
|
|
98
|
+
schemaPath,
|
|
99
|
+
`
|
|
100
|
+
export const schema = {
|
|
101
|
+
'~standard': {
|
|
102
|
+
version: 1,
|
|
103
|
+
vendor: 'cts-test',
|
|
104
|
+
validate: () => ({ value: {} })
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const schema = await SchemaLoader.loadSchema(jsonlPath);
|
|
111
|
+
|
|
112
|
+
expect(schema).toBeDefined();
|
|
113
|
+
expect(schema['~standard'].vendor).toBe('cts-test');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should prefer .schema.ts over .schema.mts when both exist', async () => {
|
|
117
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
118
|
+
|
|
119
|
+
await writeFile(jsonlPath, '{"id":1}\n');
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(testDir, 'users.schema.ts'),
|
|
122
|
+
`
|
|
123
|
+
export const schema = {
|
|
124
|
+
'~standard': {
|
|
125
|
+
version: 1,
|
|
126
|
+
vendor: 'ts-priority',
|
|
127
|
+
validate: () => ({ value: {} })
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
`,
|
|
131
|
+
);
|
|
132
|
+
await writeFile(
|
|
133
|
+
join(testDir, 'users.schema.mts'),
|
|
134
|
+
`
|
|
135
|
+
export const schema = {
|
|
136
|
+
'~standard': {
|
|
137
|
+
version: 1,
|
|
138
|
+
vendor: 'mts-secondary',
|
|
139
|
+
validate: () => ({ value: {} })
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const schema = await SchemaLoader.loadSchema(jsonlPath);
|
|
146
|
+
expect(schema['~standard'].vendor).toBe('ts-priority');
|
|
147
|
+
});
|
|
148
|
+
|
|
68
149
|
it('should throw when no schema file exists', async () => {
|
|
69
150
|
const jsonlPath = join(testDir, 'users.jsonl');
|
|
70
151
|
await writeFile(jsonlPath, '{"id":1}\n');
|
|
@@ -72,6 +153,15 @@ describe('SchemaLoader', () => {
|
|
|
72
153
|
await expect(SchemaLoader.loadSchema(jsonlPath)).rejects.toThrow(/Schema file not found/);
|
|
73
154
|
});
|
|
74
155
|
|
|
156
|
+
it('should list all supported extensions in error message', async () => {
|
|
157
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
158
|
+
await writeFile(jsonlPath, '{"id":1}\n');
|
|
159
|
+
|
|
160
|
+
await expect(SchemaLoader.loadSchema(jsonlPath)).rejects.toThrow(
|
|
161
|
+
/\.schema\.ts.*\.schema\.mts.*\.schema\.cts/,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
75
165
|
it('should throw for invalid schema export', async () => {
|
|
76
166
|
const jsonlPath = join(testDir, 'users.jsonl');
|
|
77
167
|
const schemaPath = join(testDir, 'users.schema.ts');
|
package/src/schema-loader.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join, basename } from 'node:path';
|
|
2
|
+
import { dirname, basename } from 'node:path';
|
|
4
3
|
import type { StandardSchema } from './types.js';
|
|
4
|
+
import { findSchemaFile, SCHEMA_EXTENSIONS } from './schema-extensions.js';
|
|
5
5
|
|
|
6
6
|
export class SchemaLoader {
|
|
7
7
|
/**
|
|
@@ -10,31 +10,23 @@ export class SchemaLoader {
|
|
|
10
10
|
static async hasSchema(jsonlPath: string): Promise<boolean> {
|
|
11
11
|
const dir = dirname(jsonlPath);
|
|
12
12
|
const tableName = basename(jsonlPath, '.jsonl');
|
|
13
|
-
const schemaPath =
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
await access(schemaPath);
|
|
17
|
-
return true;
|
|
18
|
-
} catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
13
|
+
const schemaPath = await findSchemaFile(dir, tableName);
|
|
14
|
+
return schemaPath !== undefined;
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
/**
|
|
24
18
|
* Load a validation schema file for a table
|
|
25
|
-
* Requires ${tableName}.schema.ts to exist alongside the JSONL file
|
|
19
|
+
* Requires ${tableName}.schema.{ts,mts,cts} to exist alongside the JSONL file
|
|
26
20
|
*/
|
|
27
21
|
static async loadSchema(jsonlPath: string): Promise<StandardSchema> {
|
|
28
22
|
const dir = dirname(jsonlPath);
|
|
29
23
|
const tableName = basename(jsonlPath, '.jsonl');
|
|
30
|
-
const schemaPath =
|
|
24
|
+
const schemaPath = await findSchemaFile(dir, tableName);
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cause: error instanceof Error ? error : undefined,
|
|
37
|
-
});
|
|
26
|
+
if (!schemaPath) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Schema file not found for table '${tableName}'. Expected one of: ${SCHEMA_EXTENSIONS.map((ext) => `${tableName}${ext}`).join(', ')}`,
|
|
29
|
+
);
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
try {
|
package/src/type-generator.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
2
|
import { join, relative, basename, dirname, isAbsolute } from 'node:path';
|
|
3
3
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { findSchemaFileInEntries, rewriteExtensionForImport } from './schema-extensions.js';
|
|
4
5
|
|
|
5
6
|
export interface TypeGeneratorOptions {
|
|
6
7
|
dataDir: string;
|
|
7
8
|
projectRoot?: string; // Default: current working directory
|
|
9
|
+
output?: string; // Output file path (default: db.ts in dataDir)
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
interface TableInfo {
|
|
@@ -27,7 +29,9 @@ export class TypeGenerator {
|
|
|
27
29
|
this.dataDirPath = isAbsolute(this.dataDir)
|
|
28
30
|
? this.dataDir
|
|
29
31
|
: join(this.projectRoot, this.dataDir);
|
|
30
|
-
this.outputFile =
|
|
32
|
+
this.outputFile = options.output
|
|
33
|
+
? (isAbsolute(options.output) ? options.output : join(this.projectRoot, options.output))
|
|
34
|
+
: join(this.dataDirPath, 'db.ts');
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -67,15 +71,11 @@ export class TypeGenerator {
|
|
|
67
71
|
for (const entry of entries) {
|
|
68
72
|
if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
69
73
|
const tableName = basename(entry.name, '.jsonl');
|
|
70
|
-
const
|
|
71
|
-
const schemaFilePath = join(this.dataDirPath, schemaFileName);
|
|
72
|
-
|
|
73
|
-
// Check if schema file exists
|
|
74
|
-
const hasSchema = entries.some((e) => e.isFile() && e.name === schemaFileName);
|
|
74
|
+
const schemaFilePath = findSchemaFileInEntries(this.dataDirPath, tableName, entries);
|
|
75
75
|
|
|
76
76
|
tables.push({
|
|
77
77
|
tableName,
|
|
78
|
-
schemaFile:
|
|
78
|
+
schemaFile: schemaFilePath,
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -108,9 +108,10 @@ export class TypeGenerator {
|
|
|
108
108
|
usedAliases.add(schemaIdentifier);
|
|
109
109
|
|
|
110
110
|
// Calculate relative path from output file to schema file
|
|
111
|
-
let relativePath =
|
|
112
|
-
.
|
|
113
|
-
|
|
111
|
+
let relativePath = rewriteExtensionForImport(
|
|
112
|
+
relative(join(this.outputFile, '..'), table.schemaFile)
|
|
113
|
+
.replace(/\\/g, '/'), // Convert Windows paths to Unix-style
|
|
114
|
+
);
|
|
114
115
|
|
|
115
116
|
// Ensure relative path starts with './' or '../'
|
|
116
117
|
if (!relativePath.startsWith('.')) {
|