flint-orm 0.3.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 +151 -316
  2. package/README.md +237 -134
  3. package/dist/index.js +288 -255
  4. package/package.json +42 -11
  5. package/src/cli.ts +58 -11
package/README.md CHANGED
@@ -1,238 +1,341 @@
1
- # flint-orm
1
+ # Flint ORM
2
2
 
3
- A minimal, type-safe SQLite ORM. One dialect, one runtime, no abstraction overhead.
3
+ A type-safe SQLite ORM for JavaScript. One schema, any driver.
4
4
 
5
- ## Why
5
+ ## Features
6
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.
7
+ - **Driver-agnostic** use `bun:sqlite`, `better-sqlite3`, `@libsql/client`, `@tursodatabase/database`, or `@tursodatabase/sync`
8
+ - **Type-safe queries** — full TypeScript inference for results, inserts, and updates
9
+ - **Schema-first migrations** — define tables in code, generate and apply migrations
10
+ - **Fluent query builder** — chainable API for SELECT, INSERT, UPDATE, DELETE, and JOINs
11
+ - **Aggregate functions** — count, sum, avg, min, max with type inference
12
+ - **Zero runtime dependencies** on the core — drivers are opt-in per subpath
8
13
 
9
14
  ## Install
10
15
 
11
16
  ```bash
12
- npm install flint-orm
13
17
  bun add flint-orm
18
+ # or
19
+ npm install flint-orm
14
20
  ```
15
21
 
16
22
  ## Quick Start
17
23
 
18
24
  ```ts
19
- import { flint, table, text, integer, eq } from 'flint-orm';
25
+ import { flint } from 'flint-orm/bun-sqlite';
26
+ import { table, text, integer, date } from 'flint-orm/table';
27
+ import { eq, and } from 'flint-orm/expressions';
20
28
 
29
+ // Define schema
21
30
  const users = table('users', {
22
31
  id: text('id').primaryKey(),
23
32
  name: text('name').notNull(),
24
33
  email: text('email').unique(),
25
34
  age: integer('age'),
35
+ createdAt: date('created_at').defaultNow(),
26
36
  });
27
37
 
28
- const db = flint({ url: 'app.db' });
38
+ // Connect
39
+ const db = flint({ url: './app.db' });
29
40
 
30
41
  // Insert
31
- db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
42
+ await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
32
43
 
33
44
  // 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 }
45
+ const adults = await db.select().from(users).where(eq(users.age, 18)).execute();
46
+ // ^? { id: string; name: string; email: string; age: number; createdAt: Date }[]
36
47
 
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();
48
+ // Single row
49
+ const alice = await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
50
+ // ^? { id: string; name: string; email: string; age: number; createdAt: Date } | null
42
51
  ```
43
52
 
44
- ## Features
53
+ ## Schema Definition
45
54
 
46
- ### Schema
55
+ ### Columns
47
56
 
48
57
  ```ts
49
- import { table, text, integer, boolean, json, date, real, index } from 'flint-orm';
58
+ import { table, text, integer, boolean, real, json, date, index } from 'flint-orm/table';
59
+
60
+ const posts = table('posts', {
61
+ id: text('id').primaryKey(),
62
+ title: text('title').notNull(),
63
+ body: text('body'),
64
+ published: boolean('published').default(false),
65
+ views: integer('views').default(0).autoIncrement(),
66
+ price: real('price'),
67
+ metadata: json('metadata').default({}),
68
+ createdAt: date('created_at').defaultNow(),
69
+ updatedAt: date('updated_at').onUpdateTimestamp(),
70
+ });
71
+ ```
50
72
 
73
+ ### Modifiers
74
+
75
+ - `.primaryKey()` — mark as primary key
76
+ - `.notNull()` — disallow NULL values
77
+ - `.unique()` — add unique constraint
78
+ - `.default(value)` — static default value
79
+ - `.defaultFn(fn)` — dynamic default (called on insert when value is omitted)
80
+ - `.references(target)` — foreign key reference
81
+ - `.autoIncrement()` — integer auto-increment (integer columns only)
82
+ - `.defaultNow()` — use `Date.now()` as default (date columns only)
83
+ - `.onUpdateTimestamp()` — always set to `Date.now()` on update (date columns only)
84
+
85
+ ### Indexes
86
+
87
+ ```ts
51
88
  const users = table(
52
89
  'users',
53
90
  {
54
91
  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),
92
+ email: text('email'),
93
+ name: text('name'),
63
94
  },
64
- (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_org').on(t.orgId)],
95
+ (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
65
96
  );
66
97
  ```
