Skip to content

Latest commit

 

History

History
532 lines (429 loc) · 28.2 KB

File metadata and controls

532 lines (429 loc) · 28.2 KB

AshTypescript - AI Assistant Guide

Project Overview

AshTypescript generates TypeScript types and RPC clients from Ash resources, providing end-to-end type safety between Elixir backends and TypeScript frontends.

Key Features: Type generation, RPC client generation, Phoenix channel RPC actions, typed channel event subscriptions, typed controller route helpers, action metadata support, nested calculations, multitenancy, embedded resources, union types, field/argument/metadata name mapping, load restrictions, configurable RPC warnings, JSON manifest for third-party integrations

🚨 Critical Development Rules

Rule 1: Always Use Test Environment

❌ Wrong ✅ Correct Purpose
mix ash_typescript.codegen mix test.codegen Generate types
One-off shell debugging Write proper tests Debug issues

Why: Test resources (AshTypescript.Test.*) only compile in :test environment. Using dev environment causes "No domains found" errors.

Rule 2: Documentation-First Workflow

For any complex task (3+ steps):

  1. Check documentation index below to find relevant documentation
  2. Read recommended docs first to understand patterns
  3. Then implement following established patterns

Skip documentation → broken implementations, wasted time

Essential Workflows

Type Generation Workflow

mix test.codegen                      # Generate TypeScript types
cd test/ts && npm run compileGenerated # Validate compilation
mix test                              # Run Elixir tests

Domain Configuration

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

  typescript_rpc do
    resource MyApp.Todo do
      rpc_action :list_todos, :read
      rpc_action :list_todos_no_filter, :read, enable_filter?: false  # Disable client filtering
      rpc_action :list_todos_no_sort, :read, enable_sort?: false      # Disable client sorting
      # Load restrictions - control which relationships/calculations clients can load
      rpc_action :list_todos_limited, :read, allowed_loads: [:user]           # Whitelist
      rpc_action :list_todos_no_user, :read, denied_loads: [:user]            # Blacklist
      rpc_action :list_todos_nested, :read, allowed_loads: [comments: [:author]]  # Nested
    end
  end
end

Typed Controller Configuration

Three syntaxes are supported for defining routes:

defmodule MyApp.Session do
  use AshTypescript.TypedController

  typed_controller do
    module_name MyAppWeb.SessionController

    # Verb shortcut (preferred) — method is the entity name
    get :auth do
      run fn conn, _params -> render_inertia(conn, "Auth") end
    end

    # Positional method arg
    route :login, :post do
      run fn conn, _params -> Plug.Conn.send_resp(conn, 200, "OK") end
      argument :code, :string, allow_nil?: false
    end

    # Method defaults to :get when omitted
    route :home do
      run fn conn, _params -> Plug.Conn.send_resp(conn, 200, "Home") end
    end
  end
end

TypeScript Usage

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

const todos = await listTodos({
  fields: ["id", "title", {"user" => ["name"]}],
  headers: buildCSRFHeaders()
});

Phoenix Channel-based RPC Actions

Generated Channel Functions: AshTypescript generates channel functions with Channel suffix:

import { Channel } from "phoenix";
import { listTodos, listTodosChannel } from './ash_rpc';

// HTTP-based (always available)
const httpResult = await listTodos({
  fields: ["id", "title"],
  headers: buildCSRFHeaders()
});

// Channel-based (when enabled)
listTodosChannel({
  channel: myChannel,
  fields: ["id", "title"],
  resultHandler: (result) => {
    if (result.success) {
      console.log("Todos:", result.data);
    } else {
      console.error("Error:", result.errors);
    }
  },
  errorHandler: (error) => console.error("Channel error:", error),
  timeoutHandler: () => console.error("Timeout")
});

Typed Channel Event Subscriptions

For typed one-way push events from Ash PubSub publications.

Recommended: Use transform :some_calc to reference a resource calculation. Ash auto-derives the returns type when the calculation uses :auto, so AshTypescript gets the type information it needs without manual returns declarations.

