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/README.md CHANGED
@@ -0,0 +1,341 @@
1
+ # Flint ORM
2
+
3
+ A type-safe SQLite ORM for JavaScript. One schema, any driver.
4
+
5
+ ## Features
6
+
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
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ bun add flint-orm
18
+ # or
19
+ npm install flint-orm
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
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';
28
+
29
+ // Define schema
30
+ const users = table('users', {
31
+ id: text('id').primaryKey(),
32
+ name: text('name').notNull(),
33
+ email: text('email').unique(),
34
+ age: integer('age'),
35
+ createdAt: date('created_at').defaultNow(),
36
+ });
37
+
38
+ // Connect
39
+ const db = flint({ url: './app.db' });
40
+
41
+ // Insert
42
+ await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
43
+
44
+ // Query
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 }[]
47
+
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
51
+ ```
52
+
53
+ ## Schema Definition
54
+
55
+ ### Columns
56
+
57
+ ```ts
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
+ ```
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
88
+ const users = table(
89
+ 'users',
90
+ {
91
+ id: text('id').primaryKey(),
92
+ email: text('email'),
93
+ name: text('name'),
94
+ },
95
+ (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
96
+ );
97
+ ```
98
+
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.
112
+
113
+ ## Queries
114
+
115
+ ### SELECT
116
+
117
+ ```ts
118
+ // All rows
119
+ const all = await db.select().from(users).execute();
120
+
121
+ // With conditions
122
+ const active = await db.select().from(users).where(eq(users.active, true)).execute();
123
+
124
+ // Narrow columns
125
+ const names = await db.select().from(users).columns(['id', 'name']).execute();
126
+ // ^? { id: string; name: string }[]
127
+
128
+ // Single row
129
+ const user = await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
130
+ // ^? { ... } | null
131
+
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();
137
+ ```
138
+
139
+ ### INSERT
140
+
141
+ ```ts
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();
153
+
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();
164
+
165
+ // Ignore conflicts
166
+ await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
167
+ ```
168
+
169
+ ### UPDATE
170
+
171
+ ```ts
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();
176
+ ```
177
+
178
+ ### DELETE
179
+
180
+ ```ts
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();
185
+ ```
186
+
187
+ ### JOINs
188
+
189
+ ```ts
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();
206
+ ```
207
+
208
+ Join results are nested — each joined table's data appears under its table name:
209
+
210
+ ```ts
211
+ // result shape: { id: string; name: string; posts: { id: string; title: string }[] }
212
+ ```
213
+
214
+ ### Aggregates
215
+
216
+ ```ts
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);
223
+ ```
224
+
225
+ ### Batch (Transactions)
226
+
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
+ ```
230
+
231
+ ### Raw SQL
232
+
233
+ ```ts
234
+ import { sql } from 'flint-orm';
235
+
236
+ const expr = sql`name = ${'Alice'} AND age > ${18}`;
237
+ const result = await db.select().from(users).where(expr).execute();
238
+
239
+ // Direct execution
240
+ await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
241
+ ```
242
+
243
+ ## Conditions
244
+
245
+ All conditions are composable with `and()` and `or()`:
246
+
247
+ ```ts
248
+ import { eq, neq, gt, gte, lt, lte, and, or, isIn, isNotIn, isNull, isNotNull, like, glob, between } from 'flint-orm/expressions';
249
+
250
+ // Equality
251
+ eq(users.name, 'Alice');
252
+ eq(posts.userId, users.id); // column-to-column comparison
253
+
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');
260
+
261
+ // Logical
262
+ and(eq(users.active, true), gt(users.age, 18));
263
+ or(eq(users.role, 'admin'), eq(users.role, 'moderator'));
264
+
265
+ // Sets
266
+ isIn(users.id, ['u1', 'u2', 'u3']);
267
+ isNotIn(users.id, ['excluded']);
268
+
269
+ // Null checks
270
+ isNull(users.deletedAt);
271
+ isNotNull(users.email);
272
+
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)
276
+
277
+ // Range
278
+ between(users.age, 18, 65);
279
+ ```
280
+
281
+ ## Migrations
282
+
283
+ Flint uses schema-first migrations. Define your tables in code, and the CLI generates migration files from the diff.
284
+
285
+ ### Setup
286
+
287
+ ```ts
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
+ });
299
+ ```
300
+
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
+ ```
308
+
309
+ ### Apply
310
+
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
+ ```
316
+
317
+ ### How It Works
318
+
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()` |