diff --git a/docs/go.md b/docs/go.md new file mode 100644 index 00000000..934b6e5b --- /dev/null +++ b/docs/go.md @@ -0,0 +1,140 @@ +# Go Type Generator + +Generates Go struct definitions from your PostgreSQL database schema. Produces `Select`, `Insert`, and `Update` structs for each table, with JSON struct tags for serialization. + +## Usage + +Save the generated output to a file (e.g., `database/types.go`) in your project, then import the package and use the structs in your code. + +### Reading rows + +```go +package main + +import ( + "database/sql" + "encoding/json" + + db "myproject/database" +) + +func GetUser(row *sql.Row) (db.PublicUsersSelect, error) { + var user db.PublicUsersSelect + err := row.Scan(&user.Id, &user.Name, &user.Email, &user.CreatedAt, &user.Metadata) + return user, err +} +``` + +### Inserting rows + +```go +newUser := db.PublicUsersInsert{ + Name: "Alice", + Email: "alice@example.com", +} + +// Marshal to JSON for an API request +body, _ := json.Marshal(newUser) +``` + +### Partial updates + +```go +// Update structs use pointers so you can distinguish +// between "not set" (nil) and "set to zero value" +name := "Bob" +update := db.PublicUsersUpdate{ + Name: &name, + // other fields are nil — won't be included +} +``` + +## Endpoint + +``` +GET /generators/go +``` + +## Query parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `included_schemas` | string | — | Comma-separated list of schemas to include | +| `excluded_schemas` | string | — | Comma-separated list of schemas to exclude | + +## CLI usage + +```bash +# Using the dev server (npm run dev must be running) +npm run gen:types:go + +# With a custom database +PG_META_DB_URL=postgresql://user:pass@host:5432/db npm run gen:types:go +``` + +## Output structure + +All structs are generated in a single `database` package. Struct names are derived from the schema name and table name in PascalCase, suffixed with the operation type. + +```go +package database + +type PublicUsersSelect struct { + Id int32 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt string `json:"created_at"` + Metadata interface{} `json:"metadata"` +} + +type PublicUsersInsert struct { + Id int32 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt string `json:"created_at"` + Metadata interface{} `json:"metadata"` +} + +type PublicUsersUpdate struct { + Id *int32 `json:"id"` + Name *string `json:"name"` + Email *string `json:"email"` + CreatedAt *string `json:"created_at"` + Metadata interface{} `json:"metadata"` +} +``` + +Each table produces three structs: + +- **Select** — Fields returned from a query. +- **Insert** — Fields for inserting a row. +- **Update** — All fields are pointers (nullable) to allow partial updates. + +Views and materialized views produce a `Select` struct only. + +## Type mapping + +| PostgreSQL type | Go type | Nullable Go type | +|---|---|---| +| `bool` | `bool` | `*bool` | +| `int2` | `int16` | `*int16` | +| `int4` | `int32` | `*int32` | +| `int8` | `int64` | `*int64` | +| `float4` | `float32` | `*float32` | +| `float8`, `numeric` | `float64` | `*float64` | +| `text`, `varchar`, `bpchar`, `citext` | `string` | `*string` | +| `uuid` | `string` | `*string` | +| `date`, `time`, `timetz`, `timestamp`, `timestamptz` | `string` | `*string` | +| `bytea` | `[]byte` | `[]byte` | +| `json`, `jsonb` | `interface{}` | `interface{}` | +| `vector` | `string` | `*string` | +| Range types | `string` | `*string` | +| Array types | `[]T` (where T is the element type) | `[]T` | +| Enum types | `string` | `*string` | +| Composite types | `map[string]interface{}` | `map[string]interface{}` | + +## Features + +- Struct fields are aligned for readability +- JSON struct tags match the database column names +- Composite types are supported (mapped to `map[string]interface{}`) diff --git a/docs/jsonschema.md b/docs/jsonschema.md new file mode 100644 index 00000000..483964e5 --- /dev/null +++ b/docs/jsonschema.md @@ -0,0 +1,217 @@ +# JSON Schema Generator + +Generates a [JSON Schema](https://json-schema.org/) (Draft 2020-12) document from your PostgreSQL database schema. The output is a language-agnostic JSON document that can be used for validation, documentation, or code generation in any language. + +**Status:** Planned (not yet implemented) + +## Usage + +Save the generated output to a file (e.g., `database/schema.json`) in your project. JSON Schema is language-agnostic, so it can be used with any JSON Schema validator in any language. + +### Validating data in JavaScript/TypeScript + +```ts +import Ajv from "ajv"; +import schema from "./database/schema.json"; + +const ajv = new Ajv(); +const validate = ajv.compile( + schema.properties.public.properties.Tables.properties.users.properties.Row +); + +const valid = validate(row); +if (!valid) { + console.error(validate.errors); +} +``` + +### Validating data in Python + +```python +import json +from jsonschema import validate + +with open("database/schema.json") as f: + schema = json.load(f) + +row_schema = schema["properties"]["public"]["properties"]["Tables"] \ + ["properties"]["users"]["properties"]["Row"] + +validate(instance=row_data, schema=row_schema) +``` + +### Generating types from JSON Schema + +JSON Schema can be used as an input to code generators for languages that don't have a dedicated generator: + +```bash +# Generate TypeScript types from JSON Schema +npx json-schema-to-typescript database/schema.json > types.ts + +# Generate Rust types +typify database/schema.json > types.rs +``` + +### API documentation + +The schema can be embedded in OpenAPI specs or used to document your database structure: + +```yaml +# openapi.yaml +components: + schemas: + User: + $ref: "database/schema.json#/properties/public/properties/Tables/properties/users/properties/Row" +``` + +## Endpoint + +``` +GET /generators/jsonschema +``` + +## Query parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `included_schemas` | string | — | Comma-separated list of schemas to include | +| `excluded_schemas` | string | — | Comma-separated list of schemas to exclude | +| `db_driver_type` | string | `direct` | `direct` or `postgrest` — controls type mappings for driver-sensitive types (see [Driver modes](#driver-modes)) | + +## CLI usage + +```bash +# Using the dev server (npm run dev must be running) +npm run gen:types:jsonschema + +# With a custom database +PG_META_DB_URL=postgresql://user:pass@host:5432/db npm run gen:types:jsonschema +``` + +## Output structure + +The output is a JSON object with a top-level property per database schema. Each schema contains `Tables`, `Views`, `Enums`, `CompositeTypes`, and `Functions`. Enums and composite types are defined in `$defs` and referenced via `$ref`. + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "public": { + "type": "object", + "properties": { + "Tables": { + "type": "object", + "properties": { + "users": { + "type": "object", + "properties": { + "Row": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "status": { "$ref": "#/$defs/public.user_status" } + }, + "required": ["id", "name", "status"] + }, + "Insert": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "status": { "$ref": "#/$defs/public.user_status" } + }, + "required": ["name"] + }, + "Update": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "status": { "$ref": "#/$defs/public.user_status" } + } + } + } + } + } + }, + "Views": { ... }, + "Enums": { + "type": "object", + "properties": { + "user_status": { "$ref": "#/$defs/public.user_status" } + } + }, + "CompositeTypes": { ... }, + "Functions": { ... } + } + } + }, + "$defs": { + "public.user_status": { + "type": "string", + "enum": ["active", "inactive"] + } + } +} +``` + +Each table produces three sub-schemas: + +- **Row** — All columns in `required`. +- **Insert** — Only columns without defaults in `required`. +- **Update** — No `required` array (all columns optional). + +Views and materialized views produce a Row schema only. + +## Driver modes + +The `db_driver_type` parameter controls how certain types are represented. JSON Schema is language-agnostic, so the differences are minimal. + +### `direct` (default) + +| PostgreSQL type | JSON Schema | +|---|---| +| `int8` | `{ "type": "integer" }` | + +### `postgrest` + +| PostgreSQL type | JSON Schema | +|---|---| +| `int8` | `{ "type": "string" }` (PostgREST serializes bigints as strings) | + +Timestamps and dates use `"format": "date-time"` / `"format": "date"` in both modes. JSON Schema `format` is a hint — the consumer decides how to interpret the value. + +## Type mapping + +| PostgreSQL type | JSON Schema | +|---|---| +| `bool` | `{ "type": "boolean" }` | +| `int2`, `int4` | `{ "type": "integer" }` | +| `float4`, `float8`, `numeric` | `{ "type": "number" }` | +| `text`, `varchar`, `char`, `name`, `bpchar` | `{ "type": "string" }` | +| `uuid` | `{ "type": "string", "format": "uuid" }` | +| `json`, `jsonb` | `{}` (any value) | +| `timestamp`, `timestamptz` | `{ "type": "string", "format": "date-time" }` | +| `date` | `{ "type": "string", "format": "date" }` | +| `time`, `timetz` | `{ "type": "string", "format": "time" }` | +| `bytea` | `{ "type": "string" }` | +| `inet`, `cidr` | `{ "type": "string" }` | +| Range types | `{ "type": "string" }` | +| `void` | `{ "type": "null" }` | +| `record` | `{ "type": "object" }` | +| Array types | `{ "type": "array", "items": innerSchema }` | +| Enum types | `{ "$ref": "#/$defs/schema.enum_name" }` | +| Composite types | `{ "$ref": "#/$defs/schema.composite_name" }` | +| Unknown/unmapped | `{}` | + +Nullable columns are wrapped with `{ "oneOf": [typeSchema, { "type": "null" }] }`. + +## Features + +- Targets JSON Schema Draft 2020-12 (the current stable specification) +- `$defs` and `$ref` for reusable enum and composite type definitions +- `required` arrays reflect column defaults and identity columns +- Driver-aware type mappings (`direct` vs `postgrest`) +- No external dependencies — output is plain JSON diff --git a/docs/python.md b/docs/python.md new file mode 100644 index 00000000..0b3f366f --- /dev/null +++ b/docs/python.md @@ -0,0 +1,186 @@ +# Python Type Generator + +Generates Python type definitions from your PostgreSQL database schema using [Pydantic](https://docs.pydantic.dev/) `BaseModel` classes for row types and `TypedDict` classes for insert and update types. + +## Usage + +Save the generated output to a file (e.g., `database/types.py`) in your project, then import the types. + +### Validating query results + +```python +from database.types import Users + +# Pydantic BaseModel validates and parses data +user = Users.model_validate(row_dict) +print(user.name) # str +print(user.created_at) # datetime.datetime +``` + +### Typing inserts + +```python +from database.types import UsersInsert + +# TypedDict gives you type checking without runtime validation +new_user: UsersInsert = { + "name": "Alice", + "email": "alice@example.com", + # id, status, created_at are NotRequired — they have defaults +} +``` + +### Typing updates + +```python +from database.types import UsersUpdate + +# All fields are NotRequired for partial updates +update: UsersUpdate = { + "name": "Bob", +} +``` + +### Using with FastAPI + +```python +from fastapi import FastAPI +from database.types import Users, UsersInsert + +app = FastAPI() + +@app.get("/users/{user_id}", response_model=Users) +async def get_user(user_id: int): + row = await db.fetch_one("SELECT * FROM users WHERE id = $1", user_id) + return Users.model_validate(dict(row)) + +@app.post("/users", response_model=Users) +async def create_user(user: UsersInsert): + # Pydantic validates the request body automatically + ... +``` + +### Using enums + +```python +from database.types import UserStatus + +# UserStatus is a Literal type alias +def check_status(status: UserStatus): + if status == "active": + ... +``` + +## Endpoint + +``` +GET /generators/python +``` + +## Query parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `included_schemas` | string | — | Comma-separated list of schemas to include | +| `excluded_schemas` | string | — | Comma-separated list of schemas to exclude | + +## CLI usage + +```bash +# Using the dev server (npm run dev must be running) +npm run gen:types:python + +# With a custom database +PG_META_DB_URL=postgresql://user:pass@host:5432/db npm run gen:types:python +``` + +## Output structure + +The generator produces Pydantic `BaseModel` classes for row types and `TypedDict` classes for insert/update operations. + +```python +from __future__ import annotations + +import datetime +import uuid +from typing import ( + Annotated, + Any, + List, + Literal, + NotRequired, + Optional, + TypeAlias, + TypedDict, +) + +from pydantic import BaseModel, Field, Json + + +# Enums +UserStatus: TypeAlias = Literal["active", "inactive"] + + +# Tables +class Users(BaseModel): + id: int + name: str + email: str + status: UserStatus + created_at: datetime.datetime + metadata: Any | None + + +class UsersInsert(TypedDict): + id: NotRequired[int] + name: str + email: str + status: NotRequired[UserStatus] + created_at: NotRequired[datetime.datetime] + metadata: NotRequired[Any | None] + + +class UsersUpdate(TypedDict): + id: NotRequired[int] + name: NotRequired[str] + email: NotRequired[str] + status: NotRequired[UserStatus] + created_at: NotRequired[datetime.datetime] + metadata: NotRequired[Any | None] +``` + +Each table produces three types: + +- **BaseModel class** (Row) — A Pydantic model representing a row returned from a query. +- **Insert TypedDict** — Fields with defaults use `NotRequired`. +- **Update TypedDict** — All fields use `NotRequired`. + +Views and materialized views produce a BaseModel class only. + +## Type mapping + +| PostgreSQL type | Python type | +|---|---| +| `bool` | `bool` | +| `int2`, `int4`, `int8` | `int` | +| `float4`, `float8`, `numeric` | `float` | +| `text`, `varchar`, `char`, `bpchar`, `citext`, `name` | `str` | +| `uuid` | `uuid.UUID` | +| `date` | `datetime.date` | +| `time`, `timetz` | `datetime.time` | +| `timestamp`, `timestamptz` | `datetime.datetime` | +| `json`, `jsonb` | `Any` | +| `bytea` | `str` | +| Array types | `List[T]` (where T is the element type) | +| Enum types | `TypeAlias = Literal["val1", "val2"]` | +| Composite types | Generated Pydantic BaseModel class | + +Nullable columns use `Optional[T]` (or `T \| None`). + +## Features + +- Pydantic `BaseModel` for row types with full validation support +- `TypedDict` for insert/update types for compatibility with dict-based APIs +- Field aliases via `Field(alias=...)` when column names conflict with Python reserved words +- Enum types as `Literal` type aliases +- Composite type support as nested BaseModel classes diff --git a/docs/swift.md b/docs/swift.md new file mode 100644 index 00000000..645958d4 --- /dev/null +++ b/docs/swift.md @@ -0,0 +1,203 @@ +# Swift Type Generator + +Generates Swift struct and enum definitions from your PostgreSQL database schema. Structs conform to `Codable`, `Hashable`, and `Sendable` protocols, and include `CodingKeys` enums for JSON serialization. + +## Usage + +Save the generated output to a file (e.g., `Database/Types.swift`) in your project, then use the structs in your code. + +### Decoding query results + +```swift +import Foundation + +let data = // ... JSON data from your API +let decoder = JSONDecoder() +let users = try decoder.decode([PublicSchema.UsersSelect].self, from: data) + +for user in users { + print(user.name) // String + print(user.createdAt) // String (mapped from created_at via CodingKeys) +} +``` + +### Encoding data for inserts + +```swift +let newUser = PublicSchema.UsersInsert( + id: nil, // optional — has default + name: "Alice", + email: "alice@example.com", + status: nil, // optional — has default + createdAt: nil // optional — has default +) + +let encoder = JSONEncoder() +let body = try encoder.encode(newUser) +``` + +### Partial updates + +```swift +let update = PublicSchema.UsersUpdate( + id: nil, + name: "Bob", // only update the name + email: nil, + status: nil, + createdAt: nil +) +``` + +### Using enums + +```swift +let status: PublicSchema.UserStatus = .active + +switch status { +case .active: + print("User is active") +case .inactive: + print("User is inactive") +} +``` + +## Endpoint + +``` +GET /generators/swift +``` + +## Query parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `included_schemas` | string | — | Comma-separated list of schemas to include | +| `excluded_schemas` | string | — | Comma-separated list of schemas to exclude | +| `access_control` | string | `internal` | Swift access control level: `internal`, `public`, `private`, or `package` | + +## CLI usage + +```bash +# Using the dev server (npm run dev must be running) +npm run gen:types:swift + +# With a custom database +PG_META_DB_URL=postgresql://user:pass@host:5432/db npm run gen:types:swift + +# With options +PG_META_GENERATE_TYPES=swift \ +PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL=public \ +node --loader ts-node/esm src/server/server.ts +``` + +## Environment variables + +| Variable | Description | +|---|---| +| `PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL` | Access control level: `internal` (default), `public`, `private`, or `package` | + +## Output structure + +The generator produces structs organized by schema, with each table generating `Select`, `Insert`, and `Update` variants. + +```swift +enum PublicSchema { + // Enums + enum UserStatus: String, Codable, Hashable, Sendable { + case active = "active" + case inactive = "inactive" + } + + // Tables + struct UsersSelect: Codable, Hashable, Sendable { + let id: Int32 + let name: String + let email: String + let status: UserStatus + let createdAt: String + + enum CodingKeys: String, CodingKey { + case id + case name + case email + case status + case createdAt = "created_at" + } + } + + struct UsersInsert: Codable, Hashable, Sendable { + let id: Int32? + let name: String + let email: String + let status: UserStatus? + let createdAt: String? + + enum CodingKeys: String, CodingKey { + case id + case name + case email + case status + case createdAt = "created_at" + } + } + + struct UsersUpdate: Codable, Hashable, Sendable { + let id: Int32? + let name: String? + let email: String? + let status: UserStatus? + let createdAt: String? + + enum CodingKeys: String, CodingKey { + case id + case name + case email + case status + case createdAt = "created_at" + } + } +} +``` + +Each table produces three structs: + +- **Select** — Fields returned from a query. +- **Insert** — Columns with defaults are optional (`?`). +- **Update** — All columns are optional. + +Views and materialized views produce a `Select` struct only. + +Types with identity columns also conform to `Identifiable`. + +## Type mapping + +| PostgreSQL type | Swift type | +|---|---| +| `bool` | `Bool` | +| `int2` | `Int16` | +| `int4` | `Int32` | +| `int8` | `Int64` | +| `float4` | `Float` | +| `float8` | `Double` | +| `numeric`, `decimal` | `Decimal` | +| `uuid` | `UUID` | +| `text`, `varchar`, `bpchar`, `citext` | `String` | +| `date`, `time`, `timetz`, `timestamp`, `timestamptz` | `String` | +| `bytea` | `String` | +| `vector` | `String` | +| `json`, `jsonb` | `AnyJSON` | +| `void` | `Void` | +| `record` | `JSONObject` | +| Array types | `[T]` (where T is the element type) | +| Enum types | Generated Swift enum with `String` raw value | +| Composite types | Reference to generated struct (with `Select` suffix) | + +Nullable columns use Swift optionals (`T?`). + +## Features + +- Protocol conformance: `Codable`, `Hashable`, `Sendable` +- `Identifiable` conformance for tables with identity columns +- `CodingKeys` enum for mapping between Swift property names (camelCase) and database column names (snake_case) +- Configurable access control level +- Formatted with Prettier diff --git a/docs/typescript-zod.md b/docs/typescript-zod.md new file mode 100644 index 00000000..b51430fe --- /dev/null +++ b/docs/typescript-zod.md @@ -0,0 +1,212 @@ +# TypeScript Zod Type Generator + +Generates [Zod v4](https://zod.dev/v4) schema definitions from your PostgreSQL database schema. Unlike the [TypeScript generator](typescript.md) which produces static types only, this generator produces runtime validation schemas that can parse and validate data at runtime. + +**Status:** Planned (not yet implemented) + +## Usage + +Save the generated output to a file (e.g., `database/zod.ts`) in your project, then import schemas to validate data at runtime. + +### Validating query results + +```ts +import { schemas } from "@/database/zod"; + +const { Row: UserRow } = schemas.public.Tables.users; + +// Parse validates and returns typed data — throws on invalid input +const user = UserRow.parse(row); +// ^ typed as { id: number; name: string; email: string; ... } + +// safeParse returns a result object instead of throwing +const result = UserRow.safeParse(row); +if (result.success) { + console.log(result.data.name); +} else { + console.error(result.error.issues); +} +``` + +### Validating data before inserting + +```ts +import { schemas } from "@/database/zod"; + +const { Insert: UserInsert } = schemas.public.Tables.users; + +// Validate user input before sending to the database +const newUser = UserInsert.parse({ + name: "Alice", + email: "alice@example.com", + // id, status, created_at are optional (have defaults) +}); +``` + +### Validating API request bodies + +```ts +import { schemas } from "@/database/zod"; + +const { Update: UserUpdate } = schemas.public.Tables.users; + +app.patch("/users/:id", (req, res) => { + // All fields are optional for updates — rejects unknown fields + const body = UserUpdate.parse(req.body); + // ...update the database with validated data +}); +``` + +### Deriving static types from schemas + +You don't need both the TypeScript generator and the Zod generator. Zod schemas can produce static types directly: + +```ts +import { z } from "zod"; +import { schemas } from "@/database/zod"; + +type UserRow = z.infer; +// ^ { id: number; name: string; email: string; status: ...; ... } + +type UserInsert = z.infer; +// ^ { name: string; email: string; id?: number; status?: ...; ... } +``` + +## Endpoint + +``` +GET /generators/typescript-zod +``` + +## Query parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `included_schemas` | string | — | Comma-separated list of schemas to include | +| `excluded_schemas` | string | — | Comma-separated list of schemas to exclude | +| `db_driver_type` | string | `direct` | `direct` or `postgrest` — controls type mappings for driver-sensitive types (see [Driver modes](#driver-modes)) | + +## CLI usage + +```bash +# Using the dev server (npm run dev must be running) +npm run gen:types:typescript-zod + +# With a custom database +PG_META_DB_URL=postgresql://user:pass@host:5432/db npm run gen:types:typescript-zod +``` + +## Output structure + +Schemas are nested under per-schema namespace objects to avoid name collisions when the same table name exists in multiple schemas (e.g., `public.users` and `auth.users`). This mirrors the structure of the TypeScript generator's `Database` type. + +```ts +import { z } from "zod"; + +const userStatusSchema = z.enum(["active", "inactive"]); + +const usersRowSchema = z.object({ + id: z.number().int(), + name: z.string(), + email: z.string(), + status: userStatusSchema, + created_at: z.coerce.date(), + metadata: z.unknown().nullable(), +}); + +const usersInsertSchema = z.object({ + id: z.number().int().optional(), + name: z.string(), + email: z.string(), + status: userStatusSchema.optional(), + created_at: z.coerce.date().optional(), + metadata: z.unknown().nullable().optional(), +}); + +const usersUpdateSchema = usersRowSchema.partial(); + +export const schemas = { + public: { + Tables: { + users: { + Row: usersRowSchema, + Insert: usersInsertSchema, + Update: usersUpdateSchema, + }, + }, + Views: {}, + Enums: { + user_status: userStatusSchema, + }, + CompositeTypes: {}, + Functions: {}, + }, +}; +``` + +Access schemas via `schemas.public.Tables.users.Row`. + +Each table produces three schemas: + +- **Row** — Validates data returned from a `SELECT`. +- **Insert** — Columns with defaults or identity columns are `.optional()`. +- **Update** — All fields are `.partial()` (all optional). + +Views and materialized views produce a Row schema only (unless the view is updatable). + +## Driver modes + +The `db_driver_type` parameter controls how driver-sensitive types are mapped. This matters because PostgREST and direct PostgreSQL drivers return different runtime types for certain columns. + +### `direct` (default) + +Types reflect what a direct PostgreSQL driver like `node-postgres` returns: + +| PostgreSQL type | Zod validator | +|---|---| +| `timestamp`, `timestamptz` | `z.coerce.date()` | +| `date` | `z.coerce.date()` | +| `int8` | `z.string()` (node-postgres returns bigint as string by default) | + +### `postgrest` + +Types reflect PostgREST JSON serialization: + +| PostgreSQL type | Zod validator | +|---|---| +| `timestamp`, `timestamptz` | `z.string().datetime()` | +| `date` | `z.string().date()` | +| `int8` | `z.string()` | + +## Type mapping + +| PostgreSQL type | Zod validator | +|---|---| +| `bool` | `z.boolean()` | +| `int2`, `int4` | `z.number().int()` | +| `float4`, `float8`, `numeric` | `z.number()` | +| `text`, `varchar`, `char`, `name`, `bpchar` | `z.string()` | +| `uuid` | `z.string().uuid()` | +| `json`, `jsonb` | `z.unknown()` | +| `time`, `timetz` | `z.string()` | +| `bytea` | `z.string()` | +| `inet`, `cidr` | `z.string()` | +| `macaddr`, `macaddr8` | `z.string()` | +| Range types | `z.string()` | +| `void` | `z.void()` | +| `record` | `z.record(z.unknown())` | +| Array types | `z.array(innerSchema)` | +| Enum types | `z.enum(["val1", "val2"])` | +| Composite types | `z.object({ ... })` | +| Unknown/unmapped | `z.unknown()` | + +Nullable columns append `.nullable()`. Insert schemas append `.optional()` for columns with defaults. + +## Features + +- Runtime validation schemas (not just static types) +- Schema namespacing to handle cross-schema table name collisions +- Driver-aware type mappings (`direct` vs `postgrest`) +- Enum, composite type, and array support +- Formatted with Prettier +- No runtime dependencies in this package — consumers install `zod` themselves diff --git a/docs/typescript.md b/docs/typescript.md new file mode 100644 index 00000000..695a2405 --- /dev/null +++ b/docs/typescript.md @@ -0,0 +1,178 @@ +# TypeScript Type Generator + +Generates TypeScript type definitions from your PostgreSQL database schema. The output produces a single `Database` type containing all schemas, tables, views, functions, enums, and composite types. While designed to work with the [Supabase client libraries](https://github.com/supabase/supabase-js), the generated types are framework-agnostic and can be used with any TypeScript project that interacts with PostgreSQL. + +## Usage + +Save the generated output to a file (e.g., `database/types.ts`) in your project, then import the types. + +### Typing database queries + +```ts +import { Database } from "@/database/types"; + +type User = Database["public"]["Tables"]["users"]["Row"]; + +// Use with any PostgreSQL client +function getUser(row: unknown): User { + return row as User; +} +``` + +### Typing inserts and updates + +```ts +import { Database } from "@/database/types"; + +type NewUser = Database["public"]["Tables"]["users"]["Insert"]; +type UserUpdate = Database["public"]["Tables"]["users"]["Update"]; + +function createUser(user: NewUser) { + // id, created_at are optional — they have defaults + // name, email are required +} + +function updateUser(id: number, changes: UserUpdate) { + // All fields are optional +} +``` + +### Using helper utility types + +The generated output includes shorthand helper types: + +```ts +import { Tables, TablesInsert, TablesUpdate, Enums } from "@/database/types"; + +type User = Tables<"users">; +type NewUser = TablesInsert<"users">; +type UserUpdate = TablesUpdate<"users">; +type UserStatus = Enums<"user_status">; +``` + +### With Supabase client + +```ts +import { createClient } from "@supabase/supabase-js"; +import { Database } from "@/database/types"; + +const supabase = createClient(url, key); + +// Queries are now fully typed +const { data } = await supabase.from("users").select("*"); +// ^ typed as Database["public"]["Tables"]["users"]["Row"][] +``` + +## Endpoint + +``` +GET /generators/typescript +``` + +## Query parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `included_schemas` | string | — | Comma-separated list of schemas to include | +| `excluded_schemas` | string | — | Comma-separated list of schemas to exclude | +| `detect_one_to_one_relationships` | string | `false` | Set to `true` to detect and annotate one-to-one relationships | +| `postgrest_version` | string | — | PostgREST version string for version-specific type output | + +## CLI usage + +```bash +# Using the dev server (npm run dev must be running) +npm run gen:types:typescript + +# With a custom database +PG_META_DB_URL=postgresql://user:pass@host:5432/db npm run gen:types:typescript + +# With options +PG_META_GENERATE_TYPES=typescript \ +PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,auth \ +PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS=true \ +node --loader ts-node/esm src/server/server.ts +``` + +## Environment variables + +| Variable | Description | +|---|---| +| `PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS` | Comma-separated list of schemas to include | +| `PG_META_GENERATE_TYPES_DEFAULT_SCHEMA` | Default schema (defaults to `public`) | +| `PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS` | Set to `true` to detect one-to-one relationships | +| `PG_META_POSTGREST_VERSION` | PostgREST version string | + +## Output structure + +The generator produces a single `Database` type with nested schemas: + +```ts +export type Database = { + public: { + Tables: { + users: { + Row: { + id: number + name: string + email: string + created_at: string + } + Insert: { + id?: number // optional (has default) + name: string + email: string + created_at?: string + } + Update: { + id?: number + name?: string + email?: string + created_at?: string + } + Relationships: [...] + } + } + Views: { ... } + Functions: { ... } + Enums: { ... } + CompositeTypes: { ... } + } +} +``` + +Each table produces three sub-types: + +- **Row** — The shape of a row returned from a `SELECT`. +- **Insert** — The shape accepted by `INSERT`. Columns with defaults or identity columns are optional. +- **Update** — The shape accepted by `UPDATE`. All columns are optional. + +Views and materialized views produce a `Row` type only (unless the view is updatable). + +## Type mapping + +| PostgreSQL type | TypeScript type | +|---|---| +| `bool` | `boolean` | +| `int2`, `int4`, `float4`, `float8`, `numeric` | `number` | +| `int8` | `number` | +| `text`, `varchar`, `char`, `bpchar`, `citext`, `name` | `string` | +| `uuid` | `string` | +| `date`, `time`, `timetz`, `timestamp`, `timestamptz` | `string` | +| `json`, `jsonb` | `Json` | +| `bytea` | `string` | +| `void` | `undefined` | +| `record` | `Record` | +| Array types | `T[]` (where T is the element type) | +| Enum types | Union of string literals | +| Composite types | Object type with typed fields | + +Nullable columns produce `T \| null`. + +## Features + +- Helper utility types: `Tables`, `TablesInsert`, `TablesUpdate`, `Enums`, `CompositeTypes` +- Constants object with enum values for runtime use +- One-to-one relationship detection (opt-in) +- PostgREST version-aware output +- Formatted with Prettier diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..b13452fa --- /dev/null +++ b/plan.md @@ -0,0 +1,390 @@ +# Plan: Add Zod Type Generator & JSON Schema Generator + +## Overview + +The `postgres-meta` project has an established pattern for type generators. Each language has a route handler, a template, and tests, plus registration in shared files. We'll follow this exact pattern to add two new generators: + +1. **Zod** — Generates TypeScript code targeting [Zod v4](https://zod.dev/v4) for runtime validation (unlike the existing TypeScript generator which only produces static types). +2. **JSON Schema** — Generates a language-agnostic [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12) document describing the database schema (the current stable specification). + +Both generators consume the existing `GeneratorMetadata` type from `src/lib/generators.ts` — no changes to the core introspection layer are needed. + +--- + +## 1. Zod Type Generator + +### What it produces + +TypeScript source code that imports from `zod` and exports schema objects for every table, view, enum, composite type, and function in the database. + +#### Schema namespacing + +PostgreSQL allows the same table name in different schemas (e.g., `public.users` and `auth.users`). To avoid export name collisions, the Zod generator nests all schemas under a per-schema namespace object — mirroring the structure of the existing TypeScript generator: + +```ts +import { z } from "zod"; + +const publicSchema = { + Tables: { + users: { Row: z.object({ ... }), Insert: z.object({ ... }), Update: z.object({ ... }) }, + }, + Views: { ... }, + Enums: { + user_status: z.enum(["active", "inactive"]), + }, + CompositeTypes: { ... }, + Functions: { ... }, +}; + +const authSchema = { + Tables: { + users: { Row: z.object({ ... }), Insert: z.object({ ... }), Update: z.object({ ... }) }, + }, + // ... +}; + +export const schemas = { public: publicSchema, auth: authSchema }; +``` + +This way, consumers access schemas via `schemas.public.Tables.users.Row` — no ambiguity, and the structure is consistent with how the TypeScript generator organizes its `Database` type. + +#### Full example output (`db_driver_type=direct`, the default): + +```ts +import { z } from "zod"; + +const userStatusSchema = z.enum(["active", "inactive"]); + +const usersRowSchema = z.object({ + id: z.number().int(), + name: z.string(), + email: z.string(), + status: userStatusSchema, + created_at: z.coerce.date(), // Date object in direct mode + metadata: z.unknown().nullable(), +}); + +const usersInsertSchema = z.object({ + id: z.number().int().optional(), // has default (identity) + name: z.string(), + email: z.string(), + status: userStatusSchema.optional(), // has default + created_at: z.coerce.date().optional(), + metadata: z.unknown().nullable().optional(), +}); + +const usersUpdateSchema = usersRowSchema.partial(); + +export const schemas = { + public: { + Tables: { + users: { + Row: usersRowSchema, + Insert: usersInsertSchema, + Update: usersUpdateSchema, + }, + }, + Views: {}, + Enums: { + user_status: userStatusSchema, + }, + CompositeTypes: {}, + Functions: {}, + }, +}; +``` + +With `db_driver_type=postgrest`, `created_at` would instead be `z.string().datetime()` (since PostgREST serializes timestamps as ISO strings). + +### Type mapping (`PG_TYPE_TO_ZOD_MAP`) + +These generators support two output modes, controlled by a `db_driver_type` query parameter: + +- **`direct`** (default) — Types reflect what a direct PostgreSQL driver like `node-postgres` returns (e.g., timestamps as `Date`, `int8` as `string` by default in pg). +- **`postgrest`** — Types reflect PostgREST serialization behavior (e.g., timestamps and dates are returned as strings, `int8` as string). + +The base type map is shared; only the types that differ between modes are listed below. + +#### Base types (same in both modes) + +| PostgreSQL type | Zod validator | +|---|---| +| `bool` | `z.boolean()` | +| `int2`, `int4` | `z.number().int()` | +| `float4`, `float8`, `numeric` | `z.number()` | +| `text`, `varchar`, `char`, `name`, `bpchar` | `z.string()` | +| `uuid` | `z.string().uuid()` | +| `json`, `jsonb` | `z.unknown()` | +| `time`, `timetz` | `z.string()` | +| `bytea` | `z.string()` | +| `inet`, `cidr` | `z.string()` | +| `macaddr`, `macaddr8` | `z.string()` | +| `int4range`, `int8range`, `numrange`, `tsrange`, `tstzrange`, `daterange` | `z.string()` | +| `void` | `z.void()` | +| `record` | `z.record(z.unknown())` | +| Array types (prefix `_`) | `z.array(innerSchema)` | +| Enum types | Reference to generated enum schema | +| Composite types | Reference to generated composite schema | +| Unknown/unmapped | `z.unknown()` | + +#### Types that differ by mode + +| PostgreSQL type | `postgrest` mode | `direct` mode | +|---|---|---| +| `int8` | `z.string()` (PostgREST returns bigints as strings) | `z.string()` (node-postgres also returns bigint as string by default) | +| `timestamptz`, `timestamp` | `z.string().datetime()` | `z.coerce.date()` | +| `date` | `z.string().date()` | `z.coerce.date()` | + +**Nullable handling:** Append `.nullable()` when the column is nullable. + +**Insert schemas:** Columns with defaults or identity columns get `.optional()`. + +**Update schemas:** All fields are `.partial()`. + +### Generation structure + +Per schema, generate: + +1. Enum schemas (`z.enum([...])`) +2. Composite type schemas (`z.object({...})`) +3. Table Row / Insert / Update schemas +4. View Row schemas (Insert/Update only if the view is updatable) +5. Materialized view Row schemas +6. Function input/output schemas + +Use `prettier` for formatting (same as the existing TypeScript generator). + +### Query parameters + +| Parameter | Description | +|---|---| +| `included_schemas` | Comma-separated schema whitelist | +| `excluded_schemas` | Comma-separated schema blacklist | +| `db_driver_type` | `direct` (default) or `postgrest` — controls type mappings for driver-sensitive types | + +### Files + +| Action | File | +|---|---| +| **Create** | `src/server/templates/typescript-zod.ts` | +| **Create** | `src/server/routes/generators/typescript-zod.ts` | +| Modify | `src/server/routes/index.ts` — register route | +| Modify | `src/server/server.ts` — add CLI case | +| Modify | `package.json` — add `gen:types:zod` script | +| Modify | `test/server/typegen.ts` — add snapshot tests | + +--- + +## 2. JSON Schema Generator + +### What it produces + +A JSON document conforming to JSON Schema Draft 2020-12, describing every table, view, enum, composite type, and function in the database. + +Example output: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "public": { + "type": "object", + "properties": { + "Tables": { + "type": "object", + "properties": { + "users": { + "type": "object", + "properties": { + "Row": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "status": { "$ref": "#/$defs/public.user_status" } + }, + "required": ["id", "name", "status"] + }, + "Insert": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "status": { "$ref": "#/$defs/public.user_status" } + }, + "required": ["name"] + }, + "Update": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "status": { "$ref": "#/$defs/public.user_status" } + } + } + } + } + } + }, + "Views": { "..." : "..." }, + "Enums": { + "type": "object", + "properties": { + "user_status": { "$ref": "#/$defs/public.user_status" } + } + }, + "CompositeTypes": { "..." : "..." }, + "Functions": { "..." : "..." } + } + } + }, + "$defs": { + "public.user_status": { + "type": "string", + "enum": ["active", "inactive"] + } + } +} +``` + +### Type mapping (`PG_TYPE_TO_JSON_SCHEMA_MAP`) + +Like the Zod generator, the JSON Schema generator supports a `db_driver_type` query parameter (`direct` or `postgrest`). JSON Schema is language-agnostic, so the differences are smaller — mainly around whether `int8` is represented as a string or integer. + +#### Base types (same in both modes) + +| PostgreSQL type | JSON Schema | +|---|---| +| `bool` | `{ "type": "boolean" }` | +| `int2`, `int4` | `{ "type": "integer" }` | +| `float4`, `float8`, `numeric` | `{ "type": "number" }` | +| `text`, `varchar`, `char`, `name`, `bpchar` | `{ "type": "string" }` | +| `uuid` | `{ "type": "string", "format": "uuid" }` | +| `json`, `jsonb` | `{}` (any value) | +| `timestamptz`, `timestamp` | `{ "type": "string", "format": "date-time" }` | +| `date` | `{ "type": "string", "format": "date" }` | +| `time`, `timetz` | `{ "type": "string", "format": "time" }` | +| `bytea` | `{ "type": "string" }` | +| `inet`, `cidr` | `{ "type": "string" }` | +| Range types | `{ "type": "string" }` | +| `void` | `{ "type": "null" }` | +| `record` | `{ "type": "object" }` | +| Array types (prefix `_`) | `{ "type": "array", "items": innerSchema }` | +| Enum types | `{ "$ref": "#/$defs/schema.enum_name" }` | +| Composite types | `{ "$ref": "#/$defs/schema.composite_name" }` | +| Unknown/unmapped | `{}` | + +#### Types that differ by mode + +| PostgreSQL type | `postgrest` mode | `direct` mode | +|---|---|---| +| `int8` | `{ "type": "string" }` (PostgREST serializes bigints as strings) | `{ "type": "integer" }` | + +Note: `timestamp`/`date` use `"format": "date-time"` / `"format": "date"` in both modes. JSON Schema `format` is a hint, not a type constraint — the consumer decides whether to interpret the value as a string or a Date object. + +**Nullable handling:** Wrap with `{ "oneOf": [typeSchema, { "type": "null" }] }` + +**Insert schemas:** Only columns without defaults go in `required`. + +**Update schemas:** Empty `required` array (all fields optional). + +### Generation structure + +The top-level JSON object has: +- `$schema` — the JSON Schema draft URI +- `type: "object"` with a property per database schema +- Each schema property contains `Tables`, `Views`, `Enums`, `CompositeTypes`, `Functions` +- `$defs` — shared definitions for enums and composite types (referenced via `$ref`) + +Output is `JSON.stringify(schema, null, 2)` — no external formatter needed. + +### Query parameters + +| Parameter | Description | +|---|---| +| `included_schemas` | Comma-separated schema whitelist | +| `excluded_schemas` | Comma-separated schema blacklist | +| `db_driver_type` | `direct` (default) or `postgrest` — controls type mappings for driver-sensitive types | + +### Files + +| Action | File | +|---|---| +| **Create** | `src/server/templates/jsonschema.ts` | +| **Create** | `src/server/routes/generators/jsonschema.ts` | +| Modify | `src/server/routes/index.ts` — register route | +| Modify | `src/server/server.ts` — add CLI case | +| Modify | `package.json` — add `gen:types:jsonschema` script | +| Modify | `test/server/typegen.ts` — add snapshot tests | + +--- + +## 3. Shared modifications summary + +### `src/server/routes/index.ts` + +```ts +import TypeScriptZodTypeGenRoute from './generators/typescript-zod.js' +import JsonSchemaTypeGenRoute from './generators/jsonschema.js' + +// inside registration: +fastify.register(TypeScriptZodTypeGenRoute, { prefix: '/generators/typescript-zod' }) +fastify.register(JsonSchemaTypeGenRoute, { prefix: '/generators/jsonschema' }) +``` + +### `src/server/server.ts` + +Add two new template imports and two new cases in the `getTypeOutput()` switch: + +```ts +case 'typescript-zod': + return applyTypeScriptZodTemplate({ ...generatorMeta }) +case 'jsonschema': + return applyJsonSchemaTemplate({ ...generatorMeta }) +``` + +### `package.json` + +```json +"gen:types:typescript-zod": "PG_META_GENERATE_TYPES=typescript-zod ...", +"gen:types:jsonschema": "PG_META_GENERATE_TYPES=jsonschema ..." +``` + +--- + +## 4. Testing + +Add to `test/server/typegen.ts`: + +- **`test('typegen: typescript-zod')`** — Inline snapshot test validating Zod output for the test database (enums, tables, views, composite types, functions, nullable fields, arrays, identity columns, defaults). +- **`test('typegen: jsonschema')`** — Inline snapshot test validating JSON Schema output structure, `$defs`, `$ref` usage, `required` arrays, and nullable handling. + +Both tests use the same `app.inject()` pattern as existing tests. + +--- + +## 5. Implementation order + +| Step | Task | Depends on | +|---|---|---| +| 1 | Create `src/server/templates/typescript-zod.ts` | — | +| 2 | Create `src/server/routes/generators/typescript-zod.ts` | Step 1 | +| 3 | Create `src/server/templates/jsonschema.ts` | — | +| 4 | Create `src/server/routes/generators/jsonschema.ts` | Step 3 | +| 5 | Modify `src/server/routes/index.ts` (register both routes) | Steps 2, 4 | +| 6 | Modify `src/server/server.ts` (add CLI support) | Steps 1, 3 | +| 7 | Modify `package.json` (add npm scripts) | — | +| 8 | Add tests to `test/server/typegen.ts` | Steps 1–6 | +| 9 | Run `tsc` type check, fix any issues | Steps 1–7 | +| 10 | Run tests, update snapshots | Step 8 | + +--- + +## 6. Design decisions + +- **No new dependencies.** Zod output is plain TypeScript source code — users install `zod` themselves. JSON Schema is a plain JSON object built with standard library. +- **Reuse `GeneratorMetadata` as-is.** Both generators consume the same metadata interface, requiring zero changes to `src/lib/generators.ts`. +- **Follow existing patterns exactly.** Route handler structure, template `apply()` signature, error handling, and test approach all mirror existing generators. +- **Driver-aware type modes.** Both generators accept a `db_driver_type` query parameter (`direct` or `postgrest`). The default is `direct`, producing types that match what a standard PostgreSQL driver returns (e.g., `Date` for timestamps). Users going through PostgREST can pass `db_driver_type=postgrest` to get string-based types matching PostgREST's JSON serialization. This avoids forcing a single serialization assumption on all users. +- **JSON Schema uses `$ref` for reusability.** Enums and composite types are placed in `$defs` and referenced via `$ref`, keeping the output DRY and composable.