# Resource with calculation transforms (recommended)
defmodule MyApp.Post do
  use Ash.Resource, notifiers: [Ash.Notifier.PubSub]

  pub_sub do
    module MyApp.Endpoint
    prefix "posts"

    publish :create, [:id], event: "post_created", public?: true, transform: :post_summary
    publish :update, [:id], event: "post_updated", public?: true, transform: :post_summary
  end

  calculations do
    calculate :post_summary, :auto, expr(%{id: id, title: title}) do
      public? true
    end
  end
  # ...
end

# Channel definition (unchanged — only references events)
defmodule MyApp.OrgChannel do
  use AshTypescript.TypedChannel

  typed_channel do
    topic "org:*"

    resource MyApp.Post do
      publish :post_created
      publish :post_updated
    end
  end
end

You can also use explicit returns with an anonymous function transform, but this requires manually keeping the type and transform in sync:

publish :create, [:id],
  event: "post_created",
  public?: true,
  returns: :map,
  constraints: [fields: [id: [type: :uuid], title: [type: :string]]],
  transform: fn notification -> %{id: notification.data.id, title: notification.data.title} end
import { createOrgChannel, onOrgChannelMessages, unsubscribeOrgChannel } from './ash_typed_channels';

const channel = createOrgChannel(socket, orgId);
const refs = onOrgChannelMessages(channel, {
  post_created: (payload) => console.log("New post:", payload),
  post_updated: (payload) => console.log("Updated:", payload),
});
// Cleanup: unsubscribeOrgChannel(channel, refs);

Runtime Introspection (Tidewave MCP)

Use these tools instead of shell commands for Elixir evaluation:

Tool Purpose
mcp__tidewave__project_eval Primary tool - evaluate Elixir in project context
mcp__tidewave__get_docs Get module/function documentation
mcp__tidewave__get_source_location Find source locations

Debug Examples:

# Debug field processing
mcp__tidewave__project_eval("""
fields = ["id", {"user" => ["name"]}]
AshTypescript.Rpc.RequestedFieldsProcessor.process(
  AshTypescript.Test.Todo, :read, fields
)
""")

Codebase Navigation

Key File Locations

