AttrEngine.Relation behaviour (attr_engine v0.5.0)

Copy Markdown View Source

Relation resolution runtime for reference attributes.

An attribute with data_config.type = "relation" stores a ref (an opaque identifier) in the ASD, while the actual value lives in an external target store. This module orchestrates the read and write paths through pluggable source adapters.

relation_config shape

The relation_config map on an attribute's data_config drives resolution:

%{
  "source" => "vault",       # adapter name (looked up in registry)
  "ref_only" => true,        # if true, value never materializes into ASD
  ...                        # adapter-specific keys passed through
}

Any additional keys in relation_config are passed to the adapter as context, allowing adapter-specific configuration (table names, column mappings, scopes, etc.) without the engine knowing about them.

Registering source adapters

Adapters implement the AttrEngine.Relation behaviour and are registered via application config:

config :attr_engine, relation_sources: %{
  "vault" => MyApp.Adapters.Vault,
  "users" => MyApp.Adapters.Users
}

Behaviour

Adapters must implement three callbacks:

defmodule MyApp.Adapters.Vault do
  @behaviour AttrEngine.Relation

  @impl true
  def resolve(ref, config) do
    # ref: the stored reference (e.g. secret name)
    # config: the full relation_config map
    case Vault.get(ref) do
      {:ok, value} -> {:ok, value}
      :error -> {:error, :not_found}
    end
  end

  @impl true
  def store(value, config) do
    # Persist value to the target store, return a ref
    name = config["ref_key"] || generate_name()
    :ok = Vault.put(name, value)
    {:ok, name}
  end

  @impl true
  def delete(ref, config) do
    Vault.delete(ref)
    :ok
  end
end

Composition with transforms

For sensitive relations (secrets, credentials), pair ref_only: true with data_config["transform"] = "redact_secret". The transform masks the ref in rendered/exposed output while the resolution system handles the actual value routing.

Summary

Callbacks

Delete the target value for the given ref.

Resolve a ref to its target value.

Store a value in the target, returning the ref to persist in the ASD.

Functions

Deletes target values for all relation attributes in the given data.

Looks up the adapter module for a relation_config's declared source.

Processes incoming data for write, routing values to their target stores.

Resolves all relation attributes in a data map.

Callbacks

delete(ref, config)

@callback delete(ref :: any(), config :: map()) :: :ok | {:error, any()}

Delete the target value for the given ref.

resolve(ref, config)

@callback resolve(ref :: any(), config :: map()) :: {:ok, any()} | {:error, any()}

Resolve a ref to its target value.

store(value, config)

@callback store(value :: any(), config :: map()) :: {:ok, ref :: any()} | {:error, any()}

Store a value in the target, returning the ref to persist in the ASD.

Functions

cleanup(attrs_meta, data)

@spec cleanup([map()], map()) :: :ok | {:error, [{String.t(), any()}]}

Deletes target values for all relation attributes in the given data.

Useful when deleting an ASD, to clean up external references.

lookup_adapter(arg1)

@spec lookup_adapter(map()) :: module() | nil

Looks up the adapter module for a relation_config's declared source.

process_writes(attrs_meta, incoming_data)

@spec process_writes([map()], map()) :: {:ok, map()} | {:error, [{String.t(), any()}]}

Processes incoming data for write, routing values to their target stores.

For each relation attribute marked ref_only: true, extracts the raw value from incoming_data, passes it to the adapter's store/2, and replaces it with the returned ref in the output data.

Non-relation attributes and relations without ref_only are passed through unchanged.

Returns {:ok, processed_data} or {:error, errors}.

resolve_relations(attrs_meta, data, opts \\ [])

@spec resolve_relations([map()], map(), keyword()) ::
  {:ok, map()} | {:error, [{String.t(), any()}]}

Resolves all relation attributes in a data map.

For each attribute in attrs_meta where type == :relation (or data_config["type"] == "relation"), looks up the source adapter and resolves the stored ref to its target value.

Options:

  • :skip_ref_only — when true (default), skips resolution for ref_only: true relations (the ref stays as-is, to be masked by the transform layer). Set to false to force resolution of all.

Returns {:ok, resolved_data} or {:error, errors} where errors is a list of {handle, reason} tuples.