documentation/topics/composite-primary-keys.md

# Composite Primary Keys

When working with resources that have composite primary keys (multiple fields that together form the unique identifier), AshJsonApi provides special support for encoding and decoding these keys in URLs.

## Defining Composite Primary Keys

First, define your composite primary key in the JSON API configuration:

```elixir
defmodule MyApp.Bio do
  use Ash.Resource,
    extensions: [AshJsonApi.Resource]

  attributes do
    attribute :author_id, :uuid, primary_key?: true, public?: true
    attribute :category, :string, primary_key?: true, public?: true
    attribute :content, :string, public?: true
  end

  json_api do
    type "bio"
    
    primary_key do
      keys [:author_id, :category]
      delimiter "|"  # Use a delimiter that won't conflict with your data
    end
  end
end
```

### Important Considerations for Delimiters

When choosing a delimiter, ensure it won't appear in your actual data:

- **UUIDs contain dashes (`-`)** - Don't use `-` as a delimiter if any of your composite key fields are UUIDs
- **Safe alternatives**: `|`, `~`, `::`, or other characters unlikely to appear in your data
- **Default delimiter**: If not specified, AshJsonApi uses `-` as the default delimiter

## Enabling Composite Key Parsing in Routes

To enable automatic parsing of composite primary keys in URL paths, you must opt-in by specifying the `path_param_is_composite_key` option on your routes:

```elixir
json_api do
  type "bio"
  
  primary_key do
    keys [:author_id, :category]
    delimiter "|"
  end
  
  routes do
    base "/bios"
    
    # Enable composite key parsing for the :id parameter
    get :read, path_param_is_composite_key: :id
    patch :update, path_param_is_composite_key: :id
    delete :destroy, path_param_is_composite_key: :id
    
    # Other routes that don't need composite key parsing
    index :read
    post :create
  end
end
```

## How It Works

With the above configuration:

1. **URL Format**: `/bios/550e8400-e29b-41d4-a716-446655440000|sports`
2. **Parsing**: The `:id` parameter `550e8400-e29b-41d4-a716-446655440000|sports` gets split by the `|` delimiter
3. **Mapping**: The parts are mapped to the primary key fields in order:
   - `author_id` = `550e8400-e29b-41d4-a716-446655440000`
   - `category` = `sports`
4. **Filtering**: The query is filtered to find the record with both `author_id` and `category` matching

## Example Usage

```elixir
# Creating a bio
POST /bios
{
  "data": {
    "type": "bio",
    "attributes": {
      "author_id": "550e8400-e29b-41d4-a716-446655440000",
      "category": "sports",
      "content": "Author bio for sports category"
    }
  }
}

# Retrieving the bio using composite key
GET /bios/550e8400-e29b-41d4-a716-446655440000|sports

# Updating the bio
PATCH /bios/550e8400-e29b-41d4-a716-446655440000|sports
{
  "data": {
    "type": "bio", 
    "attributes": {
      "content": "Updated bio content"
    }
  }
}

# Deleting the bio
DELETE /bios/550e8400-e29b-41d4-a716-446655440000|sports
```

## Without Opt-In Parsing

If you don't specify `path_param_is_composite_key` on a route, the path parameter will be treated as a regular single value, even if your resource has composite primary keys defined. This ensures backward compatibility and prevents unexpected behavior.

## Error Handling

If the composite key format is invalid (wrong number of parts after splitting), AshJsonApi will return a 404 Not Found error with appropriate JSON:API error formatting.