UNPKG

flint-orm

Version:

Type-safe SQLite ORM for JavaScript

346 lines (260 loc) 10.5 kB
# Flint ORM Type-safe SQLite ORM for JavaScript ## Features - **Driver-agnostic** — use `bun:sqlite`, `better-sqlite3`, `@libsql/client`, `@tursodatabase/database`, or `@tursodatabase/sync` - **Type-safe queries** — full TypeScript inference for results, inserts, and updates - **Schema-first migrations** — define tables in code, generate and apply migrations - **Fluent query builder** — chainable API for SELECT, INSERT, UPDATE, DELETE, and JOINs - **Aggregate functions** — count, sum, avg, min, max with type inference - **Zero runtime dependencies** on the coredrivers are opt-in per subpath ## Install ```bash bun add flint-orm # or npm install flint-orm ``` **Using an AI agent?** Run `npx @tanstack/intent@latest install` to get agent-friendly skills for this library. ## Quick Start ```ts import { flint } from 'flint-orm/bun-sqlite'; import { table, text, integer, date } from 'flint-orm/table'; import { eq, and } from 'flint-orm/expressions'; // Define schema const users = table('users', { id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').unique(), age: integer('age'), createdAt: date('created_at').defaultNow(), }); // Connect const db = flint({ url: './app.db' }); // Insert await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute(); // Query const adults = await db.selectFrom(users).where(eq(users.age, 18)).execute(); // ^? { id: string; name: string; email: string; age: number; createdAt: Date }[] // Single row const alice = await db.selectFrom(users).where(eq(users.id, 'u1')).single().execute(); // ^? { id: string; name: string; email: string; age: number; createdAt: Date } | null ``` ## Schema Definition ### Columns ```ts import { table, text, integer, boolean, real, json, date, index } from 'flint-orm/table'; const posts = table('posts', { id: text('id').primaryKey(), title: text('title').notNull(), body: text('body'), published: boolean('published').default(false), views: integer('views').default(0).autoIncrement(), price: real('price'), metadata: json('metadata').default({}), createdAt: date('created_at').defaultNow(), updatedAt: date('updated_at').onUpdateTimestamp(), }); ``` ### Modifiers - `.primaryKey()` — mark as primary key - `.notNull()` — disallow NULL values - `.unique()` — add unique constraint - `.default(value)` — static default value - `.defaultFn(fn)` — dynamic default (called on insert when value is omitted) - `.references(target)` — foreign key reference - `.onDelete(action)` — foreign key ON DELETE action (`cascade`, `set null`, `set default`, `restrict`, `no action`) - `.onUpdate(action)` — foreign key ON UPDATE action (`cascade`, `set null`, `set default`, `restrict`, `no action`) - `.autoIncrement()` — integer auto-increment (integer columns only) - `.defaultNow()` — use `Date.now()` as default (date columns only) - `.onUpdateTimestamp()` — always set to `Date.now()` on update (date columns only) ### Indexes ```ts const users = table( 'users', { id: text('id').primaryKey(), email: text('email'), name: text('name'), }, (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)], ); ``` ### Type Inference ```ts import type { InferRow, InsertRow } from 'flint-orm/table'; type User = InferRow<typeof users>; // { id: string; name: string; email: string | null; age: number | null; createdAt: Date } type NewUser = InsertRow<typeof users>; // { id: string; name: string; email?: string | null; age?: number | null; createdAt?: Date } ``` `InsertRow` makes columns with auto-defaults (`integer`, `date`) optional. ## Queries ### SELECT ```ts // All rows const all = await db.selectFrom(users).execute(); // With conditions const active = await db.selectFrom(users).where(eq(users.active, true)).execute(); // Narrow columns const names = await db.selectFrom(users).columns(['id', 'name']).execute(); // ^? { id: string; name: string }[] // Single row const user = await db.selectFrom(users).where(eq(users.id, 'u1')).single().execute(); // ^? { ... } | null // Ordering and pagination const page = await db.selectFrom(users).orderBy('name', 'asc').limit(10).offset(20).execute(); // Distinct const unique = await db.selectFrom(users).columns(['email']).distinct().execute(); ``` ### INSERT ```ts // Single row await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute(); // Multiple rows await db .insert(users) .values([ { id: 'u1', name: 'Alice' }, { id: 'u2', name: 'Bob' }, ]) .execute(); // Return inserted rows const inserted = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute(); // ^? { id: string; name: string; ... }[] // Upsert await db .insert(users) .values({ id: 'u1', name: 'Alice' }) .onConflictDoUpdate({ target: users.id, set: { name: 'Alice' } }) .execute(); // Ignore conflicts await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute(); ``` ### UPDATE ```ts await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute(); // Return updated rows const updated = await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute(); ``` ### DELETE ```ts await db.delete(users).where(eq(users.id, 'u1')).execute(); // Return deleted rows const deleted = await db.delete(users).where(eq(users.id, 'u1')).returning().execute(); ``` ### JOINs ```ts // Auto-join via foreign key (posts.userId references users.id) const postsWithAuthors = await db.leftJoin(users).on(posts).execute(); // Explicit join condition const result = await db.leftJoin(users).on(posts, eq(posts.userId, users.id)).execute(); // Chain multiple joins const complex = await db .leftJoin(users) .on(posts, eq(posts.userId, users.id)) .on(comments, eq(comments.postId, posts.id)) .where(eq(users.id, 'u1')) .execute(); // Inner join const inner = await db.innerJoin(users).on(posts).execute(); ``` Join results are nested — each joined table's data appears under its table name: ```ts // result shape: { id: string; name: string; posts: { id: string; title: string }[] } ``` ### Aggregates ```ts const total = await db.count(users); const activeCount = await db.count(users, eq(users.active, true)); const totalViews = await db.sum(posts, posts.views); const avgAge = await db.avg(users, users.age); const minAge = await db.min(users, users.age); const maxAge = await db.max(users, users.age); ``` ### Batch (Transactions) ```ts await db.batch([db.insert(users).values({ id: 'u1', name: 'Alice' }), db.insert(posts).values({ id: 'p1', userId: 'u1', title: 'Hello' })]); ``` ### Raw SQL ```ts import { sql } from 'flint-orm'; const expr = sql`name = ${'Alice'} AND age > ${18}`; const result = await db.selectFrom(users).where(expr).execute(); // Direct execution await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); ``` ## Conditions All conditions are composable with `and()` and `or()`: ```ts import { eq, neq, gt, gte, lt, lte, and, or, isIn, isNotIn, isNull, isNotNull, like, glob, between } from 'flint-orm/expressions'; // Equality eq(users.name, 'Alice'); eq(posts.userId, users.id); // column-to-column comparison // Comparisons gt(users.age, 18); gte(users.age, 18); lt(users.age, 65); lte(users.age, 65); neq(users.id, 'excluded'); // Logical and(eq(users.active, true), gt(users.age, 18)); or(eq(users.role, 'admin'), eq(users.role, 'moderator')); // Sets isIn(users.id, ['u1', 'u2', 'u3']); isNotIn(users.id, ['excluded']); // Null checks isNull(users.deletedAt); isNotNull(users.email); // Pattern matching like(users.name, 'A%'); // SQL LIKE (% = any characters, _ = single char) glob(users.name, 'A*'); // SQL GLOB (* = any characters, ? = single char) // Range between(users.age, 18, 65); ``` ## Migrations Flint uses schema-first migrations. Define your tables in code, and the CLI generates migration files from the diff. ### Setup ```ts // flint.config.ts import { defineConfig } from 'flint-orm/config'; export default defineConfig({ driver: 'bun-sqlite', database: { url: './app.db', }, schema: './src/schema', migrations: './flint', }); ``` ### Generate ```bash flint generate # auto-detect changes flint generate --name init # name the migration flint generate --preview # dry run, show SQL without writing ``` ### Apply ```bash flint migrate # apply pending migrations flint migrate --status # show applied vs pending flint migrate --dry-run # show what would run ``` ### How It Works 1. `flint generate` serializes your `table()` definitions to JSON 2. Diffs against the last migration's `state.json` 3. Detects adds, drops, renames, and safe modifications 4. Prompts to confirm potential renames 5. Writes a migration folder with `migration.ts` (operations) + `state.json` (snapshot) 6. `flint migrate` reads pending migrations and executes them in order 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. ## Subpath Imports | Import | What's in it | | -------------------------- | --------------------------------------------------------------- | | `flint-orm/bun-sqlite` | `flint()` factory for bun:sqlite | | `flint-orm/better-sqlite3` | `flint()` factory for better-sqlite3 | | `flint-orm/libsql` | `flint()` factory for @libsql/client | | `flint-orm/libsql-web` | `flint()` factory for @libsql/client/web | | `flint-orm/turso` | `flint()` factory for @tursodatabase/database | | `flint-orm/turso-sync` | `flint()` factory for @tursodatabase/sync | | `flint-orm/table` | `table()`, column constructors, index builder, type utilities | | `flint-orm/expressions` | `eq`, `and`, `or`, `gt`, `like`, and all condition helpers | | `flint-orm/config` | `defineConfig()` | | `flint-orm/migration` | `generate()`, `migrate()`, `serializeSchema()`, `diffSchemas()` |