Purpose Location
Core type generation (entry point) lib/ash_typescript/codegen.ex (delegator)
Type system introspection lib/ash_typescript/type_system/introspection.ex
Resource discovery lib/ash_typescript/codegen/type_discovery.ex
Type aliases generation lib/ash_typescript/codegen/type_aliases.ex
TypeScript type mapping lib/ash_typescript/codegen/type_mapper.ex
Resource schema generation lib/ash_typescript/codegen/resource_schemas.ex
Filter types generation lib/ash_typescript/codegen/filter_types.ex
Sort types generation lib/ash_typescript/codegen/sort_types.ex
Zod schema generation lib/ash_typescript/codegen/zod_schema_generator.ex
Valibot schema generation lib/ash_typescript/codegen/valibot_schema_generator.ex
Utility types generation lib/ash_typescript/codegen/utility_types.ex
Import path resolution lib/ash_typescript/codegen/import_resolver.ex
Shared types generator lib/ash_typescript/codegen/shared_types_generator.ex
Shared schema generator lib/ash_typescript/codegen/shared_schema_generator.ex
Schema formatter behaviour lib/ash_typescript/codegen/schema_formatter.ex
Schema core (shared logic) lib/ash_typescript/codegen/schema_core.ex
Multi-file orchestrator lib/ash_typescript/codegen/orchestrator.ex
RPC client generation lib/ash_typescript/rpc/codegen.ex
JSDoc comment generation lib/ash_typescript/rpc/codegen/function_generators/jsdoc_generator.ex
Manifest generation (Markdown) lib/ash_typescript/rpc/codegen/manifest_generator.ex
Manifest generation (JSON) lib/ash_typescript/rpc/codegen/json_manifest_generator.ex
Namespace resolution lib/ash_typescript/rpc/codegen/rpc_config_collector.ex
Pipeline orchestration lib/ash_typescript/rpc/pipeline.ex
Field processing (entry point) lib/ash_typescript/rpc/requested_fields_processor.ex (delegator)
Field atomization lib/ash_typescript/rpc/field_processing/atomizer.ex
Field selection (type-driven) lib/ash_typescript/rpc/field_processing/field_selector.ex
Field validation helpers lib/ash_typescript/rpc/field_processing/field_selector/validation.ex
Result extraction lib/ash_typescript/rpc/result_processor.ex
Unified value formatting lib/ash_typescript/rpc/value_formatter.ex
Input formatting lib/ash_typescript/rpc/input_formatter.ex (delegates to ValueFormatter)
Output formatting lib/ash_typescript/rpc/output_formatter.ex (delegates to ValueFormatter)
Resource verifiers lib/ash_typescript/resource/verifiers/
Typed controller DSL lib/ash_typescript/typed_controller/dsl.ex
Typed controller main lib/ash_typescript/typed_controller.ex
Controller request handler lib/ash_typescript/typed_controller/request_handler.ex
Controller codegen lib/ash_typescript/typed_controller/codegen.ex
Controller config discovery lib/ash_typescript/typed_controller/codegen/route_config_collector.ex
Router introspection lib/ash_typescript/typed_controller/codegen/router_introspector.ex
Route renderer lib/ash_typescript/typed_controller/codegen/route_renderer.ex
TypeScript static code lib/ash_typescript/typed_controller/codegen/typescript_static.ex
Controller verifier lib/ash_typescript/typed_controller/verifiers/verify_typed_controller.ex
Typed channel DSL lib/ash_typescript/typed_channel/dsl.ex
Typed channel main lib/ash_typescript/typed_channel.ex
Typed channel codegen lib/ash_typescript/typed_channel/codegen.ex
Typed channel verifier lib/ash_typescript/typed_channel/verifiers/verify_typed_channel.ex
Channel payload type mapper lib/ash_typescript/codegen/type_mapper.ex (map_channel_payload_type/2)
Test domain test/support/domain.ex
Primary test resource test/support/resources/todo.ex
TypeScript validation test/ts/shouldPass/ & test/ts/shouldFail/
TypeScript call extractor test/support/ts_action_call_extractor.ex
Codegen test helper test/support/codegen_test_helper.ex
Typed controller tests test/ash_typescript/typed_controller/
Typed channel tests test/ash_typescript/typed_channel/
Test typed controller test/support/resources/session.ex
Test router test/support/routes_test_router.ex
Generated route helpers test/ts/generated_routes.ts

Command Reference

Core Commands

mix test.codegen                      # Generate TypeScript (main command)
mix test.codegen --dry-run           # Preview output
mix test                             # Run all tests (do NOT prefix with MIX_ENV=test)
mix test test/ash_typescript/rpc/    # Test RPC functionality

TypeScript Validation (from test/ts/)

npm run compileGenerated             # Test generated types compile
npm run compileShouldPass            # Test valid patterns (type-level)
npm run compileShouldFail            # Test invalid patterns fail (type-level)
npm run testZod                      # Run generated Zod schemas against real data
npm run testValibot                  # Run generated Valibot schemas against real data

testZod / testValibot compile and execute the generated validation schemas against fixture inputs — they are the only path that exercises schema runtime behavior (e.g. catches a bug like an empty z.object({}) for a type that should validate { amount, currency }). Always run them after touching third_party_types, constraint generation, or any other validation codegen.

Quality Checks

mix format                           # Code formatting
mix credo --strict                   # Linting

Documentation Index

Core Files

File Purpose
troubleshooting.md Development troubleshooting
testing-and-validation.md Test organization and validation procedures
architecture-decisions.md Architecture decisions and context

Implementation Plans

File Purpose
run-ts.md Plan for TypeScript runtime validation - executing extracted TS calls via RPC

Implementation Documentation Guide

Consult these when modifying core systems:

