Packages
ash_typescript
0.3.2
0.19.0
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.3
0.15.2
retired
0.15.1
retired
0.15.0
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
retired
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.3
0.3.2
0.3.1
0.2.0
0.1.2
0.1.0
Generate type-safe TypeScript clients directly from your Ash resources and actions, ensuring end-to-end type safety between your backend and frontend.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
ash_typescript
README.md
README.md
<img src="https://github.com/ash-project/ash_typescript/blob/main/logos/ash-typescript.png?raw=true" alt="Logo" width="300"/>[](https://opensource.org/licenses/MIT)[](https://hex.pm/packages/ash_typescript)[](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.[](https://hex.pm/packages/ash_typescript)[](https://hexdocs.pm/ash_typescript)[](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 installmix 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`:```elixirdef deps do [ {:ash_typescript, "~> 0.3.2"} ]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.```elixirdefmodule 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 endend```### 3. Set up Phoenix RPC controller```elixirdefmodule 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) endend```### 4. Add RPC routesAdd these routes to your `router.ex` to map the RPC endpoints:```elixirscope "/rpc", MyAppWeb do pipe_through :api # or :browser if using session-based auth post "/run", RpcController, :run post "/validate", RpcController, :validateend```### 5. Generate TypeScript types**After using the installer or completing manual setup:****Recommended approach** (runs codegen for all Ash extensions in your project):```bashmix ash.codegen --dev"```**Alternative approach** (runs codegen only for AshTypescript):```bashmix ash_typescript.codegen --output "assets/js/ash_rpc.ts"```### 6. Use in your frontend```typescriptimport { listTodos, createTodo } from './ash_rpc';// ✅ Fully type-safe API callsconst 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 appThe welcome page includes:- Step-by-step setup instructions- Code examples with syntax highlighting- Links to documentation and demo projects- Type-safe RPC function examplesVisit `http://localhost:4000/ash-typescript` after running your Phoenix server to see the interactive guide!### 🚀 Example RepoCheck 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## ✨ 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 works1. **Resource Definition**: Define your Ash resources with attributes, relationships, and actions2. **RPC Configuration**: Expose specific actions through your domain's RPC configuration3. **Type Generation**: Run `mix ash_typescript.codegen` to generate TypeScript types4. **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```typescriptimport { listTodos, getTodo, createTodo, updateTodo, destroyTodo } from './ash_rpc';// List todos with field selectionconst todos = await listTodos({ fields: ["id", "title", "completed", "priority"], filter: { status: "active" }, sort: "-priority,+createdAt"});// Get single todo with relationshipsconst todo = await getTodo({ fields: ["id", "title", { user: ["name", "email"] }], id: "todo-123"});// Create new todoconst 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 selectionconst todoWithDetails = await getTodo({ fields: [ "id", "title", "description", { user: ["name", "email", "avatarUrl"], comments: ["id", "text", { author: ["name"] }], tags: ["name", "color"] } ], id: "todo-123"});// Calculations with argumentsconst todoWithCalc = await getTodo({ fields: [ "id", "title", { "priorityScore": { "args": { "multiplier": 2 }, "fields": ["score", "rank"] } } ], id: "todo-123"});```### Error HandlingAll generated RPC functions return a `{success: true/false}` structure instead of throwing exceptions:```typescriptconst 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```typescriptimport { listTodos, buildCSRFHeaders } from './ash_rpc';// With CSRF protectionconst todos = await listTodos({ fields: ["id", "title"], headers: buildCSRFHeaders()});// With custom authenticationconst todos = await listTodos({ fields: ["id", "title"], headers: { "Authorization": "Bearer your-token-here", "X-Custom-Header": "value" }});```### Custom Fetch Functions and Request OptionsAshTypescript allows you to customize the HTTP client used for requests by providing custom fetch functions and additional fetch options.#### Using fetchOptions for Request CustomizationAll generated RPC functions accept an optional `fetchOptions` parameter that allows you to customize the underlying fetch request:```typescriptimport { createTodo, listTodos } from './ash_rpc';// Add request timeout and custom cache settingsconst 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 requestsconst controller = new AbortController();const todos = await listTodos({ fields: ["id", "title"], fetchOptions: { signal: controller.signal }});// Cancel the request if neededcontroller.abort();```#### Custom Fetch FunctionsYou 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 trackingconst 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 functionconst todos = await listTodos({ fields: ["id", "title"], customFetch: enhancedFetch});```#### Using Axios with AshTypescriptWhile AshTypescript uses the fetch API by default, you can create an adapter to use axios or other HTTP clients:```typescriptimport axios from 'axios';// Create axios adapter that matches fetch APIconst 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 requestsconst todos = await listTodos({ fields: ["id", "title"], customFetch: axiosAdapter});```### Phoenix Channel-based RPC ActionsAshTypescript 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.#### ConfigurationEnable channel function generation in your configuration:```elixir# config/config.exsconfig :ash_typescript, generate_phx_channel_rpc_actions: true, phoenix_import_path: "phoenix" # customize if needed```#### Generated Channel FunctionsWhen enabled, AshTypescript generates channel functions with the suffix `Channel` for each RPC action:```typescriptimport { 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 ChannelsFirst, establish a Phoenix channel connection:```typescriptimport { 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 SetupTo 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 equivalentdefmodule 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.idend# In your my_app_web/channels/ash_typescript_rpc_channel.exdefmodule 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} endend```**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 FeaturesChannel functions support all the same features as HTTP functions:```typescript// Pagination with channelslistTodosChannel({ 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 selectiongetTodoChannel({ channel: ashTypeScriptRpcChannel, primaryKey: "todo-123", fields: [ "id", "title", "description", { user: ["name", "email"], comments: ["text", { author: ["name"] }] } ], resultHandler: (result) => { // Fully type-safe result handling }});```#### Error HandlingChannel functions provide the same error structure as HTTP functions:```typescriptcreateTodoChannel({ 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```typescriptimport { listTodos } from './ash_rpc';// Complex filtering with paginationconst 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 ResourcesFull support for embedded resources with type safety:```elixir# In your resourceattribute :metadata, MyApp.TodoMetadata do public? trueend``````typescript// TypeScript usageconst todo = await getTodo({ fields: [ "id", "title", { metadata: ["priority", "tags", "customFields"] } ], id: "todo-123"});```### Union TypesSupport for Ash union types with selective field access:```elixir# In your resourceattribute :content, :union do constraints types: [ text: [type: :string], checklist: [type: MyApp.ChecklistContent] ]end``````typescript// TypeScript usage with union field selectionconst todo = await getTodo({ fields: [ "id", "title", { content: ["text", { checklist: ["items", "completedCount"] }] } ], id: "todo-123"});```### Multitenancy SupportAutomatic tenant parameter handling for multitenant resources:```elixir# Configurationconfig :ash_typescript, require_tenant_parameters: true``````typescript// Tenant parameters automatically added to function signaturesconst todos = await listTodos({ fields: ["id", "title"], tenant: "org-123"});```### Calculations and AggregatesFull support for Ash calculations with type inference:```elixir# In your resourcecalculations do calculate :full_name, :string do expr(first_name <> " " <> last_name) endend``````typescript// TypeScript usageconst users = await listUsers({ fields: ["id", "firstName", "lastName", "fullName"]});```## 🚀 Advanced Features### Zod Runtime ValidationAshTypescript generates Zod schemas for all your actions, enabling runtime type checking and form validation.#### Enable Zod Generation```elixir# config/config.exsconfig :ash_typescript, generate_zod_schemas: true, zod_import_path: "zod", # or "@hookform/resolvers/zod" etc. zod_schema_suffix: "ZodSchema"```#### Generated Zod SchemasFor each action, AshTypescript generates validation schemas:#### Zod Schema Examples```typescript// Generated schema for creating a todoexport 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 FunctionsAshTypescript generates dedicated validation functions for client-side form validation when `generate_validation_functions` is enabled:```typescriptimport { validateCreateTodo } from './ash_rpc';// Validate form input before submissionconst 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 ValidationWhen both `generate_validation_functions` and `generate_phx_channel_rpc_actions` are enabled, AshTypescript also generates channel-based validation functions:```typescriptimport { validateCreateTodoChannel } from './ash_rpc';import { Channel } from "phoenix";// Validate over Phoenix channelsvalidateCreateTodoChannel({ 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 FilteringAshTypescript automatically generates comprehensive filter types for all resources:```typescriptimport { listTodos } from './ash_rpc';// Complex filtering with full type safetyconst 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 SSRDefine reusable, type-safe queries for server-side rendering and optimized data fetching:#### Define Typed Queries```elixirdefmodule 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 endend```#### Generated TypeScript Types```typescript// Generated type for the typed query resultexport type DashboardTodosResult = Array<InferResult<TodoResourceSchema, ["id", "title", "priority", "isOverdue", { user: ["name", "email"], comments: ["id", "content"] }]>>;// Reusable field constant for client-side refetchingexport const dashboardTodosFields = [ "id", "title", "priority", "isOverdue", { user: ["name", "email"], comments: ["id", "content"] }] as const;```#### Server-Side Usage```elixir# In your Phoenix controllerdefmodule 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 endend```#### Client-Side Refetching```typescript// Use the same field selection for client-side updatesconst refreshedTodos = await listTodos({ fields: dashboardTodosFields, filter: { isOverdue: { eq: true } }});```### Flexible Field FormattingConfigure separate formatters for input parsing and output generation:```elixir# config/config.exsconfig :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 outputconfig :ash_typescript, input_field_formatter: :snake_case, # Client sends snake_case output_field_formatter: :camel_case # Client receives camelCase```## ⚙️ Configuration### Application Configuration```elixir# config/config.exsconfig :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```elixirdefmodule 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 endend```### Field FormattingCustomize how field names are formatted in generated TypeScript:```elixir# Default: snake_case → camelCase# user_name → userName# created_at → createdAt```### Custom TypesCreate custom Ash types with TypeScript integration:```elixir# 1. Create custom type in Elixirdefmodule 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.tsexport type PriorityScore = number;export type ColorPalette = { primary: string; secondary: string; accent: string;};``````elixir# 3. Use in your resourcesdefmodule 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 endend```The generated TypeScript will automatically include your custom types:```typescript// Generated TypeScript includes importsimport * as CustomTypes from "./customTypes";// Your resource types use the custom typesinterface 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 setupmix 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 AshTypescriptmix ash.codegen --dev# With custom output locationmix 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 locationmix ash_typescript.codegen --output "frontend/src/api/ash.ts"# Custom RPC endpointsmix 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 filemix ash_typescript.codegen --dry_run```## 📖 API Reference### Generated Code StructureAshTypescript generates:1. **TypeScript interfaces** for all resources with metadata for field selection2. **RPC client functions** for each exposed action3. **Validation functions** for client-side form validation4. **Filter input types** for type-safe querying with comprehensive operators5. **Zod schemas** for runtime validation (when enabled)6. **Typed query constants** and result types for SSR7. **Field selection types** for type-safe field specification8. **Custom type imports** for external TypeScript definitions9. **Enum types** for Ash enum types10. **Utility functions** for headers and CSRF protection### Generated FunctionsFor each `rpc_action` in your domain, AshTypescript generates:```typescript// For rpc_action :list_todos, :readfunction 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_todosfunction 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, :createfunction 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_todofunction 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 applicationsfunction 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 writingmix ash_typescript.codegen --dry_run# Validate TypeScript compilationcd assets/js && npx tsc --noEmit# Check for updatesmix ash_typescript.codegen --check```## 🤝 Contributing### Development Setup```bash# Clone the repositorygit clone https://github.com/ash-project/ash_typescript.gitcd ash_typescript# Install dependenciesmix deps.get# Run testsmix test# Generate test typesmix test.codegen```## 📄 LicenseThis 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.*