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 :create action and re-run idempotently through upserts
  • csv_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 ...
end

And 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 id

Declares 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

NameTypeDefaultDocs
idatomStable identifier for this CSV type (e.g. :contacts).

Options

NameTypeDefaultDocs
labelString.tHuman-readable name shown in the upload UI dropdown.
headerskeywordColumn-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_actionatomName 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} | nilOptional {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 id

Declares 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

NameTypeDefaultDocs
idatomStable identifier for this export type (e.g. :contacts).

Options

NameTypeDefaultDocs
labelString.tHuman-readable name shown in the download UI.
read_actionatomName 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.
columnslist({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