67
98
 
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 | |
99
+ ### Type Inference
100
+
101
+ ```ts
102
+ import type { InferRow, InsertRow } from 'flint-orm/table';
103
+
104
+ type User = InferRow<typeof users>;
105
+ // { id: string; name: string; email: string | null; age: number | null; createdAt: Date }
106
+
107
+ type NewUser = InsertRow<typeof users>;
108
+ // { id: string; name: string; email?: string | null; age?: number | null; createdAt?: Date }
109
+ ```
110
+
111
+ `InsertRow` makes columns with auto-defaults (`integer`, `date`) optional.
76
112
 
77
- ### Query Builder
113
+ ## Queries
78
114
 
79
- Two-phase builders prevent invalid queries at compile time:
115
+ ### SELECT
80
116
 
81
117
  ```ts
82
- // SELECT
83
- db.select().from(users).columns(['id', 'name']).where(eq(users.active, true)).orderBy('name').limit(10).execute();
118
+ // All rows
119
+ const all = await db.select().from(users).execute();
84
120
 
85
- // INSERT
86
- db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
121
+ // With conditions
122
+ const active = await db.select().from(users).where(eq(users.active, true)).execute();
87
123
 
88
- // UPSERT
89
- db.insert(users)
90
- .values({ id: 'u1', name: 'Alice' })
91
- .onConflictDoUpdate({ target: users.id, set: { name: 'Bob' } })
92
- .execute();
124
+ // Narrow columns
125
+ const names = await db.select().from(users).columns(['id', 'name']).execute();
126
+ // ^? { id: string; name: string }[]
93
127
 
94
- // UPDATE
95
- db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
128
+ // Single row
129
+ const user = await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
130
+ // ^? { ... } | null
96
131
 
97
- // DELETE
98
- db.delete(users).where(eq(users.id, 'u1')).returning().execute();
132
+ // Ordering and pagination
133
+ const page = await db.select().from(users).orderBy('name', 'asc').limit(10).offset(20).execute();
134
+
135
+ // Distinct
136
+ const unique = await db.select().from(users).columns(['email']).distinct().execute();
99
137
  ```
100
138
 
101
- ### Joins
139
+ ### INSERT
102
140
 
103
141
  ```ts
104
- const orders = table('orders', {
105
- id: text('id').primaryKey(),
106
- userId: text('userId').references(users.id),
107
- total: integer('total').notNull(),
108
- });
142
+ // Single row
143
+ await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
144
+
145
+ // Multiple rows
146
+ await db
147
+ .insert(users)
148
+ .values([
149
+ { id: 'u1', name: 'Alice' },
150
+ { id: 'u2', name: 'Bob' },
151
+ ])
152
+ .execute();
109
153
 
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 }] }]
154
+ // Return inserted rows
155
+ const inserted = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
156
+ // ^? { id: string; name: string; ... }[]
157
+
158
+ // Upsert
159
+ await db
160
+ .insert(users)
161
+ .values({ id: 'u1', name: 'Alice' })
162
+ .onConflictDoUpdate({ target: users.id, set: { name: 'Alice' } })
163
+ .execute();
113
164
 
114
- // Multi-join
115
- db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
165
+ // Ignore conflicts
166
+ await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
116
167
  ```
117
168
 
118
- ### Aggregates
169
+ ### UPDATE
119
170
 
120
171
  ```ts
121
- db.count(users);
122
- db.sum(orders, orders.total);
123
- db.avg(orders, orders.total, eq(orders.userId, 'u1'));
172
+ await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
173
+
174
+ // Return updated rows
175
+ const updated = await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
124
176
  ```
125
177
 
