Ramal.Resource (Ramal v0.2.0)

Copy Markdown View Source

The Spark extension that makes an Ash resource branchable.

Add it to a resource and every read action of that resource is patched with the current branch's changes:

defmodule MyApp.Farm.Herd do
  use Ash.Resource,
    domain: MyApp.Farm,
    data_layer: AshPostgres.DataLayer,
    extensions: [Ramal.Resource]

  # ...
end

A resource opts out with a ramal section. A table that must never be branched, such as the one Ramal stores branches in, should do so:

ramal do
  branchable? false
end

The extension only installs hooks. They do nothing unless the calling process is inside Ramal.run/2, so a resource behaves exactly as before outside a branch.

What the transformer installs, confirmed against the locked Ash 3.37 source:

  • a global preparation, appended with Ash.Resource.Builder.add_preparation/2, which Ash applies to every read action of the resource;
  • a global change, Ramal.Capture, appended with Ash.Resource.Builder.add_change/3 for create, update and destroy actions;
  • require_atomic? false on update and destroy actions, so that Ash can fall back from its atomic path (which goes straight to the data layer) to the one Ramal.Capture hooks into when a branch is current. See docs/adr/0005-write-capture.md.

Example

iex> Ramal.Resource.branchable?(Ramal.Test.Farm.Herd)
true

Summary

Functions

The attribute values of an Ash record as a plain map, leaving out relationships, calculations, aggregates and any attribute that was not loaded (a read with select leaves the others as %Ash.NotLoaded{}).

Whether resource uses Ramal.Resource and has not opted out with branchable? false.

The resources of domain that are branchable.

Refuses a tenant: {:error, %Ramal.Error{reason: :multitenancy_unsupported}} when resource uses multitenancy and tenant is set, :ok otherwise.

The fingerprint of a record: :erlang.phash2/1 of its attributes, or nil when some attribute is not loaded, because a hash of part of a row cannot be compared with a hash of the whole row.

The overlay key of record: the value of its primary key attribute, or a tuple of the values for a composite key.

Whether notifications for writes to resource made inside a branch are sent when Ramal.run/2 returns.

Functions

attributes(record)

@spec attributes(struct()) :: %{optional(atom()) => term()}

The attribute values of an Ash record as a plain map, leaving out relationships, calculations, aggregates and any attribute that was not loaded (a read with select leaves the others as %Ash.NotLoaded{}).

Example

iex> herd = struct(Ramal.Test.Farm.Herd, id: "r7", name: "R7", head: 30)
iex> Ramal.Resource.attributes(herd) |> Map.take([:id, :name, :head])
%{id: "r7", name: "R7", head: 30}

branchable?(resource)

@spec branchable?(module()) :: boolean()

Whether resource uses Ramal.Resource and has not opted out with branchable? false.

Example

iex> Ramal.Resource.branchable?(Ramal.Test.Farm.Herd)
true

branchable_resources(domain)

@spec branchable_resources(module()) :: [module()]

The resources of domain that are branchable.

Example

iex> Ramal.Resource.branchable_resources(Ramal.Test.Farm) |> Enum.sort()
[
  Ramal.Test.Farm.Herd,
  Ramal.Test.Farm.HerdVet,
  Ramal.Test.Farm.Move,
  Ramal.Test.Farm.Paddock,
  Ramal.Test.Farm.Vet
]

ensure_untenanted(resource, tenant)

@spec ensure_untenanted(module(), term()) :: :ok | {:error, Ramal.Error.t()}

Refuses a tenant: {:error, %Ramal.Error{reason: :multitenancy_unsupported}} when resource uses multitenancy and tenant is set, :ok otherwise.

The overlay is keyed by {resource, primary key}, so two tenants' rows would share one entry, and the staleness read of Ramal.merge/2 has no tenant to read with (docs/adr/0009-multitenancy-refused.md). A resource that is multitenant but global? true can still be used without a tenant.

Example

iex> Ramal.Resource.ensure_untenanted(Ramal.Test.Farm.Herd, "acme")
:ok
iex> {:error, error} = Ramal.Resource.ensure_untenanted(Ramal.Test.Tenanted.Task, "acme")
iex> error.reason
:multitenancy_unsupported

fingerprint(record)

@spec fingerprint(struct()) :: non_neg_integer() | nil

The fingerprint of a record: :erlang.phash2/1 of its attributes, or nil when some attribute is not loaded, because a hash of part of a row cannot be compared with a hash of the whole row.

Example

iex> a = struct(Ramal.Test.Farm.Herd, id: "r7", name: "R7", head: 30)
iex> b = struct(Ramal.Test.Farm.Herd, id: "r7", name: "R7", head: 31)
iex> Ramal.Resource.fingerprint(a) == Ramal.Resource.fingerprint(a)
true
iex> Ramal.Resource.fingerprint(a) == Ramal.Resource.fingerprint(b)
false

key(resource, record)

@spec key(module(), map()) :: term()

The overlay key of record: the value of its primary key attribute, or a tuple of the values for a composite key.

Example

iex> Ramal.Resource.key(Ramal.Test.Farm.Herd, %{id: "r7"})
"r7"

notify_in_branch?(resource)

@spec notify_in_branch?(module()) :: boolean()

Whether notifications for writes to resource made inside a branch are sent when Ramal.run/2 returns.

Example

iex> Ramal.Resource.notify_in_branch?(Ramal.Test.Farm.Herd)
false

ramal(body)

(macro)