AshDynamo.EmbeddedType (ash_dynamo v0.6.1)

Copy Markdown View Source

Generates a custom Ash.Type for an embedded Ash resource stored in DynamoDB.

DynamoDB stores embedded resources as plain maps with string keys. When Ash reads them back, it tries to "load through" embedded attributes expecting Ash structs with __metadata__. This causes a KeyError on :__metadata__.

This macro generates a type that handles the DynamoDB map ↔ struct conversion and sets cast_in_query?: false to prevent Ash from loading through.

Usage

First, define your embedded resource:

defmodule MyApp.Tag do
  use Ash.Resource, data_layer: :embedded

  attributes do
    attribute :name, :string, allow_nil?: false, public?: true
    attribute :color, :string, allow_nil?: false, public?: true
  end
end

Then create a type module for it:

defmodule MyApp.Types.Tag do
  use AshDynamo.EmbeddedType, resource: MyApp.Tag
end

Finally, use the type in your DynamoDB-backed resource:

defmodule MyApp.Post do
  use Ash.Resource,
    data_layer: AshDynamo.DataLayer,
    domain: MyApp.Domain

  attributes do
    attribute :tags, {:array, MyApp.Types.Tag}, allow_nil?: false
  end
end

Why is this needed?

When using {:array, MyApp.Tag} directly (the embedded resource), Ash attempts to "load through" the embedded attributes after reading from the data layer. This works with data layers like AshPostgres that return properly cast embedded structs, but DynamoDB returns plain maps with string keys, causing:

** (KeyError) key :__metadata__ not found in: %{"name" => "elixir", "color" => "purple"}

The custom type solves this by:

  1. Casting string-keyed maps from DynamoDB into the embedded resource struct (cast_stored/2)
  2. Casting input maps (atom or string keys) into the struct (cast_input/2)
  3. Dumping the struct back to a plain map for DynamoDB storage (dump_to_native/2)
  4. Returning cast_in_query?: false to prevent Ash from loading through the type

Type Validation

Both cast_input/2 and cast_stored/2 validate each field through their respective Ash.Type casting functions. If any field fails validation, the entire cast returns :error. This ensures invalid data is caught on both writes and reads — a corrupted DynamoDB record will raise an error immediately rather than propagating bad data.

ExAws.Dynamo.Encodable

The macro automatically implements the ExAws.Dynamo.Encodable protocol for the embedded resource, so ExAws can encode it when writing to DynamoDB. The implementation converts the struct to a plain map containing only the resource's attribute fields.

If you prefer to control the encoding yourself (e.g. to exclude specific fields), you can add @derive {ExAws.Dynamo.Encodable, only: [:name, :color]} to your embedded resource module before use AshDynamo.EmbeddedType is called. The macro will skip the automatic implementation if one already exists.