126
- ### Batch
178
+ ### DELETE
127
179
 
128
180
  ```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'))]);
181
+ await db.delete(users).where(eq(users.id, 'u1')).execute();
182
+
183
+ // Return deleted rows
184
+ const deleted = await db.delete(users).where(eq(users.id, 'u1')).returning().execute();
130
185
  ```
131
186
 
132
- ### Raw SQL
187
+ ### JOINs
133
188
 
134
189
  ```ts
135
- db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
190
+ // Auto-join via foreign key (posts.userId references users.id)
191
+ const postsWithAuthors = await db.leftJoin(users).on(posts).execute();
192
+
193
+ // Explicit join condition
194
+ const result = await db.leftJoin(users).on(posts, eq(posts.userId, users.id)).execute();
195
+
196
+ // Chain multiple joins
197
+ const complex = await db
198
+ .leftJoin(users)
199
+ .on(posts, eq(posts.userId, users.id))
200
+ .on(comments, eq(comments.postId, posts.id))
201
+ .where(eq(users.id, 'u1'))
202
+ .execute();
203
+
204
+ // Inner join
205
+ const inner = await db.innerJoin(users).on(posts).execute();
136
206
  ```
137
207
 
138
- ## Conditions
208
+ Join results are nested — each joined table's data appears under its table name:
139
209
 
140
210
  ```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();
211
+ // result shape: { id: string; name: string; posts: { id: string; title: string }[] }
147
212
  ```
148
213
 
149
- ## Types
214
+ ### Aggregates
150
215
 
151
216
  ```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
217
+ const total = await db.count(users);
218
+ const activeCount = await db.count(users, eq(users.active, true));
219
+ const totalViews = await db.sum(posts, posts.views);
220
+ const avgAge = await db.avg(users, users.age);
221
+ const minAge = await db.min(users, users.age);
222
+ const maxAge = await db.max(users, users.age);
156
223
  ```
157
224
 
158
- ## Migration System
225
+ ### Batch (Transactions)
159
226
 
160
- ### CLI
227
+ ```ts
228
+ await db.batch([db.insert(users).values({ id: 'u1', name: 'Alice' }), db.insert(posts).values({ id: 'p1', userId: 'u1', title: 'Hello' })]);
229
+ ```
161
230
 
162
- ```bash
163
- # Generate migration from schema changes
164
- flint generate --name init_schema
231
+ ### Raw SQL
165
232
 
166
- # Preview SQL without writing files
167
- flint generate --preview
233
+ ```ts
234
+ import { sql } from 'flint-orm';
168
235
 
169
- # Apply pending migrations
170
- flint migrate
236
+ const expr = sql`name = ${'Alice'} AND age > ${18}`;
237
+ const result = await db.select().from(users).where(expr).execute();
171
238
 
172
- # Check status
173
- flint migrate --status
239
+ // Direct execution
240
+ await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
174
241
  ```
175
242
 
176
- ### Config
243
+ ## Conditions
244
+
245
+ All conditions are composable with `and()` and `or()`:
177
246
 
178
247
  ```ts
179
- // flint.config.ts
180
- import { defineConfig } from 'flint-orm/config';
248
+ import { eq, neq, gt, gte, lt, lte, and, or, isIn, isNotIn, isNull, isNotNull, like, glob, between } from 'flint-orm/expressions';
181
249
 
182
- export default defineConfig({
183
- schema: './src/schema',
184
- migrations: './flint',
185
- database: { url: './app.db' },
186
- });
187
- ```
250
+ // Equality
251
+ eq(users.name, 'Alice');
252
+ eq(posts.userId, users.id); // column-to-column comparison
188
253
 
189
- ### How It Works
254
+ // Comparisons
255
+ gt(users.age, 18);
256
+ gte(users.age, 18);
257
+ lt(users.age, 65);
258
+ lte(users.age, 65);
259
+ neq(users.id, 'excluded');
190
260
 
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
261
+ // Logical
262
+ and(eq(users.active, true), gt(users.age, 18));
263
+ or(eq(users.role, 'admin'), eq(users.role, 'moderator'));
194
264
 
195
- ### Programmatic
265
+ // Sets
266
+ isIn(users.id, ['u1', 'u2', 'u3']);
267
+ isNotIn(users.id, ['excluded']);
196
268
 
197
- ```ts
198
- import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
199
- ```
269
+ // Null checks
270
+ isNull(users.deletedAt);
271
+ isNotNull(users.email);
200
272
 
201
- ## CLI
273
+ // Pattern matching
274
+ like(users.name, 'A%'); // SQL LIKE (% = any characters, _ = single char)
275
+ glob(users.name, 'A*'); // SQL GLOB (* = any characters, ? = single char)
202
276
 
277
+ // Range
278
+ between(users.age, 18, 65);
203
279
  ```
