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.
- package/API.md +151 -316
- package/README.md +237 -134
- package/dist/index.js +288 -255
- package/package.json +42 -11
- package/src/cli.ts +58 -11
package/README.md
CHANGED
|
@@ -1,238 +1,341 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Flint ORM
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A type-safe SQLite ORM for JavaScript. One schema, any driver.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
35
|
-
// { id:
|
|
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
|
-
//
|
|
38
|
-
db.
|
|
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
|
-
##
|
|
53
|
+
## Schema Definition
|
|
45
54
|
|
|
46
|
-
###
|
|
55
|
+
### Columns
|
|
47
56
|
|
|
48
57
|
```ts
|
|
49
|
-
import { table, text, integer, boolean, json, date,
|
|
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
|
-
|
|
56
|
-
|
|
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('
|
|
95
|
+
(t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
|
|
65
96
|
);
|
|
66
97
|
```
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
113
|
+
## Queries
|
|
78
114
|
|
|
79
|
-
|
|
115
|
+
### SELECT
|
|
80
116
|
|
|
81
117
|
```ts
|
|
82
|
-
//
|
|
83
|
-
db.select().from(users).
|
|
118
|
+
// All rows
|
|
119
|
+
const all = await db.select().from(users).execute();
|
|
84
120
|
|
|
85
|
-
//
|
|
86
|
-
db.
|
|
121
|
+
// With conditions
|
|
122
|
+
const active = await db.select().from(users).where(eq(users.active, true)).execute();
|
|
87
123
|
|
|
88
|
-
//
|
|
89
|
-
db.
|
|
90
|
-
|
|
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
|
-
//
|
|
95
|
-
db.
|
|
128
|
+
// Single row
|
|
129
|
+
const user = await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
|
|
130
|
+
// ^? { ... } | null
|
|
96
131
|
|
|
97
|
-
//
|
|
98
|
-
db.
|
|
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
|
-
###
|
|
139
|
+
### INSERT
|
|
102
140
|
|
|
103
141
|
```ts
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
//
|
|
111
|
-
const
|
|
112
|
-
//
|
|
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
|
-
//
|
|
115
|
-
db.
|
|
165
|
+
// Ignore conflicts
|
|
166
|
+
await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
|
|
116
167
|
```
|
|
117
168
|
|
|
118
|
-
###
|
|
169
|
+
### UPDATE
|
|
119
170
|
|
|
120
171
|
```ts
|
|
121
|
-
db.
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
###
|
|
178
|
+
### DELETE
|
|
127
179
|
|
|
128
180
|
```ts
|
|
129
|
-
|
|
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
|
-
###
|
|
187
|
+
### JOINs
|
|
133
188
|
|
|
134
189
|
```ts
|
|
135
|
-
|
|
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
|
-
|
|
208
|
+
Join results are nested — each joined table's data appears under its table name:
|
|
139
209
|
|
|
140
210
|
```ts
|
|
141
|
-
|
|
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
|
-
|
|
214
|
+
### Aggregates
|
|
150
215
|
|
|
151
216
|
```ts
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
225
|
+
### Batch (Transactions)
|
|
159
226
|
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
# Generate migration from schema changes
|
|
164
|
-
flint generate --name init_schema
|
|
231
|
+
### Raw SQL
|
|
165
232
|
|
|
166
|
-
|
|
167
|
-
|
|
233
|
+
```ts
|
|
234
|
+
import { sql } from 'flint-orm';
|
|
168
235
|
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
const expr = sql`name = ${'Alice'} AND age > ${18}`;
|
|
237
|
+
const result = await db.select().from(users).where(expr).execute();
|
|
171
238
|
|
|
172
|
-
|
|
173
|
-
|
|
239
|
+
// Direct execution
|
|
240
|
+
await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
|
|
174
241
|
```
|
|
175
242
|
|
|
176
|
-
|
|
243
|
+
## Conditions
|
|
244
|
+
|
|
245
|
+
All conditions are composable with `and()` and `or()`:
|
|
177
246
|
|
|
178
247
|
```ts
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
265
|
+
// Sets
|
|
266
|
+
isIn(users.id, ['u1', 'u2', 'u3']);
|
|
267
|
+
isNotIn(users.id, ['excluded']);
|
|
196
268
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
269
|
+
// Null checks
|
|
270
|
+
isNull(users.deletedAt);
|
|
271
|
+
isNotNull(users.email);
|
|
200
272
|
|
|
201
|
-
|
|
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
|
-
|
|
207
|
-
generate Generate a new migration from schema changes
|
|
208
|
-
migrate Apply pending migrations
|
|
209
|
-
status Show migration status
|
|
281
|
+
## Migrations
|
|
210
282
|
|
|
211
|
-
|
|
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
|
-
|
|
285
|
+
### Setup
|
|
218
286
|
|
|
219
287
|
```ts
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
309
|
+
### Apply
|
|
229
310
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
317
|
+
### How It Works
|
|
237
318
|
|
|
238
|
-
|
|
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()` |
|