UNPKG

flint-orm

Version:

Type-safe SQLite ORM for JavaScript

624 lines (439 loc) 15.8 kB
# Flint ORM — API Reference A type-safe, driver-agnostic SQLite ORM for JavaScript. One schema, any driver. ## Installation ```bash bun add flint-orm # or npm install flint-orm ``` ## Quick Start ```ts import { flint } from 'flint-orm/bun-sqlite'; import { table, text, integer, date } from 'flint-orm/table'; import { eq } from 'flint-orm/expressions'; // Define schema const users = table('users', { id: text().primaryKey(), name: text().notNull(), email: text().unique(), age: integer(), createdAt: date().defaultNow(), }); // Connect const db = flint({ url: './app.db' }); // Query const user = await db.selectFrom(users).where(eq(users.id, 'u1')).single().execute(); // { id: "u1", name: "Alice", email: "alice@example.com", age: 30, createdAt: Date } ``` --- ## Config Create `flint.config.ts` in your project root: ```ts import { defineConfig } from 'flint-orm/config'; export default defineConfig({ driver: 'bun-sqlite', // 'bun-sqlite' | 'better-sqlite3' | 'libsql' | 'libsql-web' | 'turso' | 'turso-sync' database: { url: './app.db', authToken: '...', // for libsql/libsql-web/turso-sync only }, schema: './src/schema', migrations: './flint', }); ``` --- ## Schema ### `table(name, columns, indexFn?)` Define a table. Columns live as direct properties. SQL metadata is under `._`. ```ts import { table, text, integer, boolean, index } from 'flint-orm/table'; const users = table('users', { id: text().primaryKey(), name: text().notNull(), email: text().unique(), active: boolean().default(true), age: integer(), }); ``` ### `snakeCase.table(name, columns, indexFn?)` Auto-converts camelCase keys to snake_case SQL names. ```ts import { snakeCase, text } from 'flint-orm/table'; const users = snakeCase.table('users', { id: text().primaryKey(), // SQL: id firstName: text().notNull(), // SQL: first_name createdAt: text(), // SQL: created_at }); ``` ### Column Types | Function | TS Type | SQLite Storage | Notes | | ----------- | --------- | ------------------ | ------------------------------------------------ | | `text()` | `string` | TEXT | | | `integer()` | `number` | INTEGER | Supports `.autoIncrement()` | | `boolean()` | `boolean` | INTEGER (0/1) | Encodes/decodes automatically | | `json<T>()` | `T` | TEXT (JSON) | Generic, encodes/decodes automatically | | `real()` | `number` | REAL | | | `date()` | `Date` | INTEGER (epoch ms) | Supports `.defaultNow()`, `.onUpdateTimestamp()` | ### Column Modifiers Every column supports chaining: ```ts text() .primaryKey() // PRIMARY KEY .notNull() // NOT NULL .unique() // UNIQUE .default('hello') // DEFAULT 'hello' .defaultFn(() => new Date()) // DEFAULT (computed at insert) .references(otherColumn) // REFERENCES otherColumn .onDelete('cascade') // ON DELETE CASCADE .onUpdate('set null'); // ON UPDATE SET NULL ``` **Integer-only:** ```ts integer().autoIncrement(); // AUTOINCREMENT ``` **Date-only:** ```ts date() .defaultNow() // DEFAULT (current epoch ms) .onUpdateTimestamp(); // Always set to now on UPDATE ``` ### `InferRow<T>` Derives the row type from a table definition. ```ts import type { InferRow } from 'flint-orm/table'; type UserRow = InferRow<typeof users>; // { id: string; name: string; email: string | null; active: boolean; age: number | null; createdAt: Date } ``` ### `InsertRow<T>` Row type for INSERT. Columns with defaults or autoIncrement are optional. ```ts import type { InsertRow } from 'flint-orm/table'; type UserInsert = InsertRow<typeof users>; // { id: string; name: string; email?: string; active?: boolean; age?: number; createdAt?: Date } ``` --- ## Indexes ### `index(name).on(columns).unique()` Define indexes via the table callback. Chainable API. ```ts const users = table( 'users', { id: text().primaryKey(), email: text(), name: text(), }, (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)], ); ``` --- ## Query Builder All `execute()` methods return `Promise<T>` always `await` regardless of driver. ### `db.selectFrom(table)` Start a SELECT query. Call `.columns()` to narrow, `.where()` to filter, `.execute()` to run. ```ts await db.selectFrom(users).execute(); // SELECT * FROM users ``` ### `.columns(keys)` Narrow which columns appear in the result. ```ts await db.selectFrom(users).columns(['id', 'name']).execute(); // SELECT id, name FROM users // Returns: { id: string; name: string }[] ``` ### `.where(condition)` Filter rows. ```ts await db.selectFrom(users).where(eq(users.active, true)).execute(); // SELECT * FROM users WHERE active = 1 ``` ### `.single()` Return one row or null instead of an array. Adds `LIMIT 1`. ```ts await db.selectFrom(users).where(eq(users.id, 'u1')).single().execute(); // SELECT * FROM users WHERE id = ? LIMIT 1 // Returns: UserRow | null ``` ### `.orderBy(key, direction?)` Sort results. Default direction is `"asc"`. ```ts await db.selectFrom(users).orderBy('name', 'desc').execute(); // SELECT * FROM users ORDER BY name DESC ``` ### `.limit(n)` Limit the number of results. ```ts await db.selectFrom(users).limit(10).execute(); // SELECT * FROM users LIMIT 10 ``` ### `.offset(n)` Skip N rows. ```ts await db.selectFrom(users).limit(10).offset(20).execute(); // SELECT * FROM users LIMIT 10 OFFSET 20 ``` ### `.distinct()` Return unique rows. ```ts await db.selectFrom(users).columns(['name']).distinct().execute(); // SELECT DISTINCT name FROM users ``` --- ### `db.insert(table).values(row)` Insert one or more rows. Two-phase: `.values()` is required before `.execute()`. ```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(); ``` ### `.returning()` Return the inserted row(s) instead of void. Pass an array to narrow which columns are returned. ```ts const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute(); // Returns: { id: string; name: string; ... }[] const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id', 'name']).execute(); // Returns: { id: string; name: string }[] ``` ### `.onConflictDoNothing()` Skip the insert if a row with the same primary key already exists. ```ts await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute(); ``` ### `.onConflictDoUpdate()` Update specific columns when a row with the same primary key already exists (upsert). ```ts await db .insert(users) .values({ id: 'u1', name: 'Alice' }) .onConflictDoUpdate({ target: users.id, set: { name: 'Alice Updated' }, }) .execute(); ``` --- ### `db.update(table).set(partial).where(condition)` Update rows. Two-phase: `.set()` is required before `.execute()`. ```ts await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute(); ``` Multiple `.set()` calls merge: ```ts await db.update(users).set({ name: 'Bob' }).set({ email: 'bob@example.com' }).where(eq(users.id, 'u1')).execute(); ``` ### `.returning()` Return the updated row(s) instead of void. ```ts const updated = await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute(); // Returns: { id: string; name: string; ... }[] ``` --- ### `db.delete(table).where(condition)` Delete rows. ```ts await db.delete(users).where(eq(users.id, 'u1')).execute(); ``` ### `.returning()` Return the deleted row(s) instead of void. ```ts const deleted = await db.delete(users).where(eq(users.id, 'u1')).returning().execute(); // Returns: { id: string; name: string; ... }[] ``` --- ## Joins ### `db.leftJoin(parent).on(child, condition?)` LEFT JOIN. Returns all parent rows, with matching child data nested under the child table name. ```ts const orders = table('orders', { id: text().primaryKey(), userId: text().notNull().references(users.id), total: integer().notNull(), }); // With explicit condition await db.leftJoin(users).on(orders, eq(orders.userId, users.id)).execute(); // Auto-join from foreign key (if .references() is defined) await db.leftJoin(users).on(orders).execute(); ``` ### `db.innerJoin(parent).on(child, condition?)` INNER JOIN. Returns only rows where both tables match. ```ts await db.innerJoin(users).on(orders, eq(orders.userId, users.id)).execute(); ``` ### Join Result Shape Joins return **nested** results, not flat-merged: ```ts [ { id: 'u1', name: 'Alice', orders: [ { id: 'o1', userId: 'u1', total: 100 }, { id: 'o2', userId: 'u1', total: 200 }, ], }, ]; ``` ### Join + Columns Narrow parent columns with `.columns()`: ```ts await db.leftJoin(users).on(orders).columns(['id', 'name']).execute(); // Returns: { id: string; name: string; orders: OrderRow[] }[] ``` ### `.single()` on Joins ```ts await db.leftJoin(users).on(orders).single().execute(); // Returns: { id: string; name: string; orders: OrderRow[] } | null ``` ### Multi-Join Chain multiple joins: ```ts await db.leftJoin(users).on(orders).leftJoin(orders).on(orderItems).execute(); ``` --- ## Conditions All conditions are imported from `flint-orm/expressions`. ### Comparison ```ts import { eq, neq, gt, gte, lt, lte } from 'flint-orm/expressions'; eq(column, value); // column = value eq(left, right); // left = right (column-to-column) neq(column, value); // column != value gt(column, value); // column > value gte(column, value); // column >= value lt(column, value); // column < value lte(column, value); // column <= value ``` ### Range ```ts import { between } from 'flint-orm/expressions'; between(column, low, high); // column BETWEEN low AND high ``` ### Null Checks ```ts import { isNull, isNotNull } from 'flint-orm/expressions'; isNull(column); // column IS NULL isNotNull(column); // column IS NOT NULL ``` ### Array ```ts import { isIn, isNotIn } from 'flint-orm/expressions'; isIn(column, values); // column IN (?, ?, ...) isNotIn(column, values); // column NOT IN (?, ?, ...) ``` ### Pattern Matching ```ts import { like, glob } from 'flint-orm/expressions'; like(column, pattern); // column LIKE ? (% and _ wildcards, case-insensitive) glob(column, pattern); // column GLOB ? (* and ? wildcards, case-sensitive) ``` ### Logical ```ts import { and, or } from 'flint-orm/expressions'; and(...conditions); // cond1 AND cond2 AND ... or(...conditions); // (cond1 OR cond2 OR ...) ``` --- ## Aggregates Aggregate functions are methods on the `db` object. They return `Promise<T>`. ```ts const total = await db.count(users); const active = 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 Run multiple queries atomically in a single transaction. ```ts await db.batch([ db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }), db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1')), ]); ``` --- ## Raw SQL ### `db.$run(sql, ...params)` Execute raw SQL directly against the database. ```ts await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); await db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice'); ``` ### Tagged Template SQL Build parameterized SQL expressions with automatic placeholder handling. ```ts import { sql } from 'flint-orm'; const expr = sql`name = ${'Alice'} AND age > ${18}`; // { sql: "name = ? AND age > ?", params: ["Alice", 18] } const result = await db.selectFrom(users).where(expr).execute(); ``` --- ## Migration System ### CLI ```bash flint generate --name init_schema # Generate migration flint generate --preview # Preview SQL without writing flint migrate # Apply pending migrations flint migrate --status # Show applied vs pending flint migrate --dry-run # Preview without executing ``` ### Programmatic API ```ts import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration'; // Serialize table definitions to JSON const state = serializeSchema([users, orders]); // Diff two schema states const operations = diffSchemas(previousState, currentState); // Generate SQL from operations const sql = generateSQL(operations); // Generate a migration folder const result = await generate([users, orders], './flint', { name: 'init_schema', interactive: true }); // Apply pending migrations const result = await migrate(executor, { migrationsDir: './flint' }); // Check migration status const status = await getMigrationStatus(executor, './flint'); ``` ### Migration Operations ```ts import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration'; ``` | Operation | SQL Generated | | -------------- | -------------------------------------- | | `addTable` | `CREATE TABLE ...` | | `dropTable` | `DROP TABLE ...` | | `renameTable` | `ALTER TABLE ... RENAME TO ...` | | `addColumn` | `ALTER TABLE ... ADD COLUMN ...` | | `dropColumn` | `ALTER TABLE ... DROP COLUMN ...` | | `renameColumn` | `ALTER TABLE ... RENAME COLUMN ... TO` | | `createIndex` | `CREATE [UNIQUE] INDEX ...` | | `dropIndex` | `DROP INDEX ...` | | `modifyColumn` | `ALTER TABLE ... ALTER COLUMN ...` | | `modifyIndex` | `DROP INDEX IF EXISTS ...; CREATE ...` | | `rebuildTable` | Temp table copy drop rename | --- ## Types | Type | Description | | ----------------- | ----------------------------------------------------------------------------------------- | | `TableDef<T>` | Table definition with hidden `._` metadata | | `ColumnDef<T, S>` | Column definition with phantom types | | `InferRow<T>` | Derives row type from table definition | | `InsertRow<T>` | Derives insert type (defaults are optional) | | `Executor` | Database executor interface (all, get, run, transaction) | | `SQLExpression` | `{ sql: string; params: unknown[] }` | | `Executable` | Anything with a `.toSQL()` method (for `batch()`) | | `Driver` | `'bun-sqlite' \| 'better-sqlite3' \| 'libsql' \| 'libsql-web' \| 'turso' \| 'turso-sync'` | | `RebuildTableOp` | Migration op that recreates a table with a new schema | --- ## Error Classes | Class | When | | ---------------------- | ----------------------------------------------------------------- | | `FlintValidationError` | Invalid query construction (e.g., no primary key for `.single()`) | | `FlintQueryError` | Runtime SQL execution failure |