204
- flint <command> [options]
205
280
 
206
- Commands:
207
- generate Generate a new migration from schema changes
208
- migrate Apply pending migrations
209
- status Show migration status
281
+ ## Migrations
210
282
 
211
- Options:
212
- --name, -n Migration name
213
- --preview, -p Show SQL without writing files
214
- --status Show which migrations are pending/applied
215
- ```
283
+ Flint uses schema-first migrations. Define your tables in code, and the CLI generates migration files from the diff.
216
284
 
217
- ## Error Handling
285
+ ### Setup
218
286
 
219
287
  ```ts
220
- import { FlintValidationError, FlintQueryError } from 'flint-orm';
288
+ // flint.config.ts
289
+ import { defineConfig } from 'flint-orm/config';
290
+
291
+ export default defineConfig({
292
+ driver: 'bun-sqlite',
293
+ database: {
294
+ url: './app.db',
295
+ },
296
+ schema: './src/schema',
297
+ migrations: './flint',
298
+ });
221
299
  ```
222
300
 
223
- | Error | When |
224
- | ---------------------- | ----------------------------- |
225
- | `FlintValidationError` | Invalid query construction |
226
- | `FlintQueryError` | Runtime SQL execution failure |
301
+ ### Generate
302
+
303
+ ```bash
304
+ flint generate # auto-detect changes
305
+ flint generate --name init # name the migration
306
+ flint generate --preview # dry run, show SQL without writing
307
+ ```
227
308
 
228
- ## Philosophy
309
+ ### Apply
229
310
 
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
311
+ ```bash
312
+ flint migrate # apply pending migrations
313
+ flint migrate --status # show applied vs pending
314
+ flint migrate --dry-run # show what would run
315
+ ```
235
316
 
236
- ## License
317
+ ### How It Works
237
318
 
238
- MIT
319
+ 1. `flint generate` serializes your `table()` definitions to JSON
320
+ 2. Diffs against the last migration's `state.json`
321
+ 3. Detects adds, drops, renames, and safe modifications
322
+ 4. Prompts to confirm potential renames
323
+ 5. Writes a migration folder with `migration.ts` (operations) + `state.json` (snapshot)
324
+ 6. `flint migrate` reads pending migrations and executes them in order
325
+
326
+ Unsafe changes (type changes, primary key changes) throw an error and must be handled manually.
327
+
328
+ ## Subpath Imports
329
+
330
+ | Import | What's in it |
331
+ | -------------------------- | --------------------------------------------------------------- |
332
+ | `flint-orm/bun-sqlite` | `flint()` factory for bun:sqlite |
333
+ | `flint-orm/better-sqlite3` | `flint()` factory for better-sqlite3 |
334
+ | `flint-orm/libsql` | `flint()` factory for @libsql/client |
335
+ | `flint-orm/libsql-web` | `flint()` factory for @libsql/client/web |
336
+ | `flint-orm/turso` | `flint()` factory for @tursodatabase/database |
337
+ | `flint-orm/turso-sync` | `flint()` factory for @tursodatabase/sync |
338
+ | `flint-orm/table` | `table()`, column constructors, index builder, type utilities |
339
+ | `flint-orm/expressions` | `eq`, `and`, `or`, `gt`, `like`, and all condition helpers |
340
+ | `flint-orm/config` | `defineConfig()` |
341
+ | `flint-orm/migration` | `generate()`, `migrate()`, `serializeSchema()`, `diffSchemas()` |