README.md

<img src="https://github.com/ash-project/ash_typescript/blob/main/logos/ash-typescript.png?raw=true" alt="Logo" width="300"/>

![Elixir CI](https://github.com/ash-project/ash_typescript/workflows/CI/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Hex version badge](https://img.shields.io/hexpm/v/ash_typescript.svg)](https://hex.pm/packages/ash_typescript)
[![Hexdocs badge](https://img.shields.io/badge/docs-hexdocs-purple)](https://hexdocs.pm/ash_typescript)
# AshTypescript

**๐Ÿ”ฅ Automatic TypeScript type generation for Ash resources and actions**

Generate type-safe TypeScript clients directly from your Elixir Ash resources, ensuring end-to-end type safety between your backend and frontend. Never write API types manually again.

[![Hex.pm](https://img.shields.io/hexpm/v/ash_typescript.svg)](https://hex.pm/packages/ash_typescript)
[![Documentation](https://img.shields.io/badge/docs-hexdocs-blue.svg)](https://hexdocs.pm/ash_typescript)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

## โšก Quick Start

**Get up and running in under 5 minutes:**

### 1. Installation & Setup

**Option A: Automatic Setup with Igniter (Recommended)**

Add AshTypescript to your project and run the automated installer:

```bash
# Add ash_typescript to your mix.exs and install
mix igniter.install ash_typescript

# For a full-stack Phoenix + React setup, use the --framework flag:
mix igniter.install ash_typescript --framework react
```

The installer automatically:
- โœ… Adds AshTypescript to your dependencies
- โœ… Configures AshTypescript settings in `config.exs`
- โœ… Creates RPC controller and routes
- โœ… With `--framework react`: Sets up React + TypeScript environment, and a getting started guide

**Option B: Manual Installation**

Add to your `mix.exs`:

```elixir
def deps do
  [
    {:ash_typescript, "~> 0.4.0"}
  ]
end
```

### 2. Configure your domain

**Note:** If you used the automatic installer (`mix igniter.install ash_typescript`), you can skip to step 5. The following steps are only needed for manual installation.

```elixir
defmodule MyApp.Domain do
  use Ash.Domain, extensions: [AshTypescript.Rpc]

  typescript_rpc do
    resource MyApp.Todo do
      rpc_action :list_todos, :read
      rpc_action :create_todo, :create
      rpc_action :get_todo, :get
    end
  end

  resources do
    resource MyApp.Todo
  end
end
```

### 3. Set up Phoenix RPC controller

```elixir
defmodule MyAppWeb.RpcController do
  use MyAppWeb, :controller

  def run(conn, params) do
    # Actor (and tenant if needed) must be set on the conn before calling run/2 or validate/2
    # If your pipeline does not set these, you must add something like the following code:
    # conn = Ash.PlugHelpers.set_actor(conn, conn.assigns[:current_user])
    # conn = Ash.PlugHelpers.set_tenant(conn, conn.assigns[:tenant])
    result = AshTypescript.Rpc.run_action(:my_app, conn, params)
    json(conn, result)
  end

  def validate(conn, params) do
    result = AshTypescript.Rpc.validate_action(:my_app, conn, params)
    json(conn, result)
  end
end
```

### 4. Add RPC routes

Add these routes to your `router.ex` to map the RPC endpoints:

```elixir
scope "/rpc", MyAppWeb do
  pipe_through :api  # or :browser if using session-based auth

  post "/run", RpcController, :run
  post "/validate", RpcController, :validate
end
```

### 5. Generate TypeScript types

**After using the installer or completing manual setup:**

**Recommended approach** (runs codegen for all Ash extensions in your project):
```bash
mix ash.codegen --dev"
```

**Alternative approach** (runs codegen only for AshTypescript):
```bash
mix ash_typescript.codegen --output "assets/js/ash_rpc.ts"
```

### 6. Use in your frontend

```typescript
import { listTodos, createTodo } from './ash_rpc';

// โœ… Fully type-safe API calls
const todos = await listTodos({
  fields: ["id", "title", "completed"],
  filter: { completed: false }
});

const newTodo = await createTodo({
  fields: ["id", "title", { user: ["name", "email"] }],
  input: { title: "Learn AshTypescript", priority: "high" }
});
```

**๐ŸŽ‰ That's it!** Your TypeScript frontend now has compile-time type safety for your Elixir backend.

### React Setup (with `--framework react`)

When you use `mix igniter.install ash_typescript --framework react`, the installer creates a full Phoenix + React + TypeScript setup:

- **๐Ÿ“ฆ Package.json** with React 19 & TypeScript
- **โš›๏ธ React components** with a beautiful welcome page and documentation
- **๐ŸŽจ Tailwind CSS** integration with modern styling
- **๐Ÿ”ง Build configuration** with esbuild and TypeScript compilation
- **๐Ÿ“„ Templates** with proper script loading and syntax highlighting
- **๐ŸŒ Getting started guide** accessible at `/ash-typescript` in your Phoenix app

The welcome page includes:
- Step-by-step setup instructions
- Code examples with syntax highlighting
- Links to documentation and demo projects
- Type-safe RPC function examples

Visit `http://localhost:4000/ash-typescript` after running your Phoenix server to see the interactive guide!

### ๐Ÿš€ Example Repo

Check out this **[example repo](https://github.com/ChristianAlexander/ash_typescript_demo)** by Christian Alexander, which showcases:

- Complete Phoenix + React + TypeScript integration
- TanStack Query for data fetching
- TanStack Table for data display

## ๐Ÿšจ Breaking Changes

### Resource Extension Requirement (Security Enhancement)

**Important**: All resources that should be accessible through the TypeScript RPC layer must now explicitly use the `AshTypescript.Resource` extension.

#### What Changed

The TypeScript RPC layer now requires the `AshTypescript.Resource` extension for:

1. **Resources with RPC actions** - Resources that have `rpc_action` definitions in your domain
2. **Resources accessed through relationships** - Resources accessed via relationship field selection in RPC calls

This prevents accidental exposure of internal resources through the TypeScript RPC interface.

#### Why This Change

This security enhancement ensures that only resources that should be accessible through the TypeScript RPC layer are available, requiring explicit opt-in for resource exposure and preventing unintended data access.

#### Migration Required

**Before**: Resources were accessible without explicit configuration
```elixir
# This resource was previously accessible via RPC
defmodule MyApp.Todo do
  use Ash.Resource, domain: MyApp.Domain
  # No AshTypescript.Resource extension required
end

defmodule MyApp.User do
  use Ash.Resource, domain: MyApp.Domain
  # No extension, but accessible via Todo's user relationship
end
```

**After**: Resources must explicitly opt-in to RPC access
```elixir
# Now required: Add extension for resources with RPC actions
defmodule MyApp.Todo do
  use Ash.Resource,
    domain: MyApp.Domain,
    extensions: [AshTypescript.Resource]  # โ† Required for RPC actions

  typescript do
    type_name "Todo"
  end
end

# Now required: Add extension for resources accessed via relationships
defmodule MyApp.User do
  use Ash.Resource,
    domain: MyApp.Domain,
    extensions: [AshTypescript.Resource]  # โ† Required for relationship access

  typescript do
    type_name "User"
  end
end
```

#### Error Symptoms

You may see errors like:
```
Unknown field 'relationshipName' for resource MyApp.SomeResource
```

#### Migration Steps

For each resource that should be accessible via RPC:

1. **Add the extension**:
   ```elixir
   use Ash.Resource,
     domain: MyApp.Domain,
     extensions: [AshTypescript.Resource]  # Add this line
   ```

2. **Configure the TypeScript type**:
   ```elixir
   typescript do
     type_name "YourResourceName"  # Choose appropriate name
   end
   ```

3. **Regenerate types**:
   ```bash
   mix ash.codegen --dev
   ```

#### Resources That Need the Extension

- โœ… **Resources with RPC actions** in your domain configuration
- โœ… **Resources accessed through relationships** in RPC field selection
- โŒ **Internal resources** not meant for frontend access
- โŒ **System resources** (audit logs, internal settings, etc.)

This change makes your API more secure by requiring explicit opt-in for all RPC resource access.

## โœจ Features

- **๐Ÿ”ฅ Zero-config TypeScript generation** - Automatically generates types from Ash resources
- **๐Ÿ›ก๏ธ End-to-end type safety** - Catch integration errors at compile time, not runtime
- **โšก Smart field selection** - Request only needed fields with full type inference
- **๐ŸŽฏ RPC client generation** - Type-safe function calls for all action types
- **๐Ÿ“ก Phoenix Channel support** - Generate channel-based RPC functions for real-time applications
- **๐Ÿข Multitenancy ready** - Automatic tenant parameter handling
- **๐Ÿ“ฆ Advanced type support** - Enums, unions, embedded resources, and calculations
- **๐Ÿ”ง Highly configurable** - Custom endpoints, formatting, and output options
- **๐Ÿงช Runtime validation** - Zod schemas for runtime type checking and form validation
- **๐Ÿ” Auto-generated filters** - Type-safe filtering with comprehensive operator support
- **๐Ÿ“‹ Form validation** - Client-side validation functions for all actions
- **๐ŸŽฏ Typed queries** - Pre-configured queries for SSR and optimized data fetching
- **๐ŸŽจ Flexible field formatting** - Separate input/output formatters (camelCase, snake_case, etc.)
- **๐Ÿ”Œ Custom HTTP clients** - Support for custom fetch functions and request options (axios, interceptors, etc.)

## ๐Ÿ“š Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Usage Examples](#usage-examples)
- [Advanced Features](#advanced-features)
- [Configuration](#configuration)
- [Mix Tasks](#mix-tasks)
- [API Reference](#api-reference)
- [Requirements](#requirements)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
- [License](#license)

## ๐Ÿ—๏ธ Core Concepts

### How it works

1. **Resource Definition**: Define your Ash resources with attributes, relationships, and actions
2. **RPC Configuration**: Expose specific actions through your domain's RPC configuration
3. **Type Generation**: Run `mix ash_typescript.codegen` to generate TypeScript types
4. **Frontend Integration**: Import and use fully type-safe client functions

### Type Safety Benefits

- **Compile-time validation** - TypeScript compiler catches API misuse
- **Autocomplete support** - Full IntelliSense for all resource fields and actions
- **Refactoring safety** - Rename fields in Elixir, get TypeScript errors immediately
- **Documentation** - Generated types serve as living API documentation

## ๐Ÿ’ก Usage Examples

### Basic CRUD Operations

```typescript
import { listTodos, getTodo, createTodo, updateTodo, destroyTodo } from './ash_rpc';

// List todos with field selection
const todos = await listTodos({
  fields: ["id", "title", "completed", "priority"],
  filter: { status: "active" },
  sort: "-priority,+createdAt"
});

// Get single todo with relationships
const todo = await getTodo({
  fields: ["id", "title", { user: ["name", "email"] }],
  id: "todo-123"
});

// Create new todo
const newTodo = await createTodo({
  fields: ["id", "title", "createdAt"],
  input: {
    title: "Learn AshTypescript",
    priority: "high",
    dueDate: "2024-01-01"
  }
});

// Update existing todo (primary key separate from input)
const updatedTodo = await updateTodo({
  fields: ["id", "title", "priority", "updatedAt"],
  primaryKey: "todo-123",  // Primary key as separate parameter
  input: {
    title: "Updated: Learn AshTypescript",
    priority: "urgent"
  }
});

// Delete todo (primary key separate from input)
const deletedTodo = await destroyTodo({
  fields: [],
  primaryKey: "todo-123"    // Primary key as separate parameter
});
```

### Advanced Field Selection

```typescript
// Complex nested field selection
const todoWithDetails = await getTodo({
  fields: [
    "id", "title", "description",
    {
      user: ["name", "email", "avatarUrl"],
      comments: ["id", "text", { author: ["name"] }],
      tags: ["name", "color"]
    }
  ],
  id: "todo-123"
});

// Calculations with arguments
const todoWithCalc = await getTodo({
  fields: [
    "id", "title",
    {
      "priorityScore": {
        "args": { "multiplier": 2 },
        "fields": ["score", "rank"]
      }
    }
  ],
  id: "todo-123"
});
```

### Error Handling

All generated RPC functions return a `{success: true/false}` structure instead of throwing exceptions:

```typescript
const result = await createTodo({
  fields: ["id", "title"],
  input: { title: "New Todo" }
});

if (result.success) {
  // Access the created todo
  console.log("Created todo:", result.data);
  const todoId: string = result.data.id;
  const todoTitle: string = result.data.title;
} else {
  // Handle validation errors, network errors, etc.
  result.errors.forEach(error => {
    console.error(`Error: ${error.message}`);
    if (error.fieldPath) {
      console.error(`Field: ${error.fieldPath}`);
    }
  });
}
```

### Custom Headers and Authentication

```typescript
import { listTodos, buildCSRFHeaders } from './ash_rpc';

// With CSRF protection
const todos = await listTodos({
  fields: ["id", "title"],
  headers: buildCSRFHeaders()
});

// With custom authentication
const todos = await listTodos({
  fields: ["id", "title"],
  headers: {
    "Authorization": "Bearer your-token-here",
    "X-Custom-Header": "value"
  }
});
```

### Custom Fetch Functions and Request Options

AshTypescript allows you to customize the HTTP client used for requests by providing custom fetch functions and additional fetch options.

#### Using fetchOptions for Request Customization

All generated RPC functions accept an optional `fetchOptions` parameter that allows you to customize the underlying fetch request:

```typescript
import { createTodo, listTodos } from './ash_rpc';

// Add request timeout and custom cache settings
const todo = await createTodo({
  fields: ["id", "title"],
  input: { title: "New Todo" },
  fetchOptions: {
    signal: AbortSignal.timeout(5000), // 5 second timeout
    cache: 'no-cache',
    credentials: 'include'
  }
});

// Use with abort controller for cancellable requests
const controller = new AbortController();

const todos = await listTodos({
  fields: ["id", "title"],
  fetchOptions: {
    signal: controller.signal
  }
});

// Cancel the request if needed
controller.abort();
```

#### Custom Fetch Functions

You can replace the native fetch function entirely by providing a `customFetch` parameter. This is useful for:
- Adding global authentication
- Using alternative HTTP clients like axios
- Adding request/response interceptors
- Custom error handling

```typescript
// Custom fetch with user preferences and tracking
const enhancedFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
  // Get user preferences from localStorage (safe, non-sensitive data)
  const userLanguage = localStorage.getItem('userLanguage') || 'en';
  const userTimezone = localStorage.getItem('userTimezone') || 'UTC';
  const apiVersion = localStorage.getItem('preferredApiVersion') || 'v1';

  // Generate correlation ID for request tracking
  const correlationId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;

  const customHeaders = {
    'Accept-Language': userLanguage,
    'X-User-Timezone': userTimezone,
    'X-API-Version': apiVersion,
    'X-Correlation-ID': correlationId,
  };

  return fetch(url, {
    ...init,
    headers: {
      ...init?.headers,
      ...customHeaders
    }
  });
};

// Use custom fetch function
const todos = await listTodos({
  fields: ["id", "title"],
  customFetch: enhancedFetch
});
```

#### Using Axios with AshTypescript

While AshTypescript uses the fetch API by default, you can create an adapter to use axios or other HTTP clients:

```typescript
import axios from 'axios';

// Create axios adapter that matches fetch API
const axiosAdapter = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
  try {
    const url = typeof input === 'string' ? input : input.toString();

    const axiosResponse = await axios({
      url,
      method: init?.method || 'GET',
      headers: init?.headers,
      data: init?.body,
      timeout: 10000,
      // Add other axios-specific options
      validateStatus: () => true // Don't throw on HTTP errors
    });

    // Convert axios response to fetch Response
    return new Response(JSON.stringify(axiosResponse.data), {
      status: axiosResponse.status,
      statusText: axiosResponse.statusText,
      headers: new Headers(axiosResponse.headers as any)
    });
  } catch (error) {
    if (error.response) {
      // HTTP error status
      return new Response(JSON.stringify(error.response.data), {
        status: error.response.status,
        statusText: error.response.statusText
      });
    }
    throw error; // Network error
  }
};

// Use axios for all requests
const todos = await listTodos({
  fields: ["id", "title"],
  customFetch: axiosAdapter
});
```

### Phoenix Channel-based RPC Actions

AshTypescript can generate Phoenix channel-based RPC functions alongside the standard HTTP-based functions. This is useful for real-time applications that need to communicate over WebSocket connections.

#### Configuration

Enable channel function generation in your configuration:

```elixir
# config/config.exs
config :ash_typescript,
  generate_phx_channel_rpc_actions: true,
  phoenix_import_path: "phoenix"  # customize if needed
```

#### Generated Channel Functions

When enabled, AshTypescript generates channel functions with the suffix `Channel` for each RPC action:

```typescript
import { Channel } from "phoenix";
import { createTodo, createTodoChannel } from './ash_rpc';

// Standard HTTP-based function (always available)
const httpResult = await createTodo({
  fields: ["id", "title"],
  input: { title: "New Todo" }
});

// Channel-based function (generated when enabled)
createTodoChannel({
  channel: myChannel,
  fields: ["id", "title"],
  input: { title: "New Todo" },
  resultHandler: (result) => {
    if (result.success) {
      console.log("Todo created:", result.data);
    } else {
      console.error("Creation failed:", result.errors);
    }
  },
  errorHandler: (error) => {
    console.error("Channel error:", error);
  },
  timeoutHandler: () => {
    console.error("Request timed out");
  }
});
```

#### Setting up Phoenix Channels

First, establish a Phoenix channel connection:

```typescript
import { Socket } from "phoenix";

const socket = new Socket("/socket", {
  params: { authToken: "your-auth-token" }
});

socket.connect();

const ashTypeScriptRpcChannel = socket.channel("ash_typescript_rpc:<user-id or something else unique>", {});
ashTypeScriptRpcChannel.join()
  .receive("ok", () => console.log("Connected to channel"))
  .receive("error", resp => console.error("Unable to join", resp));
```

#### Backend Channel Setup

To enable Phoenix Channel support for AshTypescript RPC actions, configure your Phoenix socket and channel handlers:

```elixir
# In your my_app_web/channels/user_socket.ex or equivalent
defmodule MyAppWeb.UserSocket do
  use Phoenix.Socket

  channel "ash_typescript_rpc:*", MyAppWeb.AshTypescriptRpcChannel

  @impl true
  def connect(params, socket, _connect_info) do
    # AshTypescript assumes that socket.assigns.ash_actor & socket.assigns.ash_tenant are correctly set if needed.
    # This should be done during the socket connection setup, usually by decrypting the auth token sent by the client, or any other necessary data.
    # See https://hexdocs.pm/phoenix/channels.html#using-token-authentication for more information.
    {:ok, socket}
  end

  def id(socket), do: socket.assigns.ash_actor.id
end

# In your my_app_web/channels/ash_typescript_rpc_channel.ex
defmodule MyAppWeb.AshTypescriptRpcChannel do
  use Phoenix.Channel

  @impl true
  def join("ash_typescript_rpc:" <> _user_id, _payload, socket) do
    {:ok, socket}
  end

  def handle_in("run", params, socket) do
    result =
      AshTypescript.Rpc.run_action(
        :my_app,
        socket,
        params
      )

    {:reply, {:ok, result}, socket}
  end

  def handle_in("validate", params, socket) do
    result =
      AshTypescript.Rpc.validate_action(
        :my_app,
        socket,
        params
      )

    {:reply, {:ok, result}, socket}
  end

  # Catch-all for unhandled messages
  @impl true
  def handle_in(event, payload, socket) do
    {:reply, {:error, %{reason: "Unknown event: #{event}", payload: payload}}, socket}
  end
end
```

**Important Notes:**
- Replace `:my_app` with your actual app's OTP application name (the atom used in `AshTypescript.Rpc.run_action/3`)
- The socket connection should set `socket.assigns.ash_actor` and `socket.assigns.ash_tenant` if your app uses authentication or multitenancy

#### Channel Function Features

Channel functions support all the same features as HTTP functions:

```typescript
// Pagination with channels
listTodosChannel({
  channel: ashTypeScriptRpcChannel,
  fields: ["id", "title", { user: ["name"] }],
  filter: { status: "active" },
  page: { limit: 10, offset: 0 },
  resultHandler: (result) => {
    if (result.success) {
      console.log("Todos:", result.data.results);
      console.log("Has more:", result.data.hasMore);
    }
  }
});

// Complex field selection
getTodoChannel({
  channel: ashTypeScriptRpcChannel,
  primaryKey: "todo-123",
  fields: [
    "id", "title", "description",
    {
      user: ["name", "email"],
      comments: ["text", { author: ["name"] }]
    }
  ],
  resultHandler: (result) => {
    // Fully type-safe result handling
  }
});
```

#### Error Handling

Channel functions provide the same error structure as HTTP functions:

```typescript
createTodoChannel({
  channel: myChannel,
  fields: ["id", "title"],
  input: { title: "New Todo" },
  resultHandler: (result) => {
    if (result.success) {
      // result.data is fully typed based on selected fields
      console.log("Created:", result.data.title);
    } else {
      // Handle validation errors, network errors, etc.
      result.errors.forEach(error => {
        console.error(`Error: ${error.message}`);
        if (error.fieldPath) {
          console.error(`Field: ${error.fieldPath}`);
        }
      });
    }
  },
  errorHandler: (error) => {
    // Handle channel-level errors
    console.error("Channel communication error:", error);
  },
  timeoutHandler: () => {
    // Handle timeouts
    console.error("Request timed out");
  }
});
```

### Advanced Filtering and Pagination

```typescript
import { listTodos } from './ash_rpc';

// Complex filtering with pagination
const result = await listTodos({
  fields: ["id", "title", "priority", "dueDate", { user: ["name"] }],
  filter: {
    and: [
      { status: { eq: "ongoing" } },
      { priority: { in: ["high", "urgent"] } },
      {
        or: [
          { dueDate: { lessThan: "2024-12-31" } },
          { user: { name: { eq: "John Doe" } } }
        ]
      }
    ]
  },
  sort: "-priority,+dueDate",
  page: {
    limit: 20,
    offset: 0,
    count: true
  }
});

if (result.success) {
  console.log(`Found ${result.data.count} todos`);
  console.log(`Showing ${result.data.results.length} results`);
  console.log(`Has more: ${result.data.hasMore}`);
}
```

## ๐Ÿ”ง Advanced Features

### Embedded Resources

Full support for embedded resources with type safety:

```elixir
# In your resource
attribute :metadata, MyApp.TodoMetadata do
  public? true
end
```

```typescript
// TypeScript usage
const todo = await getTodo({
  fields: [
    "id", "title",
    { metadata: ["priority", "tags", "customFields"] }
  ],
  id: "todo-123"
});
```

### Union Types

Support for Ash union types with selective field access:

```elixir
# In your resource
attribute :content, :union do
  constraints types: [
    text: [type: :string],
    checklist: [type: MyApp.ChecklistContent]
  ]
end
```

```typescript
// TypeScript usage with union field selection
const todo = await getTodo({
  fields: [
    "id", "title",
    { content: ["text", { checklist: ["items", "completedCount"] }] }
  ],
  id: "todo-123"
});
```

### Multitenancy Support

Automatic tenant parameter handling for multitenant resources:

```elixir
# Configuration
config :ash_typescript, require_tenant_parameters: true
```

```typescript
// Tenant parameters automatically added to function signatures
const todos = await listTodos({
  fields: ["id", "title"],
  tenant: "org-123"
});
```

### Calculations and Aggregates

Full support for Ash calculations with type inference:

```elixir
# In your resource
calculations do
  calculate :full_name, :string do
    expr(first_name <> " " <> last_name)
  end
end
```

```typescript
// TypeScript usage
const users = await listUsers({
  fields: ["id", "firstName", "lastName", "fullName"]
});
```

## ๐Ÿš€ Advanced Features

### Zod Runtime Validation

AshTypescript generates Zod schemas for all your actions, enabling runtime type checking and form validation.

#### Enable Zod Generation

```elixir
# config/config.exs
config :ash_typescript,
  generate_zod_schemas: true,
  zod_import_path: "zod",  # or "@hookform/resolvers/zod" etc.
  zod_schema_suffix: "ZodSchema"
```

#### Generated Zod Schemas

For each action, AshTypescript generates validation schemas:

#### Zod Schema Examples

```typescript
// Generated schema for creating a todo
export const createTodoZodSchema = z.object({
  title: z.string().min(1),
  description: z.string().optional(),
  priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
  dueDate: z.date().optional(),
  tags: z.array(z.string()).optional()
});
```

### Form Validation Functions

AshTypescript generates dedicated validation functions for client-side form validation when `generate_validation_functions` is enabled:

```typescript
import { validateCreateTodo } from './ash_rpc';

// Validate form input before submission
const validationResult = await validateCreateTodo({
  input: {
    title: "New Todo",
    priority: "high"
  }
});

if (!validationResult.success) {
  // Handle validation errors
  validationResult.errors.forEach(error => {
    console.log(`Field ${error.fieldPath}: ${error.message}`);
  });
}
```

#### Channel-Based Validation

When both `generate_validation_functions` and `generate_phx_channel_rpc_actions` are enabled, AshTypescript also generates channel-based validation functions:

```typescript
import { validateCreateTodoChannel } from './ash_rpc';
import { Channel } from "phoenix";

// Validate over Phoenix channels
validateCreateTodoChannel({
  channel: myChannel,
  input: {
    title: "New Todo",
    priority: "high"
  },
  resultHandler: (result) => {
    if (result.success) {
      console.log("Validation passed");
    } else {
      result.errors.forEach(error => {
        console.log(`Field ${error.fieldPath}: ${error.message}`);
      });
    }
  },
  errorHandler: (error) => console.error("Channel error:", error),
  timeoutHandler: () => console.error("Validation timeout")
});
```

### Type-Safe Filtering

AshTypescript automatically generates comprehensive filter types for all resources:

```typescript
import { listTodos } from './ash_rpc';

// Complex filtering with full type safety
const todos = await listTodos({
  fields: ["id", "title", "status", "priority"],
  filter: {
    and: [
      { status: { eq: "ongoing" } },
      { priority: { in: ["high", "urgent"] } },
      {
        or: [
          { dueDate: { lessThan: "2024-12-31" } },
          { isOverdue: { eq: true } }
        ]
      }
    ]
  },
  sort: "-priority,+dueDate"
});
```

#### Available Filter Operators

- **Equality**: `eq`, `notEq`, `in`
- **Comparison**: `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`
- **Logic**: `and`, `or`, `not`
- **Relationships**: Nested filtering on related resources

### Typed Queries for SSR

Define reusable, type-safe queries for server-side rendering and optimized data fetching:

#### Define Typed Queries

```elixir
defmodule MyApp.Domain do
  use Ash.Domain, extensions: [AshTypescript.Rpc]

  typescript_rpc do
    resource MyApp.Todo do
      # Regular RPC actions
      rpc_action :list_todos, :read

      # Typed query with predefined fields
      typed_query :dashboard_todos, :read do
        ts_result_type_name "DashboardTodosResult"
        ts_fields_const_name "dashboardTodosFields"

        fields [
          :id, :title, :priority, :isOverdue,
          %{
            user: [:name, :email],
            comments: [:id, :content]
          }
        ]
      end
    end
  end
end
```

#### Generated TypeScript Types

```typescript
// Generated type for the typed query result
export type DashboardTodosResult = Array<InferResult<TodoResourceSchema,
  ["id", "title", "priority", "isOverdue",
   {
     user: ["name", "email"],
     comments: ["id", "content"]
   }]
>>;

// Reusable field constant for client-side refetching
export const dashboardTodosFields = [
  "id", "title", "priority", "isOverdue",
  {
    user: ["name", "email"],
    comments: ["id", "content"]
  }
] as const;
```

#### Server-Side Usage

```elixir
# In your Phoenix controller
defmodule MyAppWeb.DashboardController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    result = AshTypescript.Rpc.run_typed_query(:my_app, :dashboard_todos, %{}, conn)

    case result do
      %{"success" => true, "data" => todos} ->
        render(conn, "index.html", todos: todos)
      %{"success" => false, "errors" => errors} ->
        conn
        |> put_status(:bad_request)
        |> render("error.html", errors: errors)
    end
  end
end
```

#### Client-Side Refetching

```typescript
// Use the same field selection for client-side updates
const refreshedTodos = await listTodos({
  fields: dashboardTodosFields,
  filter: { isOverdue: { eq: true } }
});
```

### Flexible Field Formatting

Configure separate formatters for input parsing and output generation:

```elixir
# config/config.exs
config :ash_typescript,
  # How client field names are converted to internal Elixir fields (default is :camel_case)
  input_field_formatter: :camel_case,
  # How internal Elixir fields are formatted for client consumption (default is :camel_case)
  output_field_formatter: :camel_case
```

#### Available Formatters

- `:camel_case` - `user_name` โ†’ `userName`
- `:pascal_case` - `user_name` โ†’ `UserName`
- `:snake_case` - `user_name` โ†’ `user_name`
- Custom formatter: `{MyModule, :format_field}` or `{MyModule, :format_field, [extra_args]}`

#### Different Input/Output Formatting

```elixir
# Use different formatting for input vs output
config :ash_typescript,
  input_field_formatter: :snake_case,    # Client sends snake_case
  output_field_formatter: :camel_case    # Client receives camelCase
```

#### Unconstrained Map Handling

Actions that accept or return unconstrained maps (maps without specific field constraints) bypass standard field name formatting:

**Input Maps**: When an action input is an unconstrained map, field names are passed through as-is without applying the `input_field_formatter`. This allows maximum flexibility for dynamic data structures.

**Output Maps**: When an action returns an unconstrained map, field names are returned as-is without applying the `output_field_formatter`. The entire map is returned without field selection processing.

```elixir
# Action that accepts/returns unconstrained map
defmodule MyApp.DataProcessor do
  use Ash.Resource, domain: MyApp.Domain

  actions do
    action :process_raw_data, :map do
      argument :raw_data, :map  # Unconstrained map input
      # Returns unconstrained map
    end
  end
end
```

```typescript
// Generated TypeScript - no field formatting applied
const result = await processRawData({
  input: {
    // Field names sent exactly as specified (no camelCase conversion)
    user_name: "john",
    created_at: "2024-01-01",
    nested_data: { field_one: "value" }
  }
  // Note: no fields parameter - entire result map returned
});

// Result contains original field names as stored in the backend
if (result.success) {
  // Field names received exactly as returned by Elixir (no camelCase conversion)
  console.log(result.data.user_name);  // Access with original snake_case
  console.log(result.data.created_at);
}
```

## โš™๏ธ Configuration

### Application Configuration

```elixir
# config/config.exs
config :ash_typescript,
  # File generation
  output_file: "assets/js/ash_rpc.ts",

  # RPC endpoints
  run_endpoint: "/rpc/run",
  validate_endpoint: "/rpc/validate",

  # Field formatting
  input_field_formatter: :camel_case,
  output_field_formatter: :camel_case,

  # Multitenancy
  require_tenant_parameters: false,

  # Zod schema generation
  generate_zod_schemas: true,
  zod_import_path: "zod",
  zod_schema_suffix: "ZodSchema",

  # Validation functions
  generate_validation_functions: true,

  # Phoenix channel-based RPC actions
  generate_phx_channel_rpc_actions: false,
  phoenix_import_path: "phoenix",

  # Custom type imports
  import_into_generated: [
    %{
      import_name: "CustomTypes",
      file: "./customTypes"
    }
  ]
```

### Domain Configuration

```elixir
defmodule MyApp.Domain do
  use Ash.Domain, extensions: [AshTypescript.Rpc]

  typescript_rpc do
    resource MyApp.Todo do
      # Standard CRUD actions
      rpc_action :list_todos, :read
      rpc_action :get_todo, :get
      rpc_action :create_todo, :create
      rpc_action :update_todo, :update
      rpc_action :destroy_todo, :destroy

      # Custom actions
      rpc_action :complete_todo, :complete
      rpc_action :archive_todo, :archive

      # Typed queries for SSR and optimized data fetching
      typed_query :dashboard_todos, :read do
        ts_result_type_name "DashboardTodosResult"
        ts_fields_const_name "dashboardTodosFields"

        fields [
          :id, :title, :priority, :status,
          %{
            user: [:name, :email],
            comments: [:id, :content]
          },
        ]
      end
    end

    resource MyApp.User do
      rpc_action :list_users, :read
      rpc_action :get_user, :get
    end
  end
end
```

### Field Formatting

Customize how field names are formatted in generated TypeScript:

```elixir
# Default: snake_case โ†’ camelCase
# user_name โ†’ userName
# created_at โ†’ createdAt
```

### Custom Types

Create custom Ash types with TypeScript integration:

```elixir
# 1. Create custom type in Elixir
defmodule MyApp.PriorityScore do
  use Ash.Type

  def storage_type(_), do: :integer
  def cast_input(value, _) when is_integer(value) and value >= 1 and value <= 100, do: {:ok, value}
  def cast_input(_, _), do: {:error, "must be integer 1-100"}
  def cast_stored(value, _), do: {:ok, value}
  def dump_to_native(value, _), do: {:ok, value}
  def apply_constraints(value, _), do: {:ok, value}

  # AshTypescript integration
  def typescript_type_name, do: "CustomTypes.PriorityScore"
end
```

```typescript
// 2. Create TypeScript type definitions in customTypes.ts
export type PriorityScore = number;

export type ColorPalette = {
  primary: string;
  secondary: string;
  accent: string;
};
```

```elixir
# 3. Use in your resources
defmodule MyApp.Todo do
  use Ash.Resource, domain: MyApp.Domain

  attributes do
    uuid_primary_key :id
    attribute :title, :string, public?: true
    attribute :priority_score, MyApp.PriorityScore, public?: true
  end
end
```

The generated TypeScript will automatically include your custom types:

```typescript
// Generated TypeScript includes imports
import * as CustomTypes from "./customTypes";

// Your resource types use the custom types
interface TodoFieldsSchema {
  id: string;
  title: string;
  priorityScore?: CustomTypes.PriorityScore | null;
}
```

## ๐Ÿ› ๏ธ Mix Tasks

### Installation Commands

#### `mix igniter.install ash_typescript` (Recommended)

**Automated installer** that sets up everything you need to get started with AshTypescript.

```bash
# Basic installation (RPC setup only)
mix igniter.install ash_typescript

# Full-stack React + TypeScript setup
mix igniter.install ash_typescript --framework react
```

**What it does:**
- Adds AshTypescript to your dependencies and runs `mix deps.get`
- Configures AshTypescript settings in `config/config.exs`
- Creates RPC controller (`lib/*_web/controllers/ash_typescript_rpc_controller.ex`)
- Adds RPC routes to your Phoenix router
- **With `--framework react`**: Sets up complete React + TypeScript environment
- **With `--framework react`**: Creates welcome page with getting started guide

**When to use**: For new projects or when adding AshTypescript to existing projects. This is the recommended approach.

### Code Generation Commands

#### `mix ash.codegen` (Recommended)

**Preferred approach** for generating TypeScript types along with other Ash extensions in your project.

```bash
# Generate types for all Ash extensions including AshTypescript
mix ash.codegen --dev

# With custom output location
mix ash.codegen --dev --output "assets/js/ash_rpc.ts"
```

**When to use**: When you have multiple Ash extensions (AshPostgres, etc.) and want to run codegen for all of them together. This is the recommended approach for most projects.

#### `mix ash_typescript.codegen` (Specific)

Generate TypeScript types, RPC clients, Zod schemas, and validation functions **only for AshTypescript**.

**When to use**: When you want to run codegen specifically for AshTypescript only in your project.

**Options:**
- `--output` - Output file path (default: `assets/js/ash_rpc.ts`)
- `--run_endpoint` - RPC run endpoint (default: `/rpc/run`)
- `--validate_endpoint` - RPC validate endpoint (default: `/rpc/validate`)
- `--check` - Check if generated code is up to date (useful for CI)
- `--dry_run` - Print generated code without writing to file

**Generated Content:**
- TypeScript interfaces for all resources
- RPC client functions for each action
- Filter input types for type-safe querying
- Zod validation schemas (if enabled)
- Form validation functions
- Typed query constants and types
- Custom type imports

**Examples:**

```bash
# Basic generation (AshTypescript only)
mix ash_typescript.codegen

# Custom output location
mix ash_typescript.codegen --output "frontend/src/api/ash.ts"

# Custom RPC endpoints
mix ash_typescript.codegen \
  --run_endpoint "/api/rpc/run" \
  --validate_endpoint "/api/rpc/validate"

# Check if generated code is up to date (CI usage)
mix ash_typescript.codegen --check

# Preview generated code without writing to file
mix ash_typescript.codegen --dry_run
```

## ๐Ÿ“– API Reference

### Generated Code Structure

AshTypescript generates:

1. **TypeScript interfaces** for all resources with metadata for field selection
2. **RPC client functions** for each exposed action
3. **Validation functions** for client-side form validation
4. **Filter input types** for type-safe querying with comprehensive operators
5. **Zod schemas** for runtime validation (when enabled)
6. **Typed query constants** and result types for SSR
7. **Field selection types** for type-safe field specification
8. **Custom type imports** for external TypeScript definitions
9. **Enum types** for Ash enum types
10. **Utility functions** for headers and CSRF protection

### Generated Functions

For each `rpc_action` in your domain, AshTypescript generates:

```typescript
// For rpc_action :list_todos, :read
function listTodos<Fields extends ListTodosFields>(params: {
  fields: Fields;
  filter?: TodoFilterInput;
  sort?: string;
  page?: PaginationOptions;
  headers?: Record<string, string>;
  fetchOptions?: RequestInit;
  customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<ListTodosResult<Fields>>;

// Validation function for list_todos
function validateListTodos(params: {
  input: ListTodosInput;
  headers?: Record<string, string>;
  fetchOptions?: RequestInit;
  customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<ValidateListTodosResult>;

// For rpc_action :create_todo, :create
function createTodo<Fields extends CreateTodosFields>(params: {
  fields: Fields;
  input: CreateTodoInput;
  headers?: Record<string, string>;
  fetchOptions?: RequestInit;
  customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<CreateTodoResult<Fields>>;

// Validation function for create_todo
function validateCreateTodo(params: {
  input: CreateTodoInput;
  headers?: Record<string, string>;
  fetchOptions?: RequestInit;
  customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<ValidateCreateTodoResult>;

// Zod schemas (when enabled)
export const createTodoZodSchema: z.ZodObject<...>;
export const listTodosZodSchema: z.ZodObject<...>;
```

### Utility Functions

```typescript
// CSRF protection for Phoenix applications
function getPhoenixCSRFToken(): string | null;
function buildCSRFHeaders(): Record<string, string>;
```

## ๐Ÿ“‹ Requirements

- **Elixir** ~> 1.15
- **Ash** ~> 3.5
- **AshPhoenix** ~> 2.0 (for RPC endpoints)

## ๐Ÿ› Troubleshooting

### Common Issues

**TypeScript compilation errors:**
- Ensure generated types are up to date: `mix ash_typescript.codegen`
- Check that all referenced resources are properly configured

**RPC endpoint errors:**
- Verify AshPhoenix RPC endpoints are configured in your router
- Check that actions are properly exposed in domain RPC configuration

**Type inference issues:**
- Ensure all attributes are marked as `public? true`
- Check that relationships are properly defined

### Debug Commands

```bash
# Check generated output without writing
mix ash_typescript.codegen --dry_run

# Validate TypeScript compilation
cd assets/js && npx tsc --noEmit

# Check for updates
mix ash_typescript.codegen --check
```

## ๐Ÿค Contributing

### Development Setup

```bash
# Clone the repository
git clone https://github.com/ash-project/ash_typescript.git
cd ash_typescript

# Install dependencies
mix deps.get

# Run tests
mix test

# Generate test types
mix test.codegen
```

## ๐Ÿ“„ License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## ๐Ÿ†˜ Support

- **Documentation**: [hexdocs.pm/ash_typescript](https://hexdocs.pm/ash_typescript)
- **Demo App**: [AshTypescript Demo](https://github.com/ChristianAlexander/ash_typescript_demo) - Real-world example with TanStack Query & Table
- **Issues**: [GitHub Issues](https://github.com/ash-project/ash_typescript/issues)
- **Discussions**: [GitHub Discussions](https://github.com/ash-project/ash_typescript/discussions)
- **Ash Community**: [Ash Framework Discord](https://discord.gg/ash-framework)

---

**Built with โค๏ธ by the Ash Framework team**

*Generate once, type everywhere. Make your Elixir-TypeScript integration bulletproof.*