Working On See Documentation Test Files
Type generation or custom types features/type-system.md test/ash_typescript/typescript_codegen_test.exs
Field/argument name mapping features/field-argument-name-mapping.md test/ash_typescript/rpc/rpc_field_argument_mapping_test.exs
Action metadata features/action-metadata.md test/ash_typescript/rpc/rpc_metadata_test.exs, test/ash_typescript/rpc/verify_metadata_field_names_test.exs
RPC pipeline or field processing features/rpc-pipeline.md test/ash_typescript/rpc/rpc_*_test.exs
Load restrictions features/rpc-pipeline.md (RPC Action Options) test/ash_typescript/rpc/load_restrictions_test.exs
Validation schemas (Zod & Valibot) features/validation-schemas.md test/ash_typescript/rpc/zod_constraints_test.exs, test/ash_typescript/rpc/valibot_constraints_test.exs
Embedded resources features/embedded-resources.md test/support/resources/embedded/
Union types features/union-systems-core.md test/ash_typescript/rpc/rpc_union_*_test.exs
Namespaces, JSDoc, Manifest, JSON Manifest features/developer-experience.md test/ash_typescript/rpc/namespace_test.exs, test/ash_typescript/rpc/json_manifest_generator_test.exs
Typed controllers & route helpers features/typed-controller.md test/ash_typescript/typed_controller/
Typed channel event subscriptions features/typed-channel.md test/ash_typescript/typed_channel/
Development patterns development-workflows.md N/A

Key Architecture Concepts

RPC Pipeline (Four Stages)

  1. parse_request - Validate input, create extraction templates
  2. execute_ash_action - Run Ash operations
  3. process_result - Apply field selection using templates
  4. format_output - Format for client consumption

Key Modules

  • RequestedFieldsProcessor (delegator) - Entry point for field processing
  • Field Processing Subsystem - 3 modules using type-driven dispatch:
    • Atomizer - Converts client field names to internal atoms
    • FieldSelector - Unified type-driven field selection (mirrors ValueFormatter pattern)
    • FieldSelector.Validation - Field validation helpers
  • ResultProcessor - Result extraction using templates
  • Pipeline - Four-stage orchestration
  • ErrorBuilder - Comprehensive error handling
  • ValueFormatter - Unified type-aware value formatting

Multi-File Codegen Architecture

  • Orchestrator (codegen/orchestrator.ex): Coordinates all file generation — types, Zod, Valibot, RPC, routes, typed channels, namespace re-exports
  • SchemaCore (codegen/schema_core.ex): Shared validation schema logic (topological sort, type mapping, field introspection) used by both Zod and Valibot via SchemaFormatter behaviour
  • ImportResolver (codegen/import_resolver.ex): Shared utility for import path resolution and namespace re-export generation (used by both RPC and controller codegen)
  • CodegenTestHelper (test/support/codegen_test_helper.ex): Test wrapper for orchestrator — use generate_all_content/0 for string assertions, generate_files/0 for file-level assertions

Type System Architecture

  • Type Introspection: Centralized in type_system/introspection.ex
  • Codegen Organization: Focused modules (type_discovery, type_aliases, type_mapper, resource_schemas, filter_types, sort_types)
  • ValueFormatter: Unified type-aware value formatting with recursive type detection

Type Inference Architecture

  • Unified Schema: Single ResourceSchema with __type metadata
  • Schema Keys: Direct classification via key lookup
  • Utility Types: UnionToIntersection, InferFieldValue, InferResult, SortString
  • Sort Types: Per-resource {Resource}SortField union types and {resource}SortFields const arrays generated by sort_types.ex. RPC functions use SortString<TodoSortField> to provide type-safe sort parameters with +/-/++/-- prefix support.

Core Patterns

  • Field Selection: Unified format supporting nested relationships and calculations
  • Embedded Resources: Full relationship-like architecture with calculation support
  • Union Field Selection: Selective member fetching with {content: ["field1", {"nested": ["field2"]}]}
  • Union Input Format: REQUIRED wrapped format {member_name: value} for all union inputs
  • Headers Support: All RPC functions accept optional headers for custom authentication
  • Type-Driven Dispatch: Both FieldSelector and ValueFormatter use {type, constraints} pattern for recursive processing

Common Errors

