@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.
@@ -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>>;