flint-orm 0.2.0 ā 0.3.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/API.md +178 -16
- package/README.md +238 -0
- package/package.json +5 -1
- package/src/cli.ts +46 -38
package/API.md
CHANGED
|
@@ -33,12 +33,12 @@ const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute(
|
|
|
33
33
|
|
|
34
34
|
## Schema
|
|
35
35
|
|
|
36
|
-
### `table(name, columns)`
|
|
36
|
+
### `table(name, columns, indexFn?)`
|
|
37
37
|
|
|
38
38
|
Define a table. Columns live as direct properties. SQL metadata is under `._`.
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
|
-
import { table, text, integer, boolean } from 'flint-orm';
|
|
41
|
+
import { table, text, integer, boolean, index } from 'flint-orm';
|
|
42
42
|
|
|
43
43
|
const users = table('users', {
|
|
44
44
|
id: text('id').primaryKey(),
|
|
@@ -49,7 +49,7 @@ const users = table('users', {
|
|
|
49
49
|
});
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
### `snakeCase.table(name, columns)`
|
|
52
|
+
### `snakeCase.table(name, columns, indexFn?)`
|
|
53
53
|
|
|
54
54
|
Auto-converts camelCase keys to snake_case SQL names.
|
|
55
55
|
|
|
@@ -122,6 +122,28 @@ type UserInsert = InsertRow<typeof users>;
|
|
|
122
122
|
|
|
123
123
|
---
|
|
124
124
|
|
|
125
|
+
## Indexes
|
|
126
|
+
|
|
127
|
+
### `index(name).on(columns).unique()`
|
|
128
|
+
|
|
129
|
+
Define indexes via the table callback. Chainable API.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const users = table(
|
|
133
|
+
'users',
|
|
134
|
+
{
|
|
135
|
+
id: text('id').primaryKey(),
|
|
136
|
+
email: text('email'),
|
|
137
|
+
name: text('name'),
|
|
138
|
+
},
|
|
139
|
+
(t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name), index('idx_users_email_name').on(t.email, t.name)],
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The callback receives the table definition and returns an array of `IndexBuilder` objects. Indexes are attached to the table automatically and serialized for migrations.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
125
147
|
## Query Builder
|
|
126
148
|
|
|
127
149
|
### `db.select().from(table)`
|
|
@@ -333,7 +355,7 @@ const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'nam
|
|
|
333
355
|
|
|
334
356
|
### `db.leftJoin(parent).on(child, condition?)`
|
|
335
357
|
|
|
336
|
-
LEFT JOIN. Returns all parent rows, with matching child data
|
|
358
|
+
LEFT JOIN. Returns all parent rows, with matching child data nested under `__children`.
|
|
337
359
|
|
|
338
360
|
```ts
|
|
339
361
|
const orders = table('orders', {
|
|
@@ -367,7 +389,7 @@ Joins return **nested** results, not flat-merged:
|
|
|
367
389
|
{
|
|
368
390
|
id: 'u1',
|
|
369
391
|
name: 'Alice',
|
|
370
|
-
|
|
392
|
+
__children: [
|
|
371
393
|
{ id: 'o1', userId: 'u1', total: 100 },
|
|
372
394
|
{ id: 'o2', userId: 'u1', total: 200 },
|
|
373
395
|
],
|
|
@@ -381,14 +403,23 @@ Narrow parent columns with `.columns()`:
|
|
|
381
403
|
|
|
382
404
|
```ts
|
|
383
405
|
db.leftJoin(orders).on(users, eq(orders.userId, users.id)).columns(['id', 'name']).execute();
|
|
384
|
-
// Returns: { id: string; name: string;
|
|
406
|
+
// Returns: { id: string; name: string; __children: OrderRow[] }[]
|
|
385
407
|
```
|
|
386
408
|
|
|
387
409
|
### `.single()` on Joins
|
|
388
410
|
|
|
389
411
|
```ts
|
|
390
412
|
db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
|
|
391
|
-
// Returns: { id: string; name: string;
|
|
413
|
+
// Returns: { id: string; name: string; __children: OrderRow[] } | null
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Multi-Join
|
|
417
|
+
|
|
418
|
+
Chain multiple joins:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
|
|
422
|
+
// Returns nested: { ...userFields, __children: [{ ...orderFields, __children: [...items] }] }
|
|
392
423
|
```
|
|
393
424
|
|
|
394
425
|
---
|
|
@@ -557,14 +588,21 @@ All queries succeed or all roll back.
|
|
|
557
588
|
|
|
558
589
|
## Raw SQL
|
|
559
590
|
|
|
560
|
-
|
|
591
|
+
### `db.$run(sql, ...params)`
|
|
592
|
+
|
|
593
|
+
Execute raw SQL directly against the database.
|
|
561
594
|
|
|
562
595
|
```ts
|
|
563
|
-
|
|
564
|
-
|
|
596
|
+
db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
|
|
597
|
+
db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### `db.$client`
|
|
565
601
|
|
|
566
|
-
|
|
567
|
-
|
|
602
|
+
Direct access to the underlying `bun:sqlite` client.
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
const rows = db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
|
|
568
606
|
```
|
|
569
607
|
|
|
570
608
|
---
|
|
@@ -579,20 +617,142 @@ import { sql } from 'flint-orm';
|
|
|
579
617
|
const expr = sql`SELECT * FROM users WHERE name = ${'Alice'} AND age > ${18}`;
|
|
580
618
|
// { sql: "SELECT * FROM users WHERE name = ? AND age > ?", params: ["Alice", 18] }
|
|
581
619
|
|
|
582
|
-
//
|
|
620
|
+
// Use with db.$client
|
|
583
621
|
const rows = db.$client.prepare(expr.sql).all(...expr.params);
|
|
584
622
|
```
|
|
585
623
|
|
|
586
624
|
---
|
|
587
625
|
|
|
588
|
-
##
|
|
626
|
+
## Migration System
|
|
627
|
+
|
|
628
|
+
### `flint generate` (CLI)
|
|
629
|
+
|
|
630
|
+
Generate a migration from schema changes.
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
# Generate migration
|
|
634
|
+
flint generate --name init_schema
|
|
635
|
+
|
|
636
|
+
# Preview SQL without writing files
|
|
637
|
+
flint generate --name init_schema --preview
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### `flint migrate` (CLI)
|
|
641
|
+
|
|
642
|
+
Apply pending migrations to the database.
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
# Apply all pending migrations
|
|
646
|
+
flint migrate
|
|
647
|
+
|
|
648
|
+
# Show which migrations are pending/applied
|
|
649
|
+
flint migrate --status
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Config
|
|
653
|
+
|
|
654
|
+
Create `flint.config.ts` in your project root:
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
import { defineConfig } from 'flint-orm/config';
|
|
658
|
+
|
|
659
|
+
export default defineConfig({
|
|
660
|
+
schema: './src/schema', // folder or file with table() definitions
|
|
661
|
+
migrations: './flint', // where migration folders are stored
|
|
662
|
+
database: {
|
|
663
|
+
url: './app.db', // SQLite database path
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Programmatic API
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
|
|
672
|
+
|
|
673
|
+
// Serialize table definitions to JSON
|
|
674
|
+
const state = serializeSchema([users, orders]);
|
|
675
|
+
|
|
676
|
+
// Diff two schema states
|
|
677
|
+
const operations = diffSchemas(previousState, currentState);
|
|
678
|
+
|
|
679
|
+
// Generate SQL from operations
|
|
680
|
+
const sql = generateSQL(operations);
|
|
681
|
+
|
|
682
|
+
// Generate a migration folder
|
|
683
|
+
const result = generate([users, orders], './flint', 'init_schema');
|
|
684
|
+
|
|
685
|
+
// Apply pending migrations
|
|
686
|
+
const result = migrate({ url: './app.db' }, './flint');
|
|
687
|
+
|
|
688
|
+
// Check migration status
|
|
689
|
+
const status = getMigrationStatus(client, './flint');
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Migration Operations
|
|
693
|
+
|
|
694
|
+
Named, pre-vetted operations:
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration';
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
| Operation | SQL Generated |
|
|
701
|
+
| -------------- | -------------------------------------- |
|
|
702
|
+
| `addTable` | `CREATE TABLE ...` |
|
|
703
|
+
| `dropTable` | `DROP TABLE ...` |
|
|
704
|
+
| `renameTable` | `ALTER TABLE ... RENAME TO ...` |
|
|
705
|
+
| `addColumn` | `ALTER TABLE ... ADD COLUMN ...` |
|
|
706
|
+
| `dropColumn` | `ALTER TABLE ... DROP COLUMN ...` |
|
|
707
|
+
| `renameColumn` | `ALTER TABLE ... RENAME COLUMN ... TO` |
|
|
708
|
+
| `createIndex` | `CREATE [UNIQUE] INDEX ...` |
|
|
709
|
+
| `dropIndex` | `DROP INDEX ...` |
|
|
710
|
+
|
|
711
|
+
### Migration File Shape
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
import { defineMigration } from 'flint-orm/migration';
|
|
715
|
+
import { addTable, addColumn } from 'flint-orm/migration';
|
|
716
|
+
|
|
717
|
+
export default defineMigration({
|
|
718
|
+
name: 'init_schema',
|
|
719
|
+
operations: [
|
|
720
|
+
addTable({ name: 'users', columns: [...], indexes: [...] }),
|
|
721
|
+
addColumn('users', { name: 'email', sqlType: 'text', ... }),
|
|
722
|
+
],
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Tracking
|
|
727
|
+
|
|
728
|
+
Applied migrations are recorded in `__flint_migrations` (per-database). The table is created on first `migrate()` call ā never with `IF NOT EXISTS`.
|
|
729
|
+
|
|
730
|
+
### FK Ordering
|
|
731
|
+
|
|
732
|
+
Tables are topologically sorted (Kahn's algorithm) before generating `CREATE TABLE` statements. Referenced tables are created first.
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## SQLite Introspection
|
|
589
737
|
|
|
590
|
-
|
|
738
|
+
### `introspectSchema(client)`
|
|
739
|
+
|
|
740
|
+
Read the live database schema and return a `SchemaState` that can be diffed against code-defined tables.
|
|
591
741
|
|
|
592
742
|
```ts
|
|
593
|
-
|
|
743
|
+
import { introspectSchema } from 'flint-orm/sqlite';
|
|
744
|
+
import { diffSchemas } from 'flint-orm/migration';
|
|
745
|
+
|
|
746
|
+
const client = new Database('./app.db');
|
|
747
|
+
const liveState = introspectSchema(client);
|
|
748
|
+
|
|
749
|
+
// Compare live DB against code schema
|
|
750
|
+
const codeState = serializeSchema([users, orders]);
|
|
751
|
+
const operations = diffSchemas(liveState, codeState);
|
|
594
752
|
```
|
|
595
753
|
|
|
754
|
+
Normalizes SQLite type aliases (VARCHAR ā TEXT, BIGINT ā INTEGER, etc.) and parses default values.
|
|
755
|
+
|
|
596
756
|
---
|
|
597
757
|
|
|
598
758
|
## Types
|
|
@@ -603,6 +763,8 @@ db.$client; // Database instance
|
|
|
603
763
|
| `ColumnDef<T, S>` | Column definition with phantom type `T` and storage class `S` |
|
|
604
764
|
| `InferRow<T>` | Derives row type from table definition |
|
|
605
765
|
| `InsertRow<T>` | Derives insert type (defaults are optional) |
|
|
766
|
+
| `IndexDef` | Index definition: `{ name, columns, unique }` |
|
|
767
|
+
| `IndexBuilder` | Chainable index builder: `.on(cols).unique()` |
|
|
606
768
|
| `Condition` | Condition node for WHERE clauses |
|
|
607
769
|
| `Executable` | Anything with a `.toSQL()` method (for `batch()`) |
|
|
608
770
|
| `ConnectionDetails` | `{ url: string }` |
|
package/README.md
CHANGED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# flint-orm
|
|
2
|
+
|
|
3
|
+
A minimal, type-safe SQLite ORM. One dialect, one runtime, no abstraction overhead.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Drizzle is great, but its execution layer (`drizzle-kit migrate`) wasn't built for dynamic, multi-tenant, per-request-connection contexts. Flint is narrower by design: SQLite/libSQL only, no multi-dialect abstraction, no pluggable drivers ā just a query builder that does exactly what SQLite supports.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install flint-orm
|
|
13
|
+
bun add flint-orm
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { flint, table, text, integer, eq } from 'flint-orm';
|
|
20
|
+
|
|
21
|
+
const users = table('users', {
|
|
22
|
+
id: text('id').primaryKey(),
|
|
23
|
+
name: text('name').notNull(),
|
|
24
|
+
email: text('email').unique(),
|
|
25
|
+
age: integer('age'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const db = flint({ url: 'app.db' });
|
|
29
|
+
|
|
30
|
+
// Insert
|
|
31
|
+
db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
|
|
32
|
+
|
|
33
|
+
// Query
|
|
34
|
+
const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute();
|
|
35
|
+
// { id: "u1", name: "Alice", email: "alice@example.com", age: null }
|
|
36
|
+
|
|
37
|
+
// Update
|
|
38
|
+
db.update(users).set({ name: 'Alice Updated' }).where(eq(users.id, 'u1')).execute();
|
|
39
|
+
|
|
40
|
+
// Delete
|
|
41
|
+
db.delete(users).where(eq(users.id, 'u1')).execute();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
### Schema
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { table, text, integer, boolean, json, date, real, index } from 'flint-orm';
|
|
50
|
+
|
|
51
|
+
const users = table(
|
|
52
|
+
'users',
|
|
53
|
+
{
|
|
54
|
+
id: text('id').primaryKey(),
|
|
55
|
+
name: text('name').notNull(),
|
|
56
|
+
email: text('email').unique(),
|
|
57
|
+
active: boolean('active').default(true),
|
|
58
|
+
metadata: json('metadata'),
|
|
59
|
+
createdAt: date('createdAt').defaultNow().onUpdate(),
|
|
60
|
+
score: real('score'),
|
|
61
|
+
age: integer('age'),
|
|
62
|
+
orgId: text('orgId').references(orgs.id),
|
|
63
|
+
},
|
|
64
|
+
(t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_org').on(t.orgId)],
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Type | TS Type | SQLite | Modifiers |
|
|
69
|
+
| --------- | --------- | ------- | ------------------------------------------------------------------------- |
|
|
70
|
+
| `text` | `string` | TEXT | `.primaryKey()`, `.notNull()`, `.unique()`, `.default()`, `.references()` |
|
|
71
|
+
| `integer` | `number` | INTEGER | `.autoIncrement()` |
|
|
72
|
+
| `boolean` | `boolean` | INTEGER | Encodes 0/1 |
|
|
73
|
+
| `json<T>` | `T` | TEXT | JSON encode/decode |
|
|
74
|
+
| `date` | `Date` | INTEGER | `.defaultNow()`, `.onUpdate()` |
|
|
75
|
+
| `real` | `number` | REAL | |
|
|
76
|
+
|
|
77
|
+
### Query Builder
|
|
78
|
+
|
|
79
|
+
Two-phase builders prevent invalid queries at compile time:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// SELECT
|
|
83
|
+
db.select().from(users).columns(['id', 'name']).where(eq(users.active, true)).orderBy('name').limit(10).execute();
|
|
84
|
+
|
|
85
|
+
// INSERT
|
|
86
|
+
db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
|
|
87
|
+
|
|
88
|
+
// UPSERT
|
|
89
|
+
db.insert(users)
|
|
90
|
+
.values({ id: 'u1', name: 'Alice' })
|
|
91
|
+
.onConflictDoUpdate({ target: users.id, set: { name: 'Bob' } })
|
|
92
|
+
.execute();
|
|
93
|
+
|
|
94
|
+
// UPDATE
|
|
95
|
+
db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
|
|
96
|
+
|
|
97
|
+
// DELETE
|
|
98
|
+
db.delete(users).where(eq(users.id, 'u1')).returning().execute();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Joins
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const orders = table('orders', {
|
|
105
|
+
id: text('id').primaryKey(),
|
|
106
|
+
userId: text('userId').references(users.id),
|
|
107
|
+
total: integer('total').notNull(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Left join ā nested result shape
|
|
111
|
+
const result = db.leftJoin(orders).on(users).execute();
|
|
112
|
+
// [{ id: "u1", name: "Alice", __children: [{ id: "o1", total: 100 }] }]
|
|
113
|
+
|
|
114
|
+
// Multi-join
|
|
115
|
+
db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Aggregates
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
db.count(users);
|
|
122
|
+
db.sum(orders, orders.total);
|
|
123
|
+
db.avg(orders, orders.total, eq(orders.userId, 'u1'));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Batch
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
db.batch([db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }), db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1'))]);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Raw SQL
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Conditions
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { eq, neq, gt, gte, lt, lte, and, or, isIn, isNotIn, isNull, isNotNull, like, glob, between } from 'flint-orm';
|
|
142
|
+
|
|
143
|
+
db.select()
|
|
144
|
+
.from(users)
|
|
145
|
+
.where(and(eq(users.active, true), gt(users.age, 18)))
|
|
146
|
+
.execute();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Types
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import type { InferRow, InsertRow } from 'flint-orm';
|
|
153
|
+
|
|
154
|
+
type UserRow = InferRow<typeof users>;
|
|
155
|
+
type UserInsert = InsertRow<typeof users>; // defaults are optional
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Migration System
|
|
159
|
+
|
|
160
|
+
### CLI
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Generate migration from schema changes
|
|
164
|
+
flint generate --name init_schema
|
|
165
|
+
|
|
166
|
+
# Preview SQL without writing files
|
|
167
|
+
flint generate --preview
|
|
168
|
+
|
|
169
|
+
# Apply pending migrations
|
|
170
|
+
flint migrate
|
|
171
|
+
|
|
172
|
+
# Check status
|
|
173
|
+
flint migrate --status
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Config
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// flint.config.ts
|
|
180
|
+
import { defineConfig } from 'flint-orm/config';
|
|
181
|
+
|
|
182
|
+
export default defineConfig({
|
|
183
|
+
schema: './src/schema',
|
|
184
|
+
migrations: './flint',
|
|
185
|
+
database: { url: './app.db' },
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### How It Works
|
|
190
|
+
|
|
191
|
+
1. `flint generate` serializes your `table()` definitions, diffs against the last snapshot, and writes a migration folder with TypeScript operations
|
|
192
|
+
2. `flint migrate` reads pending migration folders, executes SQL via `batch()` (atomic), and records applied migrations in `__flint_migrations`
|
|
193
|
+
3. Tables are topologically sorted by foreign key dependencies (Kahn's algorithm) ā referenced tables are created first
|
|
194
|
+
|
|
195
|
+
### Programmatic
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## CLI
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
flint <command> [options]
|
|
205
|
+
|
|
206
|
+
Commands:
|
|
207
|
+
generate Generate a new migration from schema changes
|
|
208
|
+
migrate Apply pending migrations
|
|
209
|
+
status Show migration status
|
|
210
|
+
|
|
211
|
+
Options:
|
|
212
|
+
--name, -n Migration name
|
|
213
|
+
--preview, -p Show SQL without writing files
|
|
214
|
+
--status Show which migrations are pending/applied
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Error Handling
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { FlintValidationError, FlintQueryError } from 'flint-orm';
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Error | When |
|
|
224
|
+
| ---------------------- | ----------------------------- |
|
|
225
|
+
| `FlintValidationError` | Invalid query construction |
|
|
226
|
+
| `FlintQueryError` | Runtime SQL execution failure |
|
|
227
|
+
|
|
228
|
+
## Philosophy
|
|
229
|
+
|
|
230
|
+
- **SQLite only** ā no multi-dialect abstraction, no driver plugins
|
|
231
|
+
- **Type-safe by construction** ā two-phase builders, phantom types, `InferRow<T>`
|
|
232
|
+
- **Immutable** ā every chain method returns a new instance
|
|
233
|
+
- **No classes** ā functions and plain objects only
|
|
234
|
+
- **Parameterized** ā all queries use `?` placeholders, never string interpolation
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flint-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"bin": {
|
|
5
5
|
"flint": "./src/cli.ts"
|
|
6
6
|
},
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"lint": "oxlint",
|
|
41
41
|
"format": "oxfmt"
|
|
42
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@clack/prompts": "1.7.0",
|
|
45
|
+
"picocolors": "1.1.1"
|
|
46
|
+
},
|
|
43
47
|
"devDependencies": {
|
|
44
48
|
"@types/bun": "1.3.14",
|
|
45
49
|
"oxfmt": "0.57.0",
|
package/src/cli.ts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
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 { intro, 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';
|
|
12
15
|
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
14
17
|
// Config loading
|
|
@@ -93,24 +96,22 @@ async function importTableFolder(folderPath: string): Promise<unknown[]> {
|
|
|
93
96
|
async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config: FlintConfig): Promise<void> {
|
|
94
97
|
const name = typeof args.name === 'string' ? args.name : undefined;
|
|
95
98
|
const preview = args.preview === true;
|
|
99
|
+
const promptRename: RenamePrompt = (message, options) => select({ message: pc.bold(message), options }) as Promise<string | symbol>;
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
log.info(`Discovering schema from: ${config.schema}`);
|
|
98
102
|
const tables = await discoverTables(config.schema);
|
|
99
103
|
|
|
100
104
|
if (tables.length === 0) {
|
|
101
|
-
|
|
105
|
+
cancel('No table() definitions found in schema path.');
|
|
102
106
|
process.exit(1);
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
for (const t of tables) {
|
|
107
|
-
console.log(` - ${(t as Record<string, Record<string, unknown>>)._?.name ?? 'unknown'}`);
|
|
108
|
-
}
|
|
109
|
+
log.info(`Found ${tables.length} table(s): ${tables.map((t) => (t as Record<string, Record<string, unknown>>)._?.name ?? 'unknown').join(', ')}`);
|
|
109
110
|
|
|
110
111
|
if (preview) {
|
|
111
112
|
// Dynamic import of migration functions
|
|
112
113
|
const { serializeSchema } = await import('./migration/serialize.js');
|
|
113
|
-
const { diffSchemas, emptyState } = await import('./migration/diff.js');
|
|
114
|
+
const { diffSchemas, emptyState, resolveRenames } = await import('./migration/diff.js');
|
|
114
115
|
const { generateSQL } = await import('./migration/sql.js');
|
|
115
116
|
|
|
116
117
|
// Find latest state
|
|
@@ -132,20 +133,20 @@ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config:
|
|
|
132
133
|
if (!previousState) previousState = emptyState();
|
|
133
134
|
|
|
134
135
|
const currentState = serializeSchema(tables as AnyTable[]);
|
|
135
|
-
const
|
|
136
|
+
const rawOps = diffSchemas(previousState, currentState);
|
|
136
137
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
138
|
+
if (rawOps.length === 0) {
|
|
139
|
+
outro('Schema is already up to date.');
|
|
139
140
|
return;
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// Resolve renames interactively
|
|
144
|
+
const operations = await resolveRenames(rawOps, { interactive: true, prompt: promptRename });
|
|
145
|
+
|
|
142
146
|
const sql = generateSQL(operations);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.log(` Migrations dir: ${config.migrations}`);
|
|
147
|
-
console.log(`\n--- SQL ---\n${sql}\n`);
|
|
148
|
-
console.log('(dry run, no files written)');
|
|
147
|
+
log.info(`Operations: ${operations.length}`);
|
|
148
|
+
note(sql, 'SQL', { format: (text) => styleText('dim', text) });
|
|
149
|
+
log.info('(dry run, no files written)');
|
|
149
150
|
return;
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -153,13 +154,18 @@ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config:
|
|
|
153
154
|
const { generate } = await import('./migration/generate.js');
|
|
154
155
|
|
|
155
156
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
const migrationsDir = resolve(process.cwd(), config.migrations ?? './flint');
|
|
158
|
+
const result = await generate(tables as TableDef<any>[], migrationsDir, {
|
|
159
|
+
name,
|
|
160
|
+
interactive: true,
|
|
161
|
+
prompt: promptRename,
|
|
162
|
+
});
|
|
163
|
+
log.info(`Operations: ${result.operations.length}`);
|
|
164
|
+
note(result.sql, 'SQL', { format: (text) => styleText('dim', text) });
|
|
165
|
+
log.success(`Migration generated: ${migrationsDir}/${result.folderName}`);
|
|
160
166
|
} catch (err: unknown) {
|
|
161
167
|
if (err instanceof Error && err.message.includes('No changes detected')) {
|
|
162
|
-
|
|
168
|
+
outro('Schema is already up to date.');
|
|
163
169
|
} else {
|
|
164
170
|
throw err;
|
|
165
171
|
}
|
|
@@ -184,25 +190,25 @@ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config:
|
|
|
184
190
|
const status = getMigrationStatus(db, migrationsDir);
|
|
185
191
|
|
|
186
192
|
if (status.applied.length === 0 && status.pending.length === 0) {
|
|
187
|
-
|
|
193
|
+
outro('No migrations found.');
|
|
188
194
|
return;
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
if (status.applied.length > 0) {
|
|
192
|
-
|
|
198
|
+
log.info('Applied migrations:');
|
|
193
199
|
for (const m of status.applied) {
|
|
194
|
-
|
|
200
|
+
log.success(`${m.name} (${m.folderName})`);
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
if (status.pending.length > 0) {
|
|
199
|
-
|
|
205
|
+
log.warn('Pending migrations:');
|
|
200
206
|
for (const m of status.pending) {
|
|
201
|
-
|
|
207
|
+
log.message(` ā ${m.name} (${m.folderName})`);
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
|
|
205
|
-
|
|
211
|
+
log.info(`${status.applied.length} applied, ${status.pending.length} pending`);
|
|
206
212
|
return;
|
|
207
213
|
}
|
|
208
214
|
|
|
@@ -212,16 +218,16 @@ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config:
|
|
|
212
218
|
});
|
|
213
219
|
|
|
214
220
|
if (result.applied.length === 0) {
|
|
215
|
-
|
|
221
|
+
outro('No pending migrations ā database is up to date.');
|
|
216
222
|
} else {
|
|
217
|
-
|
|
218
|
-
for (const
|
|
219
|
-
|
|
223
|
+
log.info(`${dryRun ? 'Would apply' : 'Applied'} ${result.applied.length} migration(s):`);
|
|
224
|
+
for (const appliedName of result.applied) {
|
|
225
|
+
log.success(appliedName);
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
|
|
223
229
|
if (result.skipped.length > 0 && !dryRun) {
|
|
224
|
-
|
|
230
|
+
log.info(`Skipped ${result.skipped.length} already applied migration(s)`);
|
|
225
231
|
}
|
|
226
232
|
} finally {
|
|
227
233
|
db.close();
|
|
@@ -233,9 +239,7 @@ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config:
|
|
|
233
239
|
// ---------------------------------------------------------------------------
|
|
234
240
|
|
|
235
241
|
function printHelp(): void {
|
|
236
|
-
|
|
237
|
-
flint ā migration CLI for flint-orm
|
|
238
|
-
|
|
242
|
+
log.message(`
|
|
239
243
|
Usage:
|
|
240
244
|
flint <command> [options]
|
|
241
245
|
|
|
@@ -294,13 +298,17 @@ async function main(): Promise<void> {
|
|
|
294
298
|
await cmdMigrate(values, config);
|
|
295
299
|
break;
|
|
296
300
|
default:
|
|
297
|
-
|
|
301
|
+
cancel(`Unknown command: ${command}`);
|
|
298
302
|
printHelp();
|
|
299
303
|
process.exit(1);
|
|
300
304
|
}
|
|
301
305
|
}
|
|
302
306
|
|
|
303
307
|
main().catch((err) => {
|
|
304
|
-
|
|
308
|
+
if (isCancel(err) || err instanceof CancellationError) {
|
|
309
|
+
cancel('Operation cancelled.');
|
|
310
|
+
} else {
|
|
311
|
+
cancel(err.message ?? 'An error occurred');
|
|
312
|
+
}
|
|
305
313
|
process.exit(1);
|
|
306
314
|
});
|