Error Cause Solution
"No domains found" Using dev environment Use mix test.codegen
"Module not loaded" Test resources not compiled Ensure MIX_ENV=test
"Invalid field names found" Field/arg with _1 or ? Use field_names or argument_names DSL options
"Invalid field names in map/keyword/tuple" Map constraint fields invalid Create Ash.Type.NewType with typescript_field_names/0 callback
"Invalid metadata field name" Metadata field with _1 or ? Use metadata_field_names DSL option in rpc_action
"Metadata field conflicts with resource field" Metadata field shadows resource field Rename metadata field or use different mapped name
TypeScript unknown types Schema key mismatch Check __type metadata generation
Field selection fails Invalid field format Use unified field format only
"Union input must be a map" Direct value for union input Wrap in map: {member_name: value}
"Union input map contains multiple member keys" Multiple union members in input Provide exactly one member key
"Union input map does not contain any valid member key" Invalid or missing member key Use valid member name from union definition
Test reads stale generated.ts Test uses File.read!("test/ts/generated.ts") Use AshTypescript.Test.CodegenTestHelper.generate_all_content/0 in setup_all
Controller 422 error Missing required argument or invalid type cast Check allow_nil? and argument types
Controller 500 error Handler doesn't return %Plug.Conn{} Return %Plug.Conn{} from handler
Routes not generated Missing config Set typed_controllers:, router:, and routes_output_file: in config
Multi-mount ambiguity Duplicate mounts without as: Add unique as: to each scope
"load_not_allowed" error Requested field not in allowed_loads Add field to allowed_loads or remove the option
"load_denied" error Requested field in denied_loads Remove field from denied_loads list
Path param without matching argument Router path has :param but no DSL argument Add argument :param, :string to the route definition
Invalid names for TypeScript (controller) Route/argument names with _1 or ? Rename to avoid patterns that produce awkward camelCase
allow_nil?: true on always-present path param Path param always provided by router Set allow_nil?: false on the argument
allow_nil?: false on sometimes-present path param Path param only at some mounts Set allow_nil?: true (default) on the argument
"No publication with event X found" Typed channel event doesn't match any publication Check event: option on the resource's pub_sub block
"Duplicate event names found in typed_channel" Same event name across resources in one channel Use unique event names per channel
"Payload type name conflict" Same event name across different channels maps to different TS types Rename events or ensure same returns type
Channel unknown payload type Publication missing returns type (no transform :calc or explicit returns) Use transform :some_calc with an :auto-typed calculation (recommended), or add explicit returns:
"not public?" error on RPC action Action has public? false Set public? true on the action or remove it from typescript_rpc
"not public?" error on read_action read_action has public? false Set public? true on the read action
"not public?" error on relationship read action Relationship destination's read action has public? false Set public? true on the destination's read action

RPC Resource Warnings

AshTypescript provides compile-time warnings for potential RPC configuration issues:

Warning: Resources with Extension but Not in RPC Config

Message: ⚠️ Found resources with AshTypescript.Resource extension but not listed in any domain's typescript_rpc block

Cause: Resource has AshTypescript.Resource extension but isn't configured in any typescript_rpc block

Solutions:

  • Add resource to a domain's typescript_rpc block, OR
  • Remove AshTypescript.Resource extension if not needed, OR
  • Disable warning: config :ash_typescript, warn_on_missing_rpc_config: false

Warning: Non-RPC Resources Referenced by RPC Resources

Message: ⚠️ Found non-RPC resources referenced by RPC resources

Cause: RPC resource references another resource (in attribute/calculation/aggregate) that isn't itself configured as RPC

Solutions:

  • Add referenced resource to typescript_rpc block if it should be accessible, OR
  • Leave as-is if resource is intentionally internal-only, OR
  • Disable warning: config :ash_typescript, warn_on_non_rpc_references: false

Note: Both warnings can be independently configured. See Configuration Reference for details.

Typed Controller Configuration

When typed_controllers, router, and routes_output_file are configured, mix ash_typescript.codegen generates typed TypeScript route helpers alongside RPC types.

Configuration:

config :ash_typescript,
  typed_controllers: [MyApp.Session],         # TypedController modules
  router: MyAppWeb.Router,                    # Phoenix router for path introspection
  routes_output_file: "assets/js/routes.ts",  # Output file for route helpers
  typed_controller_mode: :full,               # :full (default) or :paths_only
  typed_controller_base_path: ""              # Base URL prefix (string or {:runtime_expr, "..."})

