@toiroakr/lines-db 0.1.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/LICENSE +21 -0
- package/bin/cli.js +1373 -0
- package/dist/index.cjs +1212 -0
- package/dist/index.d.cts +486 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +486 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1181 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/cli.ts +333 -0
- package/src/database.test.ts +493 -0
- package/src/database.ts +1025 -0
- package/src/directory-scanner.test.ts +91 -0
- package/src/directory-scanner.ts +38 -0
- package/src/error-formatter.ts +166 -0
- package/src/index.ts +35 -0
- package/src/jsonl-migration.ts +76 -0
- package/src/jsonl-reader.test.ts +168 -0
- package/src/jsonl-reader.ts +135 -0
- package/src/jsonl-writer.test.ts +101 -0
- package/src/jsonl-writer.ts +33 -0
- package/src/runtime.ts +34 -0
- package/src/schema-loader.test.ts +136 -0
- package/src/schema-loader.ts +64 -0
- package/src/schema.ts +135 -0
- package/src/sqlite-adapter.ts +99 -0
- package/src/type-generator.ts +201 -0
- package/src/types.ts +99 -0
- package/src/validator.test.ts +337 -0
- package/src/validator.ts +207 -0
- package/tsconfig.json +20 -0
- package/tsconfig.test.json +8 -0
- package/tsdown.config.ts +26 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite adapter that works across Node.js, Bun, and Deno
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { RUNTIME } from './runtime.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Common interface for SQLite database
|
|
9
|
+
*/
|
|
10
|
+
export interface SQLiteDatabase {
|
|
11
|
+
prepare(sql: string): SQLiteStatement;
|
|
12
|
+
exec(sql: string): void;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SQLiteStatement {
|
|
17
|
+
run(...params: any[]): { changes: number; lastInsertRowid: number | bigint };
|
|
18
|
+
get(...params: any[]): any;
|
|
19
|
+
all(...params: any[]): any[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a SQLite database instance based on the runtime environment
|
|
24
|
+
*/
|
|
25
|
+
export function createDatabase(path: string = ':memory:'): SQLiteDatabase {
|
|
26
|
+
if (RUNTIME === 'bun') {
|
|
27
|
+
return createBunDatabase(path);
|
|
28
|
+
} else if (RUNTIME === 'node' || RUNTIME === 'deno') {
|
|
29
|
+
return createNodeDatabase(path);
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(`Unsupported runtime: ${RUNTIME}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a Bun SQLite database
|
|
37
|
+
*/
|
|
38
|
+
function createBunDatabase(path: string): SQLiteDatabase {
|
|
39
|
+
// Dynamic import for Bun
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const { Database } = require('bun:sqlite');
|
|
42
|
+
const db = new Database(path);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
prepare(sql: string): SQLiteStatement {
|
|
46
|
+
const stmt = db.prepare(sql);
|
|
47
|
+
return {
|
|
48
|
+
run(...params: any[]) {
|
|
49
|
+
return stmt.run(...params);
|
|
50
|
+
},
|
|
51
|
+
get(...params: any[]) {
|
|
52
|
+
return stmt.get(...params);
|
|
53
|
+
},
|
|
54
|
+
all(...params: any[]) {
|
|
55
|
+
return stmt.all(...params);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
exec(sql: string): void {
|
|
60
|
+
db.exec(sql);
|
|
61
|
+
},
|
|
62
|
+
close(): void {
|
|
63
|
+
db.close();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a Node.js SQLite database
|
|
70
|
+
*/
|
|
71
|
+
function createNodeDatabase(path: string): SQLiteDatabase {
|
|
72
|
+
// Dynamic import for Node.js
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
74
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
75
|
+
const db = new DatabaseSync(path);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
prepare(sql: string): SQLiteStatement {
|
|
79
|
+
const stmt = db.prepare(sql);
|
|
80
|
+
return {
|
|
81
|
+
run(...params: any[]) {
|
|
82
|
+
return stmt.run(...params);
|
|
83
|
+
},
|
|
84
|
+
get(...params: any[]) {
|
|
85
|
+
return stmt.get(...params);
|
|
86
|
+
},
|
|
87
|
+
all(...params: any[]) {
|
|
88
|
+
return stmt.all(...params);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
exec(sql: string): void {
|
|
93
|
+
db.exec(sql);
|
|
94
|
+
},
|
|
95
|
+
close(): void {
|
|
96
|
+
db.close();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, basename, dirname, isAbsolute } from 'node:path';
|
|
3
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
export interface TypeGeneratorOptions {
|
|
6
|
+
dataDir: string;
|
|
7
|
+
projectRoot?: string; // Default: current working directory
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TableInfo {
|
|
11
|
+
tableName: string;
|
|
12
|
+
schemaFile?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TypeGenerator {
|
|
16
|
+
private dataDir: string;
|
|
17
|
+
private projectRoot: string;
|
|
18
|
+
private outputFile: string;
|
|
19
|
+
private dataDirPath: string;
|
|
20
|
+
|
|
21
|
+
constructor(options: TypeGeneratorOptions) {
|
|
22
|
+
// For testing: allow overriding projectRoot via environment variable
|
|
23
|
+
const envProjectRoot = process.env.LINES_DB_TEST_PROJECT_ROOT;
|
|
24
|
+
this.projectRoot =
|
|
25
|
+
envProjectRoot !== undefined ? envProjectRoot : options.projectRoot || process.cwd();
|
|
26
|
+
this.dataDir = options.dataDir;
|
|
27
|
+
this.dataDirPath = isAbsolute(this.dataDir)
|
|
28
|
+
? this.dataDir
|
|
29
|
+
: join(this.projectRoot, this.dataDir);
|
|
30
|
+
this.outputFile = join(this.dataDirPath, 'db.ts');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate types file from JSONL files and their optional schema files.
|
|
35
|
+
*/
|
|
36
|
+
async generate(): Promise<string> {
|
|
37
|
+
// Find all JSONL files and their corresponding schema files
|
|
38
|
+
const tables = await this.findTables();
|
|
39
|
+
|
|
40
|
+
if (tables.length === 0) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`No JSONL files found in ${this.dataDirPath}. Place one or more *.jsonl files in the directory.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Generate type declarations
|
|
47
|
+
const content = this.generateTypeDeclarations(tables);
|
|
48
|
+
|
|
49
|
+
// Ensure output directory exists
|
|
50
|
+
const outputDir = dirname(this.outputFile);
|
|
51
|
+
await mkdir(outputDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Write to file
|
|
54
|
+
await writeFile(this.outputFile, content, 'utf-8');
|
|
55
|
+
console.log(`Generated types at ${this.outputFile}`);
|
|
56
|
+
return this.outputFile;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find all *.jsonl files and check if they have corresponding *.schema.ts files
|
|
61
|
+
*/
|
|
62
|
+
private async findTables(): Promise<TableInfo[]> {
|
|
63
|
+
try {
|
|
64
|
+
const entries = await readdir(this.dataDirPath, { withFileTypes: true });
|
|
65
|
+
const tables: TableInfo[] = [];
|
|
66
|
+
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
69
|
+
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(
|
|
75
|
+
e => e.isFile() && e.name === schemaFileName
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
tables.push({
|
|
79
|
+
tableName,
|
|
80
|
+
schemaFile: hasSchema ? schemaFilePath : undefined,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return tables;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Data directory not found: ${this.dataDirPath}. Set lines-db.dataDir to the correct location.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate type declaration content
|
|
98
|
+
*/
|
|
99
|
+
private generateTypeDeclarations(tables: TableInfo[]): string {
|
|
100
|
+
const imports: string[] = [];
|
|
101
|
+
const tableEntries: string[] = [];
|
|
102
|
+
const usedAliases = new Set<string>();
|
|
103
|
+
|
|
104
|
+
for (const table of tables) {
|
|
105
|
+
const tableKey = this.formatTableKey(table.tableName);
|
|
106
|
+
|
|
107
|
+
if (table.schemaFile) {
|
|
108
|
+
// Table has a schema file
|
|
109
|
+
const schemaIdentifier = this.createSchemaIdentifier(table.tableName, usedAliases);
|
|
110
|
+
usedAliases.add(schemaIdentifier);
|
|
111
|
+
|
|
112
|
+
// Calculate relative path from output file to schema file
|
|
113
|
+
let relativePath = relative(join(this.outputFile, '..'), table.schemaFile)
|
|
114
|
+
.replace(/\\/g, '/') // Convert Windows paths to Unix-style
|
|
115
|
+
.replace('.ts', '.js'); // Import from .js (TypeScript module resolution)
|
|
116
|
+
|
|
117
|
+
// Ensure relative path starts with './' or '../'
|
|
118
|
+
if (!relativePath.startsWith('.')) {
|
|
119
|
+
relativePath = './' + relativePath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add import statement for schema
|
|
123
|
+
imports.push(`import { schema as ${schemaIdentifier} } from '${relativePath}';`);
|
|
124
|
+
|
|
125
|
+
// Add table entry with InferOutput
|
|
126
|
+
tableEntries.push(` ${tableKey}: InferOutput<typeof ${schemaIdentifier}>;`);
|
|
127
|
+
} else {
|
|
128
|
+
// Table has no schema file, use Record<string, unknown>
|
|
129
|
+
tableEntries.push(` ${tableKey}: Record<string, unknown>;`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Generate full content
|
|
134
|
+
const importSection = imports.length > 0 ? `${imports.join('\n')}\n` : '';
|
|
135
|
+
const inferOutputImport = imports.length > 0 ? ', InferOutput' : '';
|
|
136
|
+
|
|
137
|
+
return `// Auto-generated by lines-db
|
|
138
|
+
// Do not edit this file manually
|
|
139
|
+
|
|
140
|
+
${importSection}import type { DatabaseConfig${inferOutputImport} } from 'lines-db';
|
|
141
|
+
import { fileURLToPath } from 'node:url';
|
|
142
|
+
import { dirname } from 'node:path';
|
|
143
|
+
|
|
144
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
145
|
+
const __dirname = dirname(__filename);
|
|
146
|
+
|
|
147
|
+
export type Tables = {
|
|
148
|
+
${tableEntries.join('\n')}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const config: DatabaseConfig<Tables> = {
|
|
152
|
+
dataDir: __dirname,
|
|
153
|
+
};
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private createSchemaIdentifier(tableName: string, usedAliases: Set<string>): string {
|
|
158
|
+
const camel = toCamelCase(tableName);
|
|
159
|
+
const sanitizedBase = sanitizeIdentifier(camel);
|
|
160
|
+
let base = sanitizedBase || 'table';
|
|
161
|
+
|
|
162
|
+
if (!/^[A-Za-z_$]/.test(base)) {
|
|
163
|
+
base = `_${base}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let candidate = `${base}Schema`;
|
|
167
|
+
let suffix = 1;
|
|
168
|
+
while (usedAliases.has(candidate)) {
|
|
169
|
+
candidate = `${base}${++suffix}Schema`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return candidate;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private formatTableKey(tableName: string): string {
|
|
176
|
+
const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
177
|
+
if (identifierPattern.test(tableName)) {
|
|
178
|
+
return tableName;
|
|
179
|
+
}
|
|
180
|
+
const escaped = tableName.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
181
|
+
return `'${escaped}'`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function toCamelCase(value: string): string {
|
|
186
|
+
const parts = value
|
|
187
|
+
.split(/[^A-Za-z0-9]+/)
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.map((part) => part.toLowerCase());
|
|
190
|
+
|
|
191
|
+
if (parts.length === 0) {
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const [first, ...rest] = parts;
|
|
196
|
+
return first + rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sanitizeIdentifier(value: string): string {
|
|
200
|
+
return value.replace(/[^A-Za-z0-9_$]/g, '');
|
|
201
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Re-export StandardSchema types from official package
|
|
2
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
|
+
|
|
4
|
+
export type Table = Record<string, unknown>;
|
|
5
|
+
export type StandardSchema<
|
|
6
|
+
Input extends Table = Table,
|
|
7
|
+
Output extends Table = Input,
|
|
8
|
+
> = StandardSchemaV1<Input, Output>;
|
|
9
|
+
export type StandardSchemaResult<Output> = StandardSchemaV1.Result<Output>;
|
|
10
|
+
export type StandardSchemaIssue = StandardSchemaV1.Issue;
|
|
11
|
+
|
|
12
|
+
// Export the official type as well
|
|
13
|
+
export type { StandardSchemaV1 };
|
|
14
|
+
|
|
15
|
+
// Type inference helpers for StandardSchema
|
|
16
|
+
// These work with any StandardSchema-compatible schema (Valibot, Zod, etc.)
|
|
17
|
+
export type InferInput<T> = T extends StandardSchemaV1<infer I, unknown> ? I : never;
|
|
18
|
+
export type InferOutput<T> = T extends StandardSchemaV1<unknown, infer O> ? O : never;
|
|
19
|
+
|
|
20
|
+
export interface ForeignKeyDefinition {
|
|
21
|
+
columns: string[];
|
|
22
|
+
references: {
|
|
23
|
+
table: string;
|
|
24
|
+
columns: string[];
|
|
25
|
+
};
|
|
26
|
+
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
|
|
27
|
+
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IndexDefinition {
|
|
31
|
+
name?: string;
|
|
32
|
+
columns: string[];
|
|
33
|
+
unique?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TableSchema {
|
|
37
|
+
name: string;
|
|
38
|
+
columns: ColumnDefinition[];
|
|
39
|
+
foreignKeys?: ForeignKeyDefinition[];
|
|
40
|
+
indexes?: IndexDefinition[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ColumnDefinition {
|
|
44
|
+
name: string;
|
|
45
|
+
type: 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' | 'NULL' | 'JSON';
|
|
46
|
+
primaryKey?: boolean;
|
|
47
|
+
notNull?: boolean;
|
|
48
|
+
unique?: boolean;
|
|
49
|
+
valueType?: 'boolean';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type TableDefs = Record<string, Table>;
|
|
53
|
+
export declare const TABLES_BRAND: unique symbol;
|
|
54
|
+
|
|
55
|
+
export interface DatabaseConfig<_Tables extends TableDefs = TableDefs> {
|
|
56
|
+
dataDir: string; // Directory containing JSONL files
|
|
57
|
+
readonly [TABLES_BRAND]?: _Tables;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ExtractTables<Config> = Config extends {
|
|
61
|
+
readonly [TABLES_BRAND]?: infer T;
|
|
62
|
+
}
|
|
63
|
+
? T extends TableDefs
|
|
64
|
+
? T
|
|
65
|
+
: TableDefs
|
|
66
|
+
: Config extends DatabaseConfig<infer T>
|
|
67
|
+
? T
|
|
68
|
+
: TableDefs;
|
|
69
|
+
|
|
70
|
+
export interface TableConfig {
|
|
71
|
+
jsonlPath: string;
|
|
72
|
+
schema?: TableSchema;
|
|
73
|
+
autoInferSchema?: boolean;
|
|
74
|
+
validationSchema?: StandardSchema; // Optional validation schema
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ValidationError extends Error {
|
|
78
|
+
name: 'ValidationError';
|
|
79
|
+
issues: ReadonlyArray<StandardSchemaIssue>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type JsonValue = string | number | boolean | null | JsonObject | JsonArray;
|
|
83
|
+
export interface JsonObject {
|
|
84
|
+
[key: string]: JsonValue;
|
|
85
|
+
}
|
|
86
|
+
export type JsonArray = JsonValue[];
|
|
87
|
+
|
|
88
|
+
// WHERE condition types for filtering
|
|
89
|
+
export type WhereValue<T> = T | ((value: T) => boolean);
|
|
90
|
+
|
|
91
|
+
export type WhereObject<T extends Table> = {
|
|
92
|
+
[K in keyof T]?: WhereValue<T[K]>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type WhereCondition<T extends Table> =
|
|
96
|
+
| WhereObject<T> // Single object with AND conditions
|
|
97
|
+
| WhereConditionArray<T>; // Array with OR conditions
|
|
98
|
+
|
|
99
|
+
export type WhereConditionArray<T extends Table> = Array<WhereObject<T> | WhereConditionArray<T>>;
|