An Ash extension for moving CSV data in and out of Ash resources. Resources that use it expose:
csv_imports do ... end— named import types, each dispatched to a per-row Ash:createaction and re-run idempotently through upsertscsv_exports do ... end— named export types, each streaming a read action's records out to CSV
Setup
Name the OTP app that owns your CSV resources, then register the domains holding them under that app:
config :ash_csv_interchange, otp_app: :my_app
config :my_app, AshCsvInterchange, domains: [MyApp.Domain]otp_app is required. It names the app whose config holds the domain
registry. Without it, type discovery raises rather than silently finding
nothing.
Add the extension to each resource:
defmodule MyApp.Contact do
use Ash.Resource,
extensions: [AshCsvInterchange]
csv_imports do
csv_import :contacts do
label "Contacts Export"
headers required: ["external_id", "first_name"], optional: []
upsert_action :import_from_csv
end
end
# ... actions, attributes, identities ...
endAnd import a CSV:
AshCsvInterchange.import_csv(:contacts, csv_binary, mode: :commit)csv_imports
Declares this resource as a CSV import target.
The entities here drive AshCsvInterchange.list_import_types/0,
AshCsvInterchange.fetch_import_type/1, and the per-row dispatch in
AshCsvInterchange.import_csv/3.
Nested DSLs
Examples
csv_imports do
csv_import :contacts do
label "Contacts Export"
headers required: ["external_id", "first_name"], optional: []
upsert_action :import_contacts
end
csv_import :archived_contacts do
label "Archived Contacts"
headers required: ["external_id", "archived_at"], optional: []
upsert_action :import_archived_contacts
end
end
csv_imports.csv_import
csv_import idDeclares a single CSV import type for this resource. Multiple
entities can be declared inside one csv_imports block to support
different CSV shapes against the same resource (e.g. an active
contacts export vs. an archived one).
Examples
csv_import :contacts do
label "Contacts Export"
headers required: ["external_id", "first_name", {"date_of_birth", :dob}],
optional: ["middle_name"]
upsert_action :import_from_csv
end
Arguments
| Name | Type | Default | Docs |
|---|---|---|---|
id | atom | Stable identifier for this CSV type (e.g. :contacts). |
Options
| Name | Type | Default | Docs |
|---|---|---|---|
label | String.t | Human-readable name shown in the upload UI dropdown. | |
headers | keyword | Column-to-input mapping for this CSV type. Keys: required: (columns the file must contain), optional: (columns it may contain) and ignored: (columns to accept and discard). A required/optional entry is a column name, passed to the action under the same key, or a {column, input_key} tuple. Columns match after whitespace trim and ASCII case-fold. | |
upsert_action | atom | Name of a :create action declared on this resource. The action must declare upsert? true and upsert_identity :name so re-runs upsert rather than insert. The orchestrator calls Ash.Changeset.for_create/4 with this action name per row. | |
import_source | {atom, any} | nil | Optional {attribute, value} tuple. When set, every committed record gets attribute force-set to value — for example, {:source, :csv} writes :csv to the :source attribute on each row, marking it as CSV-imported. The attribute must already exist on the resource. nil (the default) skips stamping. |
Introspection
Target: AshCsvInterchange.Import.Type
csv_exports
Declares this resource as a CSV export source.
The entities here drive AshCsvInterchange.list_export_types/0,
AshCsvInterchange.fetch_export_type/1, and the streaming dispatch in
AshCsvInterchange.stream_export/2 / AshCsvInterchange.export_csv/2.
Nested DSLs
Examples
csv_exports do
csv_export :contacts do
label "Active Contacts"
read_action :for_csv_export
columns [
{"external_id", :external_id},
{"full_name", :full_name}
]
end
end
csv_exports.csv_export
csv_export idDeclares one CSV export type. Repeat the entity inside csv_exports
to expose multiple shapes for the same resource.
Examples
csv_export :contacts do
label "Active Contacts"
read_action :for_csv_export
columns [
{"external_id", :external_id},
{"full_name", :full_name},
{"date_of_birth", :dob, format: &Date.to_iso8601/1}
]
end
Arguments
| Name | Type | Default | Docs |
|---|---|---|---|
id | atom | Stable identifier for this export type (e.g. :contacts). |
Options
| Name | Type | Default | Docs |
|---|---|---|---|
label | String.t | Human-readable name shown in the download UI. | |
read_action | atom | Name of a :read action declared on this resource. The orchestrator calls Ash.read(resource, action: name, actor: actor). The action owns filter/sort/load/auth. | |
columns | list({String.t, atom} | {String.t, atom, keyword}) | Ordered list of {header, field} or {header, field, opts} tuples. opts may set format: to a 1-arity function capture or a {module, function, extra_args} MFA returning String.t() or iodata. |
Introspection
Target: AshCsvInterchange.Export.Type