flint-orm 0.2.0 → 0.4.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.
Files changed (5) hide show
  1. package/API.md +208 -211
  2. package/README.md +341 -0
  3. package/dist/index.js +288 -255
  4. package/package.json +46 -11
  5. package/src/cli.ts +103 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flint-orm",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "bin": {
5
5
  "flint": "./src/cli.ts"
6
6
  },
@@ -10,42 +10,77 @@
10
10
  "README.md"
11
11
  ],
12
12
  "type": "module",
13
- "main": "./dist/index.js",
13
+ "main": "./dist/src/index.js",
14
14
  "types": "./dist/index.d.ts",
15
15
  "exports": {
16
16
  ".": {
17
- "import": "./dist/index.js",
17
+ "import": "./dist/src/index.js",
18
18
  "types": "./dist/index.d.ts"
19
19
  },
20
+ "./bun-sqlite": {
21
+ "import": "./dist/src/entries/bun-sqlite.js",
22
+ "types": "./dist/entries/bun-sqlite.d.ts"
23
+ },
24
+ "./better-sqlite3": {
25
+ "import": "./dist/src/entries/better-sqlite3.js",
26
+ "types": "./dist/entries/better-sqlite3.d.ts"
27
+ },
28
+ "./libsql": {
29
+ "import": "./dist/src/entries/libsql.js",
30
+ "types": "./dist/entries/libsql.d.ts"
31
+ },
32
+ "./libsql-web": {
33
+ "import": "./dist/src/entries/libsql-web.js",
34
+ "types": "./dist/entries/libsql-web.d.ts"
35
+ },
36
+ "./turso-sync": {
37
+ "import": "./dist/src/entries/turso-sync.js",
38
+ "types": "./dist/entries/turso-sync.d.ts"
39
+ },
40
+ "./turso": {
41
+ "import": "./dist/src/entries/turso.js",
42
+ "types": "./dist/entries/turso.d.ts"
43
+ },
20
44
  "./table": {
21
- "import": "./dist/entries/table.js",
45
+ "import": "./dist/src/entries/table.js",
22
46
  "types": "./dist/entries/table.d.ts"
23
47
  },
24
48
  "./expressions": {
25
- "import": "./dist/entries/expressions.js",
49
+ "import": "./dist/src/entries/expressions.js",
26
50
  "types": "./dist/entries/expressions.d.ts"
27
51
  },
28
52
  "./config": {
29
- "import": "./dist/entries/config.js",
53
+ "import": "./dist/src/entries/config.js",
30
54
  "types": "./dist/entries/config.d.ts"
31
55
  },
32
56
  "./migration": {
33
57
  "import": "./dist/src/migration/index.js",
34
- "types": "./dist/src/migration/index.d.ts"
58
+ "types": "./dist/migration/index.d.ts"
35
59
  }
36
60
  },
37
61
  "scripts": {
38
- "build": "bun build ./src/index.ts ./src/entries/table.ts ./src/entries/expressions.ts ./src/entries/config.ts --outdir ./dist --target bun && tsc --project tsconfig.build.json",
62
+ "build": "bun build ./src/index.ts ./src/entries/bun-sqlite.ts ./src/entries/better-sqlite3.ts ./src/entries/libsql.ts ./src/entries/libsql-web.ts ./src/entries/turso-sync.ts ./src/entries/turso.ts ./src/entries/table.ts ./src/entries/expressions.ts ./src/entries/config.ts ./src/migration/index.ts --outdir ./dist --target bun && tsc --project tsconfig.build.json",
39
63
  "typecheck": "tsc --noEmit",
40
- "lint": "oxlint",
41
- "format": "oxfmt"
64
+ "lint": "oxlint --fix",
65
+ "format": "oxfmt --write",
66
+ "check": "oxlint && oxfmt"
67
+ },
68
+ "dependencies": {
69
+ "@clack/prompts": "1.7.0",
70
+ "@libsql/client": "0.17.4",
71
+ "@tursodatabase/database": "0.6.1",
72
+ "@tursodatabase/sync": "0.6.1",
73
+ "better-sqlite3": "12.11.1",
74
+ "picocolors": "1.1.1"
42
75
  },
43
76
  "devDependencies": {
77
+ "@types/better-sqlite3": "7.6.13",
44
78
  "@types/bun": "1.3.14",
79
+ "@types/node": "26.1.1",
45
80
  "oxfmt": "0.57.0",
46
81
  "oxlint": "1.72.0"
47
82
  },
