From 221ca05a3e08a56b351a6fce02384c41c2e9b854 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Mon, 17 Nov 2025 20:17:30 -0800 Subject: [PATCH] update copy for latest direction --- README.md | 183 +++--- site/app/page.tsx | 787 ++++++++++++++++++------ site/src/components/SyntaxHighlight.tsx | 66 +- 3 files changed, 758 insertions(+), 278 deletions(-) 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 -[![CI](https://github.com/ryanrasti/typegres/actions/workflows/main.yml/badge.svg)](https://github.com/ryanrasti/typegres/actions/workflows/main.yml) [![npm version](https://img.shields.io/npm/v/typegres.svg)](https://www.npmjs.com/package/typegres) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) +[![npm version](https://img.shields.io/npm/v/typegres.svg)](https://www.npmjs.com/package/typegres) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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. -![Typegres Demo GIF](https://raw.githubusercontent.com/ryanrasti/typegres/main/site/public/typegres_landing_page_demo.gif) +## 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 ( + + ); +} +``` > [!WARNING] > **Developer Preview**: Typegres is experimental and not production-ready. The API is evolving rapidly. Try the [playground](https://typegres.com/play/) and star the repo to follow along! @@ -37,7 +143,7 @@ const activeUsers = await select( { from: db.users, where: (u) => u.isActive, - } + }, ).execute(tg); console.log(activeUsers); @@ -51,73 +157,6 @@ See the [examples](https://github.com/ryanrasti/typegres/tree/main/examples) dir - **Try it live**: https://typegres.com/play/ - **API Reference**: https://typegres.com/api/ -## Key Features & Design Goals - -While traditional ORMs and query builders abstract over multiple SQL dialects, Typegres goes all-in on PostgreSQL to provide the most powerful and type-safe experience possible. In a single import, you can access the full power of Postgres with complete TypeScript type safety. - -- **Not an ORM** โ€“ Direct access to every PostgreSQL function as TypeScript methods -- **Zero SQL strings** โ€“ Write complex queries in pure TypeScript with full type inference -- **One language** โ€“ No context switching between SQL and application code - -Focus on learning Postgres itself โ€” Typegres just gives you autocomplete, type-checking, and all other benefits of TypeScript. - -## Advanced example - -```typescript -// Find all authors who have published more than 10 posts -const authorCounts = select( - (p) => ({ - author_id: p.author_id, - postCount: p.id.count(), - }), - { - from: db.posts, - groupBy: (p) => [p.author_id], - } -); - -const prolificAuthors = await select( - (ac, { u }) => ({ - id: u.id, - name: u.name, - totalPosts: ac.postCount, - }), - { - from: authorCounts.asFromItemjoin(db.users, "u", (ac, { u }) => ac.author_id["="](u.id)), - where: (ac) => ac.postCount[">"](10), - } -).execute(tg); - -// Type of prolificAuthors is { id: number; name: string; totalPosts: bigint }[] -``` - -## Roadmap - -๐Ÿงช Current Features (Developer Preview) - -The project is currently in an early but powerful state. The core foundation is in place: - -- [x] Complete Postgres API: Generated types, operators, and functions for the entire Postgres surface. -- [x] Query Builder Core: A proof-of-concept query builder with SELECT, JOIN, and GROUP BY. -- [x] Interactive Playground: A live, in-browser demo powered by PGlite. - -๐Ÿš€ Road to v1.0: Production Readiness - -The immediate priority is building a rock-solid foundation to make Typegres stable and ready for production use. This includes: -- [ ] Full query builder: Full support for aggregation, window functions, CTEs. -- [ ] Full Mutation Support: Robust implementations for INSERT, UPDATE, and DELETE. -- [ ] Essential Keywords: First-class support for IS NULL, AND, OR, IN, BETWEEN, etc. -- [ ] Comprehensive Test Suite: Dramatically expand test coverage across all features. -- [ ] Advanced Type Support: Refined typing for JSONB, arrays, and custom enums. -- [ ] Inline Documentation: Add TSDoc comments for better in-editor help and discoverability. - -๐Ÿ”ญ Long-Term Vision: A New Data Layer - -Once that stable v1.0 foundation is in place, the roadmap will focus on solving deeper, more fundamental problems that can make significant headway into resolving the object-relational impedance mismatch. -- [ ] Truly Type-Safe Migrations: Typesafe migrations without codegen. -- [ ] First-Class Relations: A simple and composable API for relations that feels natural in TypeScript. -- [ ] An Even More Idiomatic API: Write code that feels even more like TypeScript but produces clean, predictable SQL. - ## Project Structure - `src/` - Main library source code diff --git a/site/app/page.tsx b/site/app/page.tsx index fd5b8afc..fae4b723 100644 --- a/site/app/page.tsx +++ b/site/app/page.tsx @@ -1,249 +1,628 @@ "use client"; -import { motion } from "framer-motion"; -import { Code, Database, Shield, Github } from "lucide-react"; +import { Github, ArrowUpRight, Check, X, AlertTriangle } from "lucide-react"; +import { DarkModeToggle } from "@/components/DarkModeToggle"; +import { SyntaxHighlight } from "@/components/SyntaxHighlight"; -import { DarkModeToggle } from '@/components/DarkModeToggle'; +type CodeLanguage = "typescript" | "sql" | "tsx" | "javascript"; + +interface CodeExample { + title: string; + description: string; + leftCode: string; + rightCode: string; + leftLabel: string; + rightLabel: string; + leftLanguage: CodeLanguage; + rightLanguage: CodeLanguage; + badge?: string; + leftDiff?: boolean; +} export default function HomePage() { + const codeExamples: CodeExample[] = [ + { + title: "1. Decouple Your Interface from Your Schema - With All of Postgres, Fully Typed", + description: + 'Wrap your tables in a stable, public interface. You can refactor your "private" tables and columns without ever breaking clients.', + leftCode: `export class User extends Models.User { + // Your public interface stays stable as your schema evolves + createdAt() { + // - return this.metadata['->>']('createdAt').cast(Timestamptz); + // + return this.created_at; + } +}`, + leftDiff: true, + rightCode: `// 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);`, + leftLabel: "api.ts", + rightLabel: "route.ts", + leftLanguage: "typescript", + rightLanguage: "typescript", + }, + { + title: "2. Your Interface Defines Your Data Boundaries", + description: + "Allowed operations are just methods on your interface, including relations and mutations. Everything fully composable and typed.", + leftCode: `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)); + } +}`, + rightCode: ` +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);`, + leftLabel: "api.ts", + rightLabel: "route.ts", + leftLanguage: "typescript", + rightLanguage: "typescript", + }, + { + title: "3. Expose your API over RPC, Safely", + description: + "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.", + leftCode: `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`, + rightCode: `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 ( + + ); +}`, + leftLabel: "api.ts", + rightLabel: "frontend.tsx", + leftLanguage: "typescript", + rightLanguage: "tsx", + badge: "Coming Soon", + }, + ]; + return ( <> -
-
- -
+ + {/* Fixed Header */} +
+ +
-
-
-
- {/* Left side - Text and CTAs */} - -

- PostgreSQL, -
- expressed in TypeScript -

- - - Import the full power of Postgres as a TypeScript library. - - - - - Try it Live - - - - View on GitHub - - - -
- - {/* Right side - GIF */} - - - Typegres Demo - + {/* Main Content */} +
+ {/* Hero Section */} +
+
+ +
+
+

+ + SQL-over-RPC, Safely + +

+ +

+ A TypeScript API framework that lets clients compose any queries they need within boundaries you + control. +

-
+
- {/* Features section */} -
-
- -

- What makes Typegres different -

-

- A new approach to working with SQL - currently in developer preview -

-
+ {/* Three Tenets with Code */} +
+
+ {codeExamples.map((example, index) => ( +
+
+ {/* Title and Description */} +
+

+ {example.title} + {example.badge && ( + + {example.badge} + + )} +

+

+ {example.description} +

+
- - -
-
- -

- Type-Safe Query Composition -

-

- Every PostgreSQL expression is typed and composable. Build complex queries from typed primitives - from individual columns to CTEs. -

-
- - - -
-
- -

- Zero SQL Strings -

-

- Full support for 3000+ PostgreSQL built-in functions. Write complex queries as pure TypeScript. -

-
- - - -
-
- -

- Learn PostgreSQL, Not Your ORM -

-

- Master PostgreSQL's actual capabilities instead of an abstraction layer. Your knowledge transfers directly to raw SQL. -

+ {/* Two code blocks side by side with arrow */} +
+
+ {/* Left Code */} +
+
+ +
+
+ {example.leftLabel} +
+ +
+ +
+
+
+ + {/* Right Code */} +
+
+ +
+
+ {example.rightLabel} +
+ +
+ +
+
+
+
+ + {/* Arrow indicator in the middle */} +
+
+ + + +
+
+
- - +
+ ))}
- {/* Footer CTA Section */} -
-
- -

- Ready to dive in? -

-

- Experience the power of PostgreSQL with the safety and convenience of TypeScript. -

-
+ {/* Bottom CTA Section */} +
+
+
+
+

+ Ready to build the next generation of database APIs? +

+

+ Experience the power of composable, capability-first database queries with full type safety and + AI-native architecture. +

+
+ + - +
+
+
+ + {/* FAQ Section */} +
+
+

+ Frequently Asked Questions +

+
+ {/* Comparison Table 1: API Frameworks */} +
+

+ API Frameworks: Typegres vs Hasura vs PostgREST +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypegresHasuraPostgREST
Schema coupling +
+ + Decoupled +
+
+
+ + Tightly coupled +
+
+
+ + Tightly coupled +
+
Client composition +
+ + Full composable queries +
+
+
+ + GraphQL queries +
+
+
+ + REST endpoints +
+
Authorization +
+ + Capability-based +
+
+
+ + RLS + permissions +
+
+
+ + RLS +
+
Refactor safety +
+ + Safe schema evolution +
+
+
+ + Breaking changes +
+
+
+ + Breaking changes +
+
Maturity/Ecosystem +
+ + Early/Experimental +
+
+
+ + Mature +
+
+
+ + Mature +
+
+
+
+ + {/* Comparison Table 2: Typegres vs ORMs */} +
+

+ Typegres vs ORMs (Prisma, Drizzle, etc.) +

+

+ Unlike ORMs (which are local dev tools), Typegres is designed for exposing your database over RPC. You + can use it alongside your ORM, or as a standalone API layer. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Typegres + ORMs (Prisma, Drizzle, etc.) +
Purpose +
+ + API framework +
+
+
+ + Local dev tool +
+
Security model +
+ + Capability-based +
+
+
+ + N/A +
+
Refactor safety +
+ + Safe schema evolution +
+
+
+ + Breaking changes +
+
Maturity/Ecosystem +
+ + Early/Experimental +
+
+
+ + Mature +
+
+
+
+ + {/* Other FAQs */} +
+
+

+ Q: What about raw SQL + Row Level Security (RLS)? +

+

+ tl;dr Raw SQL + RLS has existed for almost a decade. Still, no one lets untrusted + SQL run against their database. +

+

+ A raw SQL approach has many drawbacks, but the most fundamental is applications need application + code, not just SQL (e.g., calling external APIs, JSON validation). +

+
+
+

+ Q: What about query performance? +

+

+ Every query maps directly 1:1 to the single Postgres query you'd expect. +

+

+ Note that relations are expressed as correlated subqueries, not raw joins, which modern versions of + Postgres should optimize. This idea may be revisited later. +

+
+
+

+ Q: How does the RPC layer actually work? +

+

+ Using the amazing{" "} + + Cap'n Web + {" "} + project. It enables an RPC layer that naturally allows composing over a set of classes/methods + safely in a single RPC call. +

+
+
+

+ Q: What's the actual security model under the hood? +

+

+ The model is capability-based security. Instead of reactive security (a blacklist) + the framework explicitly guides you to define your allowed surface area (your classes and methods) + and enforces that all queries go through it. +

+
+
+

Q: What about DoS?

+

+ Currently we recommend query timeouts. (Other options include cost calculations and limiting number + of tables allowed per query). +

+
+
+

+ Q: What's the project status? +

+

+ This is a research preview and not ready for production use. Try the{" "} + + playground + {" "} + to see the latest features and open a discussion or issue on{" "} + + GitHub + {" "} + if you have questions. +

+
+
+
diff --git a/site/src/components/SyntaxHighlight.tsx b/site/src/components/SyntaxHighlight.tsx index 86ffb393..20bff064 100644 --- a/site/src/components/SyntaxHighlight.tsx +++ b/site/src/components/SyntaxHighlight.tsx @@ -4,11 +4,73 @@ import { Highlight, themes } from "prism-react-renderer"; interface SyntaxHighlightProps { code: string; - language: "typescript" | "sql"; + language: "typescript" | "sql" | "tsx" | "javascript"; className?: string; + diff?: boolean; } -export function SyntaxHighlight({ code, language, className = "" }: SyntaxHighlightProps) { +export function SyntaxHighlight({ code, language, className = "", diff = false }: SyntaxHighlightProps) { + if (diff) { + const lines = code.split('\n'); + const processedLines = lines.map(line => { + if (line.trim().startsWith('// -')) { + return { type: 'removed' as const, content: line.replace(/^(\s*)\/\/\s*-\s*/, '$1') }; + } else if (line.trim().startsWith('// +')) { + return { type: 'added' as const, content: line.replace(/^(\s*)\/\/\s*\+\s*/, '$1') }; + } + return { type: 'normal' as const, content: line }; + }); + + const processedCode = processedLines.map(l => l.content).join('\n'); + + return ( + + {({ className: highlightClassName, style, tokens, getLineProps, getTokenProps }) => ( +
+            {tokens.map((line, i) => {
+              const lineType = processedLines[i]?.type || 'normal';
+              const lineProps = getLineProps({ line });
+              
+              let bgColor = 'transparent';
+              if (lineType === 'removed') {
+                bgColor = 'rgba(239, 68, 68, 0.3)'; // red-500 with higher opacity
+              } else if (lineType === 'added') {
+                bgColor = 'rgba(34, 197, 94, 0.3)'; // green-500 with higher opacity
+              }
+
+              return (
+                
+ {line.map((token, key) => ( + + ))} +
+ ); + })} +
+ )} +
+ ); + } + return (