README.md

# AshRpc

**Expose Ash Resource actions over tRPC with a Plug-compatible router/controller, robust error handling, and schema tooling.**

AshRpc is a comprehensive bridge between [Ash Framework](https://ash-hq.org) and [tRPC](https://trpc.io), enabling you to expose your Ash resources as type-safe, performant tRPC endpoints. It provides seamless integration with Phoenix applications, automatic TypeScript generation, and advanced features like field selection and batching.

> ⚠️ **EXPERIMENTAL WARNING**: This package is still in early development and considered highly experimental. Breaking changes may occur frequently without notice. We strongly advise against using this package in production environments until it reaches a stable release (v1.0.0+). Use at your own risk for development and testing purposes only.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Backend Setup](#backend-setup)
- [Frontend Integration](#frontend-integration)
- [Authentication](#authentication)
- [API Reference](#api-reference)
- [Contributing](#contributing)

## Features

### 🚀 **Core Features**

- **Simple Setup**: One-line router configuration with `use AshRpc.Router`
- **Spark DSL**: Declarative exposure of Ash resource actions
- **tRPC Compliance**: Full tRPC specification support with proper envelopes
- **Error Handling**: Robust, structured error responses with detailed validation messages
- **Type Safety**: Automatic TypeScript generation for end-to-end type safety

### 🔧 **Advanced Capabilities**

- **Batching**: Efficient request batching with `?batch=1` support
- **Field Selection**: Dynamic field selection with include/exclude semantics
- **Filtering & Sorting**: Rich query capabilities with complex filter expressions
- **Pagination**: Offset and keyset pagination with automatic detection
- **Relationships**: Nested relationship loading with query options

### 🛠 **Developer Experience**

- **Auto-Generation**: TypeScript types and Zod schemas from your Ash resources
- **IntelliSense**: Full IDE support with generated type definitions

## Quick Start

### 1. Install AshRpc

```bash
# If you have Igniter installed (recommended)
mix igniter.install ash_rpc

# Or manually install
mix deps.get
mix ash_rpc.install
```

This creates your tRPC router and configures your Phoenix router.

### 2. Configure Your Resources

```elixir
defmodule MyApp.Accounts.User do
  use Ash.Resource, extensions: [AshRpc]

  ash_rpc do
    expose [:read, :create, :update]

    query :read do
      filterable true
      sortable true
      selectable true
      paginatable true
    end

    mutation :create, :create do
      metadata fn _subject, user, _ctx ->
        %{user_id: user.id}
      end
    end
  end

  # ... rest of your resource
end
```

### 3. Update Your Router

```elixir
defmodule MyAppWeb.TrpcRouter do
  use AshRpc.Router, domains: [MyApp.Accounts, MyApp.Billing]
end
```

### 4. Generate Types

```bash
mix ash_rpc.codegen --output=./frontend/generated --zod
```

### 5. Use in Frontend

```typescript
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./generated/trpc";

const client = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: "/trpc" })],
});

// Type-safe API calls
const users = await client.accounts.user.read.query({
  filter: { email: { eq: "user@example.com" } },
  select: ["id", "email", "name"],
  page: { limit: 10, offset: 0 },
});
```

## Installation

### Add Dependencies

Add `ash_rpc` to your `mix.exs`:

```elixir
defp deps do
  [
    {:ash_rpc, "~> 0.1"},
    # Recommended for type generation
    # For authentication (optional)
    {:ash_authentication, "~> 3.0"},
  ]
end
```

### Install AshRpc

Run the installer to set up your Phoenix application:

```bash
mix deps.get
mix ash_rpc.install
```

This will:

- Generate `MyAppWeb.TrpcRouter` module
- Add tRPC pipeline to your Phoenix router
- Configure route forwarding to `/trpc`

### Manual Setup (Alternative)

If you prefer manual setup, create the router manually:

```elixir
# lib/my_app_web/trpc_router.ex
defmodule MyAppWeb.TrpcRouter do
  use AshRpc.Router, domains: [MyApp.Accounts]
end

# router.ex
scope "/trpc" do
  pipe_through :ash_rpc
  forward "/", MyAppWeb.TrpcRouter
end
```

## Backend Setup

### Router Configuration

```elixir
defmodule MyAppWeb.TrpcRouter do
  use AshRpc.Router,
    domains: [MyApp.Accounts, MyApp.Billing, MyApp.Notifications],
    # Optional: Custom transformer for input/output processing
    transformer: MyApp.TrpcTransformer,
    # Optional: Before hooks
    before: [MyApp.TrpcHooks.Logging],
    # Optional: After hooks
    after: [MyApp.TrpcHooks.Metrics],
    # Optional: Context creation function
    create_context: &MyApp.TrpcContext.create/1
end
```

### Resource Configuration

```elixir
defmodule MyApp.Accounts.User do
  use Ash.Resource,
    extensions: [AshRpc],
    domain: MyApp.Accounts

  ash_rpc do
    # Expose specific actions
    expose [:read, :create, :update, :destroy]

    # Or expose all actions
    # expose :all

    # Custom resource name (defaults to module name)
    resource_name "user"

    # Configure query procedures
    query :read do
      filterable true      # Allow client-side filtering
      sortable true        # Allow client-side sorting
      selectable true      # Allow client-side field selection
      paginatable true     # Allow client-side pagination

      # Custom relationship loading
      relationships [:posts, :comments]
    end

    query :by_email, :read do
      # Custom procedure name for specific action
      filterable false
      selectable true
    end

    # Configure mutation procedures
    mutation :create, :create do
      metadata fn _subject, user, _ctx ->
        %{user_id: user.id, created_at: user.inserted_at}
      end
    end

    mutation :register, :register_with_password do
      metadata fn _subject, user, _ctx ->
        %{token: user.__metadata__.token}
      end
    end
  end

  # ... resource definition
end
```

### DSL Reference

#### `ash_rpc` Block Options

- `expose`: List of actions to expose (`:all` or specific action names)
- `resource_name`: Override the default resource segment name
- `methods`: Override default method mappings (`[read: :query, create: :mutation]`)

#### Query Configuration

```elixir
query :read do
  filterable true        # Enable filtering (default: true)
  sortable true         # Enable sorting (default: true)
  selectable true       # Enable field selection (default: true)
  paginatable true      # Enable pagination (default: true)
  relationships [:posts] # Allow loading specific relationships
end
```

#### Mutation Configuration

```elixir
mutation :create, :create do
  metadata fn subject, result, ctx ->
    # Return custom metadata in response
    %{created_by: subject.id, timestamp: DateTime.utc_now()}
  end
end
```

## Authentication

AshRpc integrates seamlessly with AshAuthentication for secure API access.

### Setup Authentication

```elixir
# In your Phoenix router
pipeline :ash_rpc do
  plug :accepts, ["json"]
  plug :retrieve_from_bearer  # Extract token from Authorization header
  plug :set_actor, :user      # Set current user as actor
end

scope "/trpc" do
  pipe_through :ash_rpc
  forward "/", MyAppWeb.TrpcRouter
end
```

### Client Authentication

```typescript
// Include token in requests
const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "/trpc",
      headers() {
        const token = getAuthToken();
        return token ? { Authorization: `Bearer ${token}` } : {};
      },
    }),
  ],
});
```

### Authorization

AshRpc respects Ash's authorization rules. Configure policies on your resources:

```elixir
defmodule MyApp.Accounts.User do
  # ... resource setup

  policies do
    policy action_type(:read) do
      authorize_if actor_attribute_equals(:role, :admin)
      authorize_if relates_to_actor_via(:self)
    end
  end
end
```

## Frontend Integration

### tRPC Client Setup

```typescript
// client.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./generated/trpc";

export function createClient(token?: string) {
  return createTRPCClient<AppRouter>({
    links: [
      httpBatchLink({
        url: "/trpc",
        headers: token ? { Authorization: `Bearer ${token}` } : {},
      }),
    ],
  });
}
```

### React Integration

```tsx
// App.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "./generated/trpc";

export const trpc = createTRPCReact<AppRouter>();
const queryClient = new QueryClient();

function App() {
  return (
    <trpc.Provider client={createClient()} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <MyComponent />
      </QueryClientProvider>
    </trpc.Provider>
  );
}
```

### Usage Examples

```tsx
// UserList.tsx
import { trpc } from "./trpc";

function UserList() {
  const { data: users, isLoading } = trpc.accounts.user.read.useQuery({
    filter: { role: { eq: "admin" } },
    select: ["id", "email", "name"],
    sort: { insertedAt: "desc" },
    page: { limit: 20, offset: 0 },
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      {users?.result.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}
```

### Mutation Examples

```tsx
// CreateUser.tsx
import { trpc } from "./trpc";

function CreateUser() {
  const createUser = trpc.accounts.user.create.useMutation();

  const handleSubmit = async (data: FormData) => {
    try {
      const result = await createUser.mutateAsync({
        email: data.email,
        password: data.password,
        name: data.name,
      });

      console.log("Created user:", result.result);
      console.log("Metadata:", result.meta);
    } catch (error) {
      console.error("Failed to create user:", error);
    }
  };

  return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
```

## Advanced Features

### Batching

AshRpc supports request batching for improved performance:

```typescript
// Automatic batching with httpBatchLink
const client = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: "/trpc" })],
});

// Multiple queries batched automatically
const [users, posts] = await Promise.all([
  client.accounts.user.read.query({ limit: 10 }),
  client.blog.post.read.query({ limit: 10 }),
]);
```

### Field Selection

Dynamically select which fields to return:

```typescript
// Include specific fields
const users = await client.accounts.user.read.query({
  select: ["id", "email", "name"],
});

// Exclude fields with "-"
const users = await client.accounts.user.read.query({
  select: ["-password", "-insertedAt"],
});

// Nested field selection
const posts = await client.blog.post.read.query({
  select: [
    "id",
    "title",
    { author: ["name", "email"] },
    { comments: ["content", "-insertedAt"] },
  ],
});
```

### Filtering & Sorting

Rich query capabilities:

```typescript
// Complex filtering
const users = await client.accounts.user.read.query({
  filter: {
    and: [
      { email: { like: "%@company.com" } },
      { or: [{ role: { eq: "admin" } }, { role: { eq: "manager" } }] },
    ],
  },
  sort: { insertedAt: "desc" },
});
```

### Pagination

Support for both offset and keyset pagination:

```typescript
// Offset pagination
const users = await client.accounts.user.read.query({
  page: {
    type: "offset",
    limit: 20,
    offset: 40,
    count: true, // Include total count
  },
});

// Keyset pagination (recommended for large datasets)
const users = await client.accounts.user.read.query({
  page: {
    type: "keyset",
    limit: 20,
    after: "cursor_value",
    before: "cursor_value",
  },
});
```

## TypeScript Generation

### Generate Types

```bash
# Generate TypeScript types
mix ash_rpc.codegen --output=./frontend/generated

# Generate with Zod schemas
mix ash_rpc.codegen --output=./frontend/generated --zod
```

### Generated Files

- `trpc.d.ts`: TypeScript types for your tRPC router
- `trpc.zod.ts`: Zod schemas for client-side validation (optional)

### Usage

```typescript
import type { AppRouter } from "./generated/trpc";
import * as schemas from "./generated/trpc.zod";

// Full type safety
const client = createTRPCClient<AppRouter>();

// Client-side validation
const userSchema = schemas.AccountsUserCreateSchema;
const validated = userSchema.parse(formData);
```

## Error Handling

AshRpc provides comprehensive error handling with detailed messages:

```typescript
try {
  await client.accounts.user.create.mutate({
    email: "invalid-email", // Missing password
  });
} catch (error: any) {
  // error.shape?.message - High-level message
  // error.data?.details - Array of detailed error objects
  console.log(error.shape?.message); // "Validation failed"

  error.data?.details.forEach((detail) => {
    console.log(detail.message); // "password is required"
    console.log(detail.code); // "field_validation_error"
    console.log(detail.pointer); // "password"
  });
}
```

## API Reference

### Router Module

```elixir
defmodule AshRpc.Router do
  @moduledoc """
  Main router module for exposing Ash resources via tRPC.

  ## Options
  - `domains`: List of Ash domain modules to expose
  - `transformer`: Custom input/output transformer module
  - `before`: List of modules to run before request processing
  - `after`: List of modules to run after request processing
  - `create_context`: Function to create request context
  """
end
```

### DSL Module

```elixir
defmodule AshRpc do
  @moduledoc """
  Spark DSL extension for configuring tRPC exposure on Ash resources.

  ## DSL Structure
  ash_rpc do
    expose [:action1, :action2]
    resource_name "custom_name"

    query :action do
      filterable true
      sortable true
      selectable true
      paginatable true
      relationships [:rel1, :rel2]
    end

    mutation :action do
      metadata fn subject, result, ctx -> %{key: value} end
    end
  end
  """
end
```

## Examples

### Quick Examples

#### Basic CRUD Operations

```elixir
# Resource
defmodule MyApp.Blog.Post do
  use Ash.Resource, extensions: [AshRpc]

  ash_rpc do
    expose [:read, :create, :update, :destroy]

    query :read do
      filterable true
      sortable true
      selectable true
      paginatable true
      relationships [:author, :comments]
    end
  end
end

# Frontend usage
const posts = await client.blog.post.read.query({
  filter: { published: { eq: true } },
  sort: { publishedAt: "desc" },
  select: ["id", "title", "content", { author: ["name"] }],
  page: { limit: 10 }
});
```

#### Advanced Filtering

```typescript
// Complex queries with relationships
const posts = await client.blog.post.read.query({
  filter: {
    and: [
      { published: { eq: true } },
      { author: { name: { like: "John%" } } },
      {
        or: [
          { tags: { contains: "elixir" } },
          { tags: { contains: "phoenix" } },
        ],
      },
    ],
  },
  load: [
    { author: { filter: { active: { eq: true } } } },
    { comments: { sort: { insertedAt: "desc" }, limit: 5 } },
  ],
});
```

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests for new functionality
5. Run the test suite: `mix test`
6. Submit a pull request

### Development Setup

```bash
git clone https://github.com/antdragon-os/ash_rpc.git
cd ash_rpc
mix deps.get
mix test
```

### Documentation

Documentation is generated with ExDoc. To build locally:

```bash
mix docs
open doc/index.html
```

## License

Apache 2.0 - see `LICENSE`.

## Support

- **Issues**: [GitHub Issues](https://github.com/antdragon-os/ash_rpc/issues)
- **Discussions**: [GitHub Discussions](https://github.com/antdragon-os/ash_rpc/discussions)
- **Documentation**: [HexDocs](https://hexdocs.pm/ash_rpc)

---

Built with ❤️ using [Ash Framework](https://ash-hq.org) and [tRPC](https://trpc.io)