Modes: :full generates path helpers + typed fetch functions for mutations. :paths_only generates only path helpers.

Base path: When set (e.g., "https://api.example.com" or {:runtime_expr, "AppConfig.getBasePath()"}), all generated route URLs are prefixed with _basePath. Uses the same {:runtime_expr, "..."} pattern as RPC endpoints.

Implementation: lib/ash_typescript.ex (typed_controllers/0, router/0, routes_output_file/0, typed_controller_mode/0, typed_controller_base_path/0) + lib/mix/tasks/ash_typescript.codegen.ex + lib/ash_typescript/typed_controller/

Typed Channel Configuration

When typed_channels and typed_channels_output_file are configured, mix ash_typescript.codegen generates typed TypeScript event subscription helpers alongside RPC types.

Configuration:

config :ash_typescript,
  typed_channels: [MyApp.OrgChannel],                       # TypedChannel modules
  typed_channels_output_file: "assets/js/ash_typed_channels.ts"  # Output file for channel functions

Channel types (branded types, payload aliases, event maps) are appended to ash_types.ts. Channel functions (factory, subscription helpers) go into the separate typed_channels_output_file.

Implementation: lib/ash_typescript.ex (typed_channels/0, typed_channels_output_file/0) + lib/ash_typescript/typed_channel/ + lib/ash_typescript/codegen/orchestrator.ex

JSON Manifest (Machine-Readable)

When json_manifest_file is configured, mix ash_typescript.codegen generates a machine-readable JSON manifest alongside the TypeScript output. This manifest contains structured metadata about every RPC action (function names, types, pagination, variants) and typed controller routes, enabling third-party packages to build typed wrappers (e.g., TanStack Query integrations) without coupling to ash_typescript internals.

Configuration:

config :ash_typescript,
  json_manifest_file: "assets/js/ash_rpc_manifest.json",
  json_manifest_filename_format: :relative  # :relative (default) | :absolute | :basename
  • json_manifest_filename_format controls the filename field in each files entry. importPath (no .ts, for TypeScript imports) is always relative to the manifest.
  • The manifest includes a "version": "1.0" field using semver for consumer compatibility detection.
  • The manifest is written independently of other file changes — it's always generated if the file doesn't exist or content changed.

Implementation: lib/ash_typescript/rpc.ex (json_manifest_file/0, json_manifest_filename_format/0) + lib/ash_typescript/rpc/codegen/json_manifest_generator.ex + lib/mix/tasks/ash_typescript.codegen.ex

Always Regenerate Mode

When config :ash_typescript, always_regenerate: true is set, mix ash_typescript.codegen --check writes files directly instead of comparing and raising Ash.Error.Framework.PendingCodegen. This is useful in development with AshPhoenix.Plug.CheckCodegenStatus to avoid the stale codegen error page and always regenerate files on every request.

Configuration: config :ash_typescript, always_regenerate: true (default: false) Implementation: lib/ash_typescript.ex (always_regenerate?/0) + lib/mix/tasks/ash_typescript.codegen.ex

Testing Workflow

mix test.codegen                     # Generate types
cd test/ts && npm run compileGenerated # Validate compilation
npm run compileShouldPass            # Test valid patterns (type-level)
npm run compileShouldFail            # Test invalid patterns fail (type-level)
npm run testZod                      # Run generated Zod schemas at runtime
npm run testValibot                  # Run generated Valibot schemas at runtime
mix test                             # Run Elixir tests (do NOT prefix with MIX_ENV=test)

Safety Checklist

  • ✅ Always validate TypeScript compilation after changes
  • ✅ Test both valid and invalid usage patterns
  • ✅ Use test environment for all AshTypescript commands
  • ✅ Write proper tests for debugging (no one-off shell commands)
  • ✅ Check architecture-decisions.md for context on current patterns

🎯 Primary Goal: Generate type-safe TypeScript clients from Ash resources with full feature support and optimal developer experience.