48
83
  "peerDependencies": {
49
- "typescript": "7.0.1-rc"
84
+ "typescript": "7.0.2"
50
85
  }
51
86
  }
package/src/cli.ts CHANGED
@@ -3,20 +3,26 @@
3
3
  // flint CLI — migration generation for flint-orm
4
4
  // ---------------------------------------------------------------------------
5
5
 
6
- import { parseArgs } from 'node:util';
6
+ import { parseArgs, styleText } from 'node:util';
7
7
  import { statSync, readdirSync, existsSync, readFileSync } from 'node:fs';
8
8
  import { join, resolve, isAbsolute } from 'node:path';
9
9
  import { pathToFileURL } from 'node:url';
10
10
  import type { AnyTable, TableDef } from './schema/table.js';
11
11
  import type { SchemaState } from './migration/types.js';
12
+ import { outro, log, cancel, isCancel, select, pc, note } from './cli/ui.js';
13
+ import { CancellationError } from './migration/diff.js';
14
+ import type { RenamePrompt } from './migration/diff.js';
15
+ import type { Executor } from './executor.js';
12
16
 
13
17
  // ---------------------------------------------------------------------------
14
18
  // Config loading
15
19
  // ---------------------------------------------------------------------------
16
20
 
17
21
  interface FlintConfig {
18
- /** Path to the SQLite database file. */
19
- url: string;
22
+ /** Which SQLite driver to use. */
23
+ driver: string;
24
+ /** Database connection details. */
25
+ database: { url: string; authToken?: string };
20
26
  /** Path to schema file or directory. */
21
27
  schema: string;
22
28
  /** Path to migrations directory (default: ./flint). */
@@ -93,24 +99,22 @@ async function importTableFolder(folderPath: string): Promise<unknown[]> {
93
99
  async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config: FlintConfig): Promise<void> {
94
100
  const name = typeof args.name === 'string' ? args.name : undefined;
95
101
  const preview = args.preview === true;
102
+ const promptRename: RenamePrompt = (message, options) => select({ message: pc.bold(message), options }) as Promise<string | symbol>;
96
103
 
97
- console.log(`🔍 Discovering schema from: ${config.schema}`);
104
+ log.info(`Discovering schema from: ${config.schema}`);
98
105
  const tables = await discoverTables(config.schema);
99
106
 
100
107
  if (tables.length === 0) {
101
- console.error('No table() definitions found in schema path.');
108
+ cancel('No table() definitions found in schema path.');
102
109
  process.exit(1);
103
110
  }
104
111
 
105
- console.log(`📦 Found ${tables.length} table(s):`);
106
- for (const t of tables) {
107
- console.log(` - ${(t as Record<string, Record<string, unknown>>)._?.name ?? 'unknown'}`);
108
- }
112
+ log.info(`Found ${tables.length} table(s): ${tables.map((t) => (t as Record<string, Record<string, unknown>>)._?.name ?? 'unknown').join(', ')}`);
109
113
 
110
114
  if (preview) {
111
115
  // Dynamic import of migration functions
112
116
  const { serializeSchema } = await import('./migration/serialize.js');
113
- const { diffSchemas, emptyState } = await import('./migration/diff.js');
117
+ const { diffSchemas, emptyState, resolveRenames } = await import('./migration/diff.js');
114
118
  const { generateSQL } = await import('./migration/sql.js');
115
119
 
116
120
  // Find latest state
@@ -132,20 +136,20 @@ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config:
132
136
  if (!previousState) previousState = emptyState();
133
137
 
134
138
  const currentState = serializeSchema(tables as AnyTable[]);
135
- const operations = diffSchemas(previousState, currentState);
139
+ const rawOps = diffSchemas(previousState, currentState);
136
140
 
137
- if (operations.length === 0) {
138
- console.log('\n✅ No changes detected — schema is already up to date.');
141
+ if (rawOps.length === 0) {
142
+ outro('Schema is already up to date.');
139
143
  return;
140
144
  }
141
145
 
146
+ // Resolve renames interactively
147
+ const operations = await resolveRenames(rawOps, { interactive: true, prompt: promptRename });
148
+
142
149
  const sql = generateSQL(operations);
143
- const previewLabel = name ? `${name}` : 'unnamed';
144
- console.log(`\n--- Preview: ${previewLabel} ---`);
145
- console.log(` Operations: ${operations.length}`);
146
- console.log(` Migrations dir: ${config.migrations}`);
147
- console.log(`\n--- SQL ---\n${sql}\n`);
148
- console.log('(dry run, no files written)');
150
+ log.info(`Operations: ${operations.length}`);
151
+ note(sql, 'SQL', { format: (text) => styleText('dim', text) });
152
+ log.info('(dry run, no files written)');
149
153
  return;
150
154
  }
151
155
 
@@ -153,13 +157,18 @@ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config:
153
157
  const { generate } = await import('./migration/generate.js');
154
158
 
155
159
  try {
156
- const result = generate(tables as TableDef<any>[], resolve(process.cwd(), (config.migrations ?? './flint') as string), name);
157
- console.log(`\n✅ Migration generated: ${result.folderName}`);
158
- console.log(` Operations: ${result.operations.length}`);
159
- console.log(`\n--- SQL Preview ---\n${result.sql}`);
160
+ const migrationsDir = resolve(process.cwd(), config.migrations ?? './flint');
161
+ const result = await generate(tables as TableDef<any>[], migrationsDir, {
162
+ name,
163
+ interactive: true,
164
+ prompt: promptRename,
165
+ });
166
+ log.info(`Operations: ${result.operations.length}`);
167
+ note(result.sql, 'SQL', { format: (text) => styleText('dim', text) });
168
+ log.success(`Migration generated: ${migrationsDir}/${result.folderName}`);
160
169
  } catch (err: unknown) {
161
170
  if (err instanceof Error && err.message.includes('No changes detected')) {
162
- console.log('\n✅ No changes detected — schema is already up to date.');
171
+ outro('Schema is already up to date.');
163
172
  } else {
164
173
  throw err;
165
174
  }
@@ -167,64 +176,108 @@ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config:
167
176
  }
168
177
 
169
178
  async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config: FlintConfig): Promise<void> {
170
- const { Database } = await import('bun:sqlite');
171
179
  const { migrate, getMigrationStatus } = await import('./migration/migrate.js');
172
180
 
173
181
  const migrationsDir = resolve(process.cwd(), config.migrations ?? './flint');
174
182
  const dryRun = args['dry-run'] === true;
175
183
  const statusOnly = args.status === true;
176
- const name = typeof args.name === 'string' ? args.name : undefined;
177
184
 
178
- // Open database from config
179
- const dbUrl = resolve(process.cwd(), config.url);
180
- const db = new Database(dbUrl);
185
+ // Dynamically create executor based on configured driver
186
+ const isLocalDriver = config.driver === 'bun-sqlite' || config.driver === 'better-sqlite3';
187
+ const dbUrl = isLocalDriver ? resolve(process.cwd(), config.database.url) : config.database.url;
188
+
189
+ let executor: Executor;
190
+
191
+ switch (config.driver) {
192
+ case 'bun-sqlite': {
193
+ const { BunSqliteExecutor } = await import('./drivers/bun-sqlite');
194
+ const { Database } = await import('bun:sqlite');
195
+ executor = new BunSqliteExecutor(new Database(dbUrl));
196
+ break;
197
+ }
198
+ case 'better-sqlite3': {
199
+ const { BetterSqlite3Executor } = await import('./drivers/better-sqlite3');
200
+ const Database = (await import('better-sqlite3')).default;
201
+ executor = new BetterSqlite3Executor(new Database(dbUrl));
202
+ break;
203
+ }
204
+ case 'libsql': {
205
+ const { LibsqlExecutor } = await import('./drivers/libsql');
206
+ const { createClient: createLibsqlClient } = await import('@libsql/client');
207
+ executor = new LibsqlExecutor(createLibsqlClient({ url: dbUrl, authToken: config.database.authToken }));
208
+ break;
209
+ }
210
+ case 'libsql-web': {
211
+ const { LibsqlWebExecutor } = await import('./drivers/libsql-web');
212
+ const { createClient: createLibsqlWebClient } = await import('@libsql/client/web');
213
+ executor = new LibsqlWebExecutor(createLibsqlWebClient({ url: dbUrl, authToken: config.database.authToken }));
214
+ break;
215
+ }
216
+ case 'turso-sync': {
217
+ const { TursoSyncExecutor } = await import('./drivers/turso-sync');
218
+ const { connect } = await import('@tursodatabase/sync');
219
+ const db = await connect({ path: dbUrl, authToken: config.database.authToken });
220
+ executor = new TursoSyncExecutor(db);
221
+ break;
222
+ }
223
+ case 'turso': {
224
+ const { TursoExecutor } = await import('./drivers/turso');
225
+ const { connect } = await import('@tursodatabase/database');
226
+ const db = await connect(dbUrl);
227
+ executor = new TursoExecutor(db);
228
+ break;
229
+ }
230
+ default:
231
+ cancel(`Unsupported driver: ${config.driver}`);
232
+ process.exit(1);
233
+ }
181
234
 
182
235
  try {
183
236
  if (statusOnly) {
184
- const status = getMigrationStatus(db, migrationsDir);
237
+ const status = await getMigrationStatus(executor, migrationsDir);
185
238
 
186
239
  if (status.applied.length === 0 && status.pending.length === 0) {
187
- console.log('No migrations found.');
240
+ outro('No migrations found.');
188
241
  return;
189
242
  }
190
243
 
191
244
  if (status.applied.length > 0) {
192
- console.log('\n📋 Applied migrations:');
245
+ log.info('Applied migrations:');
193
246
  for (const m of status.applied) {
194
- console.log(` ✓ ${m.name} (${m.folderName})`);
247
+ log.success(`${m.name} (${m.folderName})`);
195
248
  }
196
249
  }
197
250
 
198
251
  if (status.pending.length > 0) {
199
- console.log('\n⏳ Pending migrations:');
252
+ log.warn('Pending migrations:');
200
253
  for (const m of status.pending) {
201
- console.log(` ○ ${m.name} (${m.folderName})`);
254
+ log.message(` ○ ${m.name} (${m.folderName})`);
202
255
  }
203
256
  }
204
257
 
205
- console.log(`\n ${status.applied.length} applied, ${status.pending.length} pending`);
258
+ log.info(`${status.applied.length} applied, ${status.pending.length} pending`);
206
259
  return;
207
260
  }
208
261
 
209
- const result = await migrate(db, {
262
+ const result = await migrate(executor, {
210
263
  migrationsDir,
211
264
  dryRun,
212
265
  });
213
266
 
214
267
  if (result.applied.length === 0) {
215
- console.log('\n✅ No pending migrations — database is up to date.');
268
+ outro('No pending migrations — database is up to date.');
216
269
  } else {
217
- console.log(`\n🚀 ${dryRun ? 'Would apply' : 'Applied'} ${result.applied.length} migration(s):`);
218
- for (const name of result.applied) {
219
- console.log(` ✓ ${name}`);
270
+ log.info(`${dryRun ? 'Would apply' : 'Applied'} ${result.applied.length} migration(s):`);
271
+ for (const appliedName of result.applied) {
272
+ log.success(appliedName);
220
273
  }
221
274
  }
222
275
 
223
276
  if (result.skipped.length > 0 && !dryRun) {
224
- console.log(`\n⏭ Skipped ${result.skipped.length} already applied migration(s)`);
277
+ log.info(`Skipped ${result.skipped.length} already applied migration(s)`);
225
278
  }
226
279
  } finally {
227
- db.close();
280
+ executor.close();
228
281
  }
229
282
  }
230
283
 
@@ -233,9 +286,7 @@ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config:
233
286
  // ---------------------------------------------------------------------------
234
287
 
235
288
  function printHelp(): void {
236
- console.log(`
237
- flint — migration CLI for flint-orm
238
-
289
+ log.message(`
239
290
  Usage:
240
291
  flint <command> [options]
241
292
 
@@ -294,13 +345,17 @@ async function main(): Promise<void> {
294
345
  await cmdMigrate(values, config);
295
346
  break;
296
347
  default:
297
- console.error(`❌ Unknown command: ${command}`);
348
+ cancel(`Unknown command: ${command}`);
298
349
  printHelp();
299
350
  process.exit(1);
300
351
  }
301
352
  }
302
353
 
303
354
  main().catch((err) => {
304
- console.error(err);
355
+ if (isCancel(err) || err instanceof CancellationError) {
356
+ cancel('Operation cancelled.');
357
+ } else {
358
+ cancel(err.message ?? 'An error occurred');
359
+ }
305
360
  process.exit(1);
306
361
  });