@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
|
@@ -0,0 +1,86 @@
|
|
|
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(dir: string, tableName: string): Promise<string | undefined> {
|
|
25
|
+
for (const ext of SCHEMA_EXTENSIONS) {
|
|
26
|
+
const candidate = join(dir, `${tableName}${ext}`);
|
|
27
|
+
try {
|
|
28
|
+
await access(candidate);
|
|
29
|
+
return candidate;
|
|
30
|
+
} catch {
|
|
31
|
+
// Continue to next extension
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Synchronously find a schema file among directory entries.
|
|
39
|
+
* Returns the full path of the first match, or undefined.
|
|
40
|
+
*/
|
|
41
|
+
export function findSchemaFileInEntries(
|
|
42
|
+
dataDirPath: string,
|
|
43
|
+
tableName: string,
|
|
44
|
+
entries: { isFile(): boolean; name: string }[],
|
|
45
|
+
): string | undefined {
|
|
46
|
+
for (const ext of SCHEMA_EXTENSIONS) {
|
|
47
|
+
const candidateName = `${tableName}${ext}`;
|
|
48
|
+
if (entries.some((e) => e.isFile() && e.name === candidateName)) {
|
|
49
|
+
return join(dataDirPath, candidateName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a filename matches any supported schema file pattern.
|
|
57
|
+
*/
|
|
58
|
+
export function isSchemaFile(fileName: string): boolean {
|
|
59
|
+
return SCHEMA_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract table name from a schema filename.
|
|
64
|
+
* e.g., "users.schema.ts" -> "users", "users.schema.mts" -> "users"
|
|
65
|
+
*/
|
|
66
|
+
export function extractTableNameFromSchemaFile(fileName: string): string | null {
|
|
67
|
+
for (const ext of SCHEMA_EXTENSIONS) {
|
|
68
|
+
if (fileName.endsWith(ext)) {
|
|
69
|
+
return fileName.slice(0, -ext.length);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Rewrite a TypeScript path to its JavaScript counterpart for ESM imports.
|
|
77
|
+
* ".schema.ts" -> ".schema.js", ".schema.mts" -> ".schema.mjs", ".schema.cts" -> ".schema.cjs"
|
|
78
|
+
*/
|
|
79
|
+
export function rewriteExtensionForImport(filePath: string): string {
|
|
80
|
+
for (const [tsExt, jsExt] of Object.entries(SCHEMA_TO_JS_IMPORT_MAP)) {
|
|
81
|
+
if (filePath.endsWith(tsExt)) {
|
|
82
|
+
return filePath.slice(0, -tsExt.length) + jsExt;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return filePath;
|
|
86
|
+
}
|
|
@@ -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,11 @@ 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)
|
|
34
|
+
? options.output
|
|
35
|
+
: join(this.projectRoot, options.output)
|
|
36
|
+
: join(this.dataDirPath, 'db.ts');
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
@@ -67,15 +73,11 @@ export class TypeGenerator {
|
|
|
67
73
|
for (const entry of entries) {
|
|
68
74
|
if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
69
75
|
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);
|
|
76
|
+
const schemaFilePath = findSchemaFileInEntries(this.dataDirPath, tableName, entries);
|
|
75
77
|
|
|
76
78
|
tables.push({
|
|
77
79
|
tableName,
|
|
78
|
-
schemaFile:
|
|
80
|
+
schemaFile: schemaFilePath,
|
|
79
81
|
});
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -108,9 +110,9 @@ export class TypeGenerator {
|
|
|
108
110
|
usedAliases.add(schemaIdentifier);
|
|
109
111
|
|
|
110
112
|
// Calculate relative path from output file to schema file
|
|
111
|
-
let relativePath =
|
|
112
|
-
.replace(/\\/g, '/') // Convert Windows paths to Unix-style
|
|
113
|
-
|
|
113
|
+
let relativePath = rewriteExtensionForImport(
|
|
114
|
+
relative(join(this.outputFile, '..'), table.schemaFile).replace(/\\/g, '/'), // Convert Windows paths to Unix-style
|
|
115
|
+
);
|
|
114
116
|
|
|
115
117
|
// Ensure relative path starts with './' or '../'
|
|
116
118
|
if (!relativePath.startsWith('.')) {
|
package/src/types.ts
CHANGED
|
@@ -18,10 +18,19 @@ export type InferInput<T> = T extends StandardSchemaV1<infer I, unknown> ? I : n
|
|
|
18
18
|
export type InferOutput<T> = T extends StandardSchemaV1<unknown, infer O> ? O : never;
|
|
19
19
|
|
|
20
20
|
// Validation result types
|
|
21
|
+
export interface TableValidationResult {
|
|
22
|
+
tableName: string;
|
|
23
|
+
valid: boolean;
|
|
24
|
+
rowCount: number;
|
|
25
|
+
errors: ValidationErrorDetail[];
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export interface ValidationResult {
|
|
22
30
|
valid: boolean;
|
|
23
31
|
errors: ValidationErrorDetail[];
|
|
24
32
|
warnings: string[];
|
|
33
|
+
tableResults: TableValidationResult[];
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
export interface ValidationErrorDetail {
|