flint-orm 0.3.0 → 0.4.1
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 +158 -317
- package/README.md +239 -134
- package/dist/index.js +288 -255
- package/package.json +43 -12
- package/src/cli.ts +0 -314
package/README.md
CHANGED
|
@@ -1,238 +1,343 @@
|
|
|
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
|
+
- `.onDelete(action)` — foreign key ON DELETE action (`cascade`, `set null`, `set default`, `restrict`, `no action`)
|
|
82
|
+
- `.onUpdate(action)` — foreign key ON UPDATE action (`cascade`, `set null`, `set default`, `restrict`, `no action`)
|
|
83
|
+
- `.autoIncrement()` — integer auto-increment (integer columns only)
|
|
84
|
+
- `.defaultNow()` — use `Date.now()` as default (date columns only)
|
|
85
|
+
- `.onUpdateTimestamp()` — always set to `Date.now()` on update (date columns only)
|
|
86
|
+
|
|
87
|
+
### Indexes
|
|
88
|
+
|
|
89
|
+
```ts
|
|
51
90
|
const users = table(
|
|
52
91
|
'users',
|
|
53
92
|
{
|
|
54
93
|
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),
|
|
94
|
+
email: text('email'),
|
|
95
|
+
name: text('name'),
|
|
63
96
|
},
|
|
64
|
-
(t) => [index('idx_users_email').on(t.email).unique(), index('
|
|
97
|
+
(t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
|
|
65
98
|
);
|
|
66
99
|
```
|
|
67
100
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
### Type Inference
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import type { InferRow, InsertRow } from 'flint-orm/table';
|
|
105
|
+
|
|
106
|
+
type User = InferRow<typeof users>;
|
|
107
|
+
// { id: string; name: string; email: string | null; age: number | null; createdAt: Date }
|
|
108
|
+
|
|
109
|
+
type NewUser = InsertRow<typeof users>;
|
|
110
|
+
// { id: string; name: string; email?: string | null; age?: number | null; createdAt?: Date }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`InsertRow` makes columns with auto-defaults (`integer`, `date`) optional.
|
|
76
114
|
|
|
77
|
-
|
|
115
|
+
## Queries
|
|
78
116
|
|
|
79
|
-
|
|
117
|
+
### SELECT
|
|
80
118
|
|
|
81
119
|
```ts
|
|
82
|
-
//
|
|
83
|
-
db.select().from(users).
|
|
120
|
+
// All rows
|
|
121
|
+
const all = await db.select().from(users).execute();
|
|
84
122
|
|
|
85
|
-
//
|
|
86
|
-
db.
|
|
123
|
+
// With conditions
|
|
124
|
+
const active = await db.select().from(users).where(eq(users.active, true)).execute();
|
|
87
125
|
|
|
88
|
-
//
|
|
89
|
-
db.
|
|
90
|
-
|
|
91
|
-
.onConflictDoUpdate({ target: users.id, set: { name: 'Bob' } })
|
|
92
|
-
.execute();
|
|
126
|
+
// Narrow columns
|
|
127
|
+
const names = await db.select().from(users).columns(['id', 'name']).execute();
|
|
128
|
+
// ^? { id: string; name: string }[]
|
|
93
129
|
|
|
94
|
-
//
|
|
95
|
-
db.
|
|
130
|
+
// Single row
|
|
131
|
+
const user = await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
|
|
132
|
+
// ^? { ... } | null
|
|
96
133
|
|
|
97
|
-
//
|
|
98
|
-
db.
|
|
134
|
+
// Ordering and pagination
|
|
135
|
+
const page = await db.select().from(users).orderBy('name', 'asc').limit(10).offset(20).execute();
|
|
136
|
+
|
|
137
|
+
// Distinct
|
|
138
|
+
const unique = await db.select().from(users).columns(['email']).distinct().execute();
|
|
99
139
|
```
|
|
100
140
|
|
|
101
|
-
###
|
|
141
|
+
### INSERT
|
|
102
142
|
|
|
103
143
|
```ts
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
144
|
+
// Single row
|
|
145
|
+
await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
|
|
146
|
+
|
|
147
|
+
// Multiple rows
|
|
148
|
+
await db
|
|
149
|
+
.insert(users)
|
|
150
|
+
.values([
|
|
151
|
+
{ id: 'u1', name: 'Alice' },
|
|
152
|
+
{ id: 'u2', name: 'Bob' },
|
|
153
|
+
])
|
|
154
|
+
.execute();
|
|
109
155
|
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
//
|
|
156
|
+
// Return inserted rows
|
|
157
|
+
const inserted = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
|
|
158
|
+
// ^? { id: string; name: string; ... }[]
|
|
159
|
+
|
|
160
|
+
// Upsert
|
|
161
|
+
await db
|
|
162
|
+
.insert(users)
|
|
163
|
+
.values({ id: 'u1', name: 'Alice' })
|
|
164
|
+
.onConflictDoUpdate({ target: users.id, set: { name: 'Alice' } })
|
|
165
|
+
.execute();
|
|
113
166
|
|
|
114
|
-
//
|
|
115
|
-
db.
|
|
167
|
+
// Ignore conflicts
|
|
168
|
+
await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
|
|
116
169
|
```
|
|
117
170
|
|
|
118
|
-
###
|
|
171
|
+
### UPDATE
|
|
119
172
|
|
|
120
173
|
```ts
|
|
121
|
-
db.
|
|
122
|
-
|
|
123
|
-
|
|
174
|
+
await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
|
|
175
|
+
|
|
176
|
+
// Return updated rows
|
|
177
|
+
const updated = await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
|
|
124
178
|
```
|
|
125
179
|
|
|
126
|
-
###
|
|
180
|
+
### DELETE
|
|
127
181
|
|
|
128
182
|
```ts
|
|
129
|
-
|
|
183
|
+
await db.delete(users).where(eq(users.id, 'u1')).execute();
|
|
184
|
+
|
|
185
|
+
// Return deleted rows
|
|
186
|
+
const deleted = await db.delete(users).where(eq(users.id, 'u1')).returning().execute();
|
|
130
187
|
```
|
|
131
188
|
|
|
132
|
-
###
|
|
189
|
+
### JOINs
|
|
133
190
|
|
|
134
191
|
```ts
|
|
135
|
-
|
|
192
|
+
// Auto-join via foreign key (posts.userId references users.id)
|
|
193
|
+
const postsWithAuthors = await db.leftJoin(users).on(posts).execute();
|
|
194
|
+
|
|
195
|
+
// Explicit join condition
|
|
196
|
+
const result = await db.leftJoin(users).on(posts, eq(posts.userId, users.id)).execute();
|
|
197
|
+
|
|
198
|
+
// Chain multiple joins
|
|
199
|
+
const complex = await db
|
|
200
|
+
.leftJoin(users)
|
|
201
|
+
.on(posts, eq(posts.userId, users.id))
|
|
202
|
+
.on(comments, eq(comments.postId, posts.id))
|
|
203
|
+
.where(eq(users.id, 'u1'))
|
|
204
|
+
.execute();
|
|
205
|
+
|
|
206
|
+
// Inner join
|
|
207
|
+
const inner = await db.innerJoin(users).on(posts).execute();
|
|
136
208
|
```
|
|
137
209
|
|
|
138
|
-
|
|
210
|
+
Join results are nested — each joined table's data appears under its table name:
|
|
139
211
|
|
|
140
212
|
```ts
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
db.select()
|
|
144
|
-
.from(users)
|
|
145
|
-
.where(and(eq(users.active, true), gt(users.age, 18)))
|
|
146
|
-
.execute();
|
|
213
|
+
// result shape: { id: string; name: string; posts: { id: string; title: string }[] }
|
|
147
214
|
```
|
|
148
215
|
|
|
149
|
-
|
|
216
|
+
### Aggregates
|
|
150
217
|
|
|
151
218
|
```ts
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
219
|
+
const total = await db.count(users);
|
|
220
|
+
const activeCount = await db.count(users, eq(users.active, true));
|
|
221
|
+
const totalViews = await db.sum(posts, posts.views);
|
|
222
|
+
const avgAge = await db.avg(users, users.age);
|
|
223
|
+
const minAge = await db.min(users, users.age);
|
|
224
|
+
const maxAge = await db.max(users, users.age);
|
|
156
225
|
```
|
|
157
226
|
|
|
158
|
-
|
|
227
|
+
### Batch (Transactions)
|
|
159
228
|
|
|
160
|
-
|
|
229
|
+
```ts
|
|
230
|
+
await db.batch([db.insert(users).values({ id: 'u1', name: 'Alice' }), db.insert(posts).values({ id: 'p1', userId: 'u1', title: 'Hello' })]);
|
|
231
|
+
```
|
|
161
232
|
|
|
162
|
-
|
|
163
|
-
# Generate migration from schema changes
|
|
164
|
-
flint generate --name init_schema
|
|
233
|
+
### Raw SQL
|
|
165
234
|
|
|
166
|
-
|
|
167
|
-
|
|
235
|
+
```ts
|
|
236
|
+
import { sql } from 'flint-orm';
|
|
168
237
|
|
|
169
|
-
|
|
170
|
-
|
|
238
|
+
const expr = sql`name = ${'Alice'} AND age > ${18}`;
|
|
239
|
+
const result = await db.select().from(users).where(expr).execute();
|
|
171
240
|
|
|
172
|
-
|
|
173
|
-
|
|
241
|
+
// Direct execution
|
|
242
|
+
await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
|
|
174
243
|
```
|
|
175
244
|
|
|
176
|
-
|
|
245
|
+
## Conditions
|
|
246
|
+
|
|
247
|
+
All conditions are composable with `and()` and `or()`:
|
|
177
248
|
|
|
178
249
|
```ts
|
|
179
|
-
|
|
180
|
-
import { defineConfig } from 'flint-orm/config';
|
|
250
|
+
import { eq, neq, gt, gte, lt, lte, and, or, isIn, isNotIn, isNull, isNotNull, like, glob, between } from 'flint-orm/expressions';
|
|
181
251
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
database: { url: './app.db' },
|
|
186
|
-
});
|
|
187
|
-
```
|
|
252
|
+
// Equality
|
|
253
|
+
eq(users.name, 'Alice');
|
|
254
|
+
eq(posts.userId, users.id); // column-to-column comparison
|
|
188
255
|
|
|
189
|
-
|
|
256
|
+
// Comparisons
|
|
257
|
+
gt(users.age, 18);
|
|
258
|
+
gte(users.age, 18);
|
|
259
|
+
lt(users.age, 65);
|
|
260
|
+
lte(users.age, 65);
|
|
261
|
+
neq(users.id, 'excluded');
|
|
190
262
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
263
|
+
// Logical
|
|
264
|
+
and(eq(users.active, true), gt(users.age, 18));
|
|
265
|
+
or(eq(users.role, 'admin'), eq(users.role, 'moderator'));
|
|
194
266
|
|
|
195
|
-
|
|
267
|
+
// Sets
|
|
268
|
+
isIn(users.id, ['u1', 'u2', 'u3']);
|
|
269
|
+
isNotIn(users.id, ['excluded']);
|
|
196
270
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
271
|
+
// Null checks
|
|
272
|
+
isNull(users.deletedAt);
|
|
273
|
+
isNotNull(users.email);
|
|
200
274
|
|
|
201
|
-
|
|
275
|
+
// Pattern matching
|
|
276
|
+
like(users.name, 'A%'); // SQL LIKE (% = any characters, _ = single char)
|
|
277
|
+
glob(users.name, 'A*'); // SQL GLOB (* = any characters, ? = single char)
|
|
202
278
|
|
|
279
|
+
// Range
|
|
280
|
+
between(users.age, 18, 65);
|
|
203
281
|
```
|
|
204
|
-
flint <command> [options]
|
|
205
282
|
|
|
206
|
-
|
|
207
|
-
generate Generate a new migration from schema changes
|
|
208
|
-
migrate Apply pending migrations
|
|
209
|
-
status Show migration status
|
|
283
|
+
## Migrations
|
|
210
284
|
|
|
211
|
-
|
|
212
|
-
--name, -n Migration name
|
|
213
|
-
--preview, -p Show SQL without writing files
|
|
214
|
-
--status Show which migrations are pending/applied
|
|
215
|
-
```
|
|
285
|
+
Flint uses schema-first migrations. Define your tables in code, and the CLI generates migration files from the diff.
|
|
216
286
|
|
|
217
|
-
|
|
287
|
+
### Setup
|
|
218
288
|
|
|
219
289
|
```ts
|
|
220
|
-
|
|
290
|
+
// flint.config.ts
|
|
291
|
+
import { defineConfig } from 'flint-orm/config';
|
|
292
|
+
|
|
293
|
+
export default defineConfig({
|
|
294
|
+
driver: 'bun-sqlite',
|
|
295
|
+
database: {
|
|
296
|
+
url: './app.db',
|
|
297
|
+
},
|
|
298
|
+
schema: './src/schema',
|
|
299
|
+
migrations: './flint',
|
|
300
|
+
});
|
|
221
301
|
```
|
|
222
302
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
303
|
+
### Generate
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
flint generate # auto-detect changes
|
|
307
|
+
flint generate --name init # name the migration
|
|
308
|
+
flint generate --preview # dry run, show SQL without writing
|
|
309
|
+
```
|
|
227
310
|
|
|
228
|
-
|
|
311
|
+
### Apply
|
|
229
312
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
313
|
+
```bash
|
|
314
|
+
flint migrate # apply pending migrations
|
|
315
|
+
flint migrate --status # show applied vs pending
|
|
316
|
+
flint migrate --dry-run # show what would run
|
|
317
|
+
```
|
|
235
318
|
|
|
236
|
-
|
|
319
|
+
### How It Works
|
|
237
320
|
|
|
238
|
-
|
|
321
|
+
1. `flint generate` serializes your `table()` definitions to JSON
|
|
322
|
+
2. Diffs against the last migration's `state.json`
|
|
323
|
+
3. Detects adds, drops, renames, and safe modifications
|
|
324
|
+
4. Prompts to confirm potential renames
|
|
325
|
+
5. Writes a migration folder with `migration.ts` (operations) + `state.json` (snapshot)
|
|
326
|
+
6. `flint migrate` reads pending migrations and executes them in order
|
|
327
|
+
|
|
328
|
+
Unsafe changes (type changes, primary key changes, FK modifications, UNIQUE changes, DEFAULT removal) automatically emit a `rebuildTable` operation that recreates the table with the new schema and copies data safely within a transaction.
|
|
329
|
+
|
|
330
|
+
## Subpath Imports
|
|
331
|
+
|
|
332
|
+
| Import | What's in it |
|
|
333
|
+
| -------------------------- | --------------------------------------------------------------- |
|
|
334
|
+
| `flint-orm/bun-sqlite` | `flint()` factory for bun:sqlite |
|
|
335
|
+
| `flint-orm/better-sqlite3` | `flint()` factory for better-sqlite3 |
|
|
336
|
+
| `flint-orm/libsql` | `flint()` factory for @libsql/client |
|
|
337
|
+
| `flint-orm/libsql-web` | `flint()` factory for @libsql/client/web |
|
|
338
|
+
| `flint-orm/turso` | `flint()` factory for @tursodatabase/database |
|
|
339
|
+
| `flint-orm/turso-sync` | `flint()` factory for @tursodatabase/sync |
|
|
340
|
+
| `flint-orm/table` | `table()`, column constructors, index builder, type utilities |
|
|
341
|
+
| `flint-orm/expressions` | `eq`, `and`, `or`, `gt`, `like`, and all condition helpers |
|
|
342
|
+
| `flint-orm/config` | `defineConfig()` |
|
|
343
|
+
| `flint-orm/migration` | `generate()`, `migrate()`, `serializeSchema()`, `diffSchemas()` |
|