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.
- package/API.md +208 -211
- package/README.md +341 -0
- package/dist/index.js +288 -255
- package/package.json +46 -11
- 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()` |
|