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
endComposition 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 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.
Useful when deleting an ASD, to clean up external references.
Looks up the adapter module for a relation_config's declared source.
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}.
@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 forref_only: truerelations (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.