@toiroakr/lines-db 0.6.1 → 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/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 = tableConfig.jsonlPath.replace('.jsonl', '.schema.ts');
152
- const schemaUrl = pathToFileURL(schemaPath).href;
153
- const schemaModule = await import(`${schemaUrl}?t=${Date.now()}`);
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
- // 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;
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 = config.jsonlPath.replace('.jsonl', '.schema.ts');
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
 
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');
@@ -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,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 = join(this.dataDirPath, 'db.ts');
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 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);
74
+ const schemaFilePath = findSchemaFileInEntries(this.dataDirPath, tableName, entries);
75
75
 
76
76
  tables.push({
77
77
  tableName,
78
- schemaFile: hasSchema ? schemaFilePath : undefined,
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 = 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)
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('.')) {