@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.
@@ -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');
@@ -1,7 +1,7 @@
1
1
  import { pathToFileURL } from 'node:url';
2
- import { access } from 'node:fs/promises';
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 = join(dir, `${tableName}.schema.ts`);
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 = join(dir, `${tableName}.schema.ts`);
24
+ const schemaPath = await findSchemaFile(dir, tableName);
31
25
 
32
- try {
33
- await access(schemaPath);
34
- } catch (error) {
35
- throw new Error(`Schema file not found for table '${tableName}'. Expected: ${schemaPath}`, {
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 {
@@ -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 = join(this.dataDirPath, 'db.ts');
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 schemaFileName = `${tableName}.schema.ts`;
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: hasSchema ? schemaFilePath : undefined,
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 = relative(join(this.outputFile, '..'), table.schemaFile)
112
- .replace(/\\/g, '/') // Convert Windows paths to Unix-style
113
- .replace('.ts', '.js'); // Import from .js (TypeScript module resolution)
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 {