diff --git a/README.md b/README.md index b9af8960..963957e0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,116 @@ -# Typegres: PostgreSQL, expressed in TypeScript +# Typegres: SQL-over-RPC, Safely -[](https://github.com/ryanrasti/typegres/actions/workflows/main.yml) [](https://www.npmjs.com/package/typegres) [](https://opensource.org/licenses/MIT) +[](https://www.npmjs.com/package/typegres) [](https://opensource.org/licenses/MIT) -Import the full power of PostgreSQL as a TypeScript library. +A TypeScript API framework that lets clients compose any queries they need within boundaries you control. - +## Core Principles + +### 1. Decouple Your Interface from Your Schema - With All of Postgres, Fully Typed + +Wrap your tables in a stable, public interface. You can refactor your "private" tables and columns without ever breaking clients. + +```typescript +// api.ts +export class User extends Models.User { + // Your public interface stays stable as your schema evolves + createdAt() { + // Before: accessing from JSONB metadata + // return this.metadata['->>'](createdAt).cast(Timestamptz); + + // After: direct column access (schema refactored) + return this.created_at; + } +} +``` + +```typescript +// route.ts +// Compiles to the single SQL query you'd write manually. +const user = await User.select() + .orderBy((u) => u.createdAt(), { desc: true }) + .limit(1) + .one(tg); +``` + +### 2. Your Interface Defines Your Data Boundaries + +Allowed operations are just methods on your interface, including relations and mutations. Everything fully composable and typed. + +```typescript +// api.ts +export class User extends Models.User { + todos() { + return Todo.select().where((t) => t.user_id.eq(this.id)); + } +} + +export class Todo extends Models.Todos { + update({ completed }: { completed: boolean }) { + return update(Todo) + .set((t) => ({ completed })) + .where((t) => t.id.eq(this.id)); + } +} +``` + +```typescript +// route.ts +const user = ... + +// The only way to get a todo is through a user: +const todo = await user.todos() + .where((t) => t.id.eq(todoId)) + .one(tg); + +// The only way to update a todo is by getting it from a user: +await todo.update({ completed: true }).execute(tg); +``` + +### 3. Expose your API over RPC, Safely (coming soon) + +Give clients a composable query builder with your unescapable data boundaries. Compose queries in the client with every Postgres feature (joins, window functions, CTEs, etc.) and function as primitives. + +```typescript +// api.ts +export class User extends Models.User { + // ... +} + +export class Todo extends Models.Todos { + // ... +} + +export class Api extends RpcTarget { + getUserFromToken(token: string) { + return User.select((u) => new User(u)).where((u) => u.token.eq(token)); + } +} + +// Clients receive composable query builders +// not flat results +``` + +```typescript +// frontend.tsx +export function TodoList({ searchQuery }: { searchQuery: string }) { + const todos = useTypegresQuery((user) => user.todos() + // Arbitrarily compose your base query... + .select((t) => ({ id: t.id, title: t.title })) + // ...using any Postgres function such as `ilike`: + .where((t) => t.title.ilike(`%${searchQuery}%`)) + .execute(tg) + ); + + return ( +