mirror of
https://github.com/ash-project/ash_events.git
synced 2024-09-19 12:52:48 +12:00
chore: add some docs and rename things
This commit is contained in:
parent
1fe4a43ff8
commit
fbaed4493e
10 changed files with 242 additions and 231 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -20,7 +20,7 @@ erl_crash.dump
|
|||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
ash_event_source-*.tar
|
||||
ash_events-*.tar
|
||||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
||||
|
|
32
README.md
32
README.md
|
@ -1,20 +1,28 @@
|
|||
# AshEventSource
|
||||
# AshEvents
|
||||
|
||||
**TODO: Add description**
|
||||
A fledgeling Ash extension for transforming Ash resources to use an event oriented architecture. This is still an experiment, it only supports create actions (but could be made to support updates and destroys without much trouble).
|
||||
|
||||
## Installation
|
||||
Caveats:
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `ash_event_source` to your list of dependencies in `mix.exs`:
|
||||
* We aren't storing the actor in any way. We would need to store actor information to perform authorization.
|
||||
* the event_driven version is not really distinguishable from `ash_paper_trail` except that it has fewer features and writes to a single events resource.
|
||||
* if you want to use this, you would have to do work to get it ready for your cases.
|
||||
|
||||
Configure the style using the `style` option, for example:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:ash_event_source, "~> 0.1.0"}
|
||||
]
|
||||
events do
|
||||
style :event_sourced
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at <https://hexdocs.pm/ash_event_source>.
|
||||
The default is `:event_driven`, and generally means there is nothing to do
|
||||
except integrate this extension.
|
||||
|
||||
## Event Driven
|
||||
|
||||
Event driven architecture is relatively simple. We encode the inputs to the action into an event and commit that event alongside the performance of the action, transactionally.
|
||||
|
||||
## Event Sourced
|
||||
|
||||
With event sourced, things change quite a bit. Instead of storing the event and performing the action, we only store the event, and it is *your responsibility* to take each event, perform the action it refers to and mark it as processed, in whatever way you see fit.
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
defmodule AshEventSource do
|
||||
@event_source %Spark.Dsl.Section{
|
||||
name: :event_source,
|
||||
schema: [
|
||||
event_resource: [
|
||||
type: {:behaviour, Ash.Resource},
|
||||
required: true,
|
||||
doc: "The resource to use to store events."
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@sections [@event_source]
|
||||
|
||||
@transformers [
|
||||
AshEventSource.Transformers.RewriteActions
|
||||
]
|
||||
|
||||
use Spark.Dsl.Extension, transformers: @transformers, sections: @sections
|
||||
end
|
25
lib/ash_events.ex
Normal file
25
lib/ash_events.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule AshEvents do
|
||||
@events %Spark.Dsl.Section{
|
||||
name: :events,
|
||||
schema: [
|
||||
event_resource: [
|
||||
type: {:behaviour, Ash.Resource},
|
||||
required: true,
|
||||
doc: "The resource to use to store events."
|
||||
],
|
||||
style: [
|
||||
type: {:one_of, [:event_sourced, :event_driven]},
|
||||
default: :event_driven,
|
||||
doc: "Which style of event architecture you want. See the getting started guide for more."
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@sections [@events]
|
||||
|
||||
@transformers [
|
||||
AshEvents.Transformers.RewriteActions
|
||||
]
|
||||
|
||||
use Spark.Dsl.Extension, transformers: @transformers, sections: @sections
|
||||
end
|
|
@ -1,3 +1,3 @@
|
|||
defmodule AshEventSource.Info do
|
||||
use Spark.InfoGenerator, extension: AshEventSource, sections: [:event_source]
|
||||
defmodule AshEvents.Info do
|
||||
use Spark.InfoGenerator, extension: AshEvents, sections: [:events]
|
||||
end
|
||||
|
|
|
@ -1,44 +1,48 @@
|
|||
defmodule AshEventSource.PersistEvent do
|
||||
defmodule AshEvents.PersistEvent do
|
||||
def run(input, opts, context) do
|
||||
action = Ash.Resource.Info.action(input.resource, opts[:action])
|
||||
|
||||
if action.type == :create do
|
||||
case AshEvents.Info.events_style!(input.resource) do
|
||||
:event_sourced ->
|
||||
opts[:event_resource]
|
||||
|> Ash.Changeset.for_create(
|
||||
:create,
|
||||
%{
|
||||
input: input.params,
|
||||
resource: input.resource,
|
||||
action: opts[:action],
|
||||
processed: true
|
||||
},
|
||||
actor: Ash.context_to_opts(context)
|
||||
)
|
||||
|> input.api.create!()
|
||||
|
||||
{:ok, :success}
|
||||
|
||||
:event_driven ->
|
||||
event =
|
||||
opts[:event_resource]
|
||||
|> Ash.Changeset.for_create(:create, %{input: input.params, resource: input.resource, action: opts[:action]}, actor: Ash.context_to_opts(context))
|
||||
|> Ash.Changeset.for_create(
|
||||
:create,
|
||||
%{
|
||||
input: input.params,
|
||||
resource: input.resource,
|
||||
action: opts[:action],
|
||||
processed: true
|
||||
},
|
||||
actor: Ash.context_to_opts(context)
|
||||
)
|
||||
|> input.api.create!()
|
||||
|
||||
event.resource
|
||||
|> Ash.Changeset.for_create(event.action, event.input)
|
||||
|> input.api.create!()
|
||||
|
||||
event =
|
||||
event
|
||||
|> Ash.Changeset.for_update(:process)
|
||||
|> input.api.update!()
|
||||
|
||||
|
||||
{:ok, :success}
|
||||
end
|
||||
|
||||
# use Ash.Resource.ManualCreate
|
||||
# use Ash.Resource.ManualUpdate
|
||||
|
||||
# @impl Ash.Resource.ManualCreate
|
||||
# def create(changeset, opts, context) do
|
||||
# input = changeset.input
|
||||
|
||||
|
||||
# # This is a hack, need to figure this out
|
||||
# Ash.Changeset.apply_attributes(changeset, force?: true)
|
||||
# end
|
||||
|
||||
# @impl Ash.Resource.ManualUpdate
|
||||
# def update(changeset, opts, context) do
|
||||
# input = changeset.input
|
||||
|
||||
# opts[:resource]
|
||||
# |> Ash.Changeset.for_create(:create, %{input: input}, actor: Ash.context_to_opts(context))
|
||||
# |> changeset.api.create!()
|
||||
|
||||
# # This is a hack, need to figure this out
|
||||
# Ash.Changeset.apply_attributes(changeset, force?: true)
|
||||
# end
|
||||
else
|
||||
raise "Only create actions are currently supported"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
defmodule AshEventSource.Transformers.RewriteActions do
|
||||
defmodule AshEvents.Transformers.RewriteActions do
|
||||
@moduledoc "Rewrite each create, update and destroy action into event sourced actions"
|
||||
use Spark.Dsl.Transformer
|
||||
|
||||
def after?(_), do: true
|
||||
|
||||
def transform(dsl) do
|
||||
event_resource = AshEventSource.Info.event_source_event_resource!(dsl)
|
||||
event_resource = AshEvents.Info.events_event_resource!(dsl)
|
||||
|
||||
dsl
|
||||
|> Ash.Resource.Info.actions()
|
||||
|
@ -33,7 +33,7 @@ defmodule AshEventSource.Transformers.RewriteActions do
|
|||
arguments =
|
||||
action.arguments
|
||||
|> Enum.concat(arguments)
|
||||
|> Enum.uniq_by(&(&1.name))
|
||||
|> Enum.uniq_by(& &1.name)
|
||||
|
||||
action = %Ash.Resource.Actions.Action{
|
||||
name: action.name,
|
||||
|
@ -41,7 +41,7 @@ defmodule AshEventSource.Transformers.RewriteActions do
|
|||
returns: :atom,
|
||||
constraints: [one_of: [:success, :failure]],
|
||||
arguments: arguments,
|
||||
run: {AshEventSource.PersistEvent, [event_resource: event_resource, action: new_name]},
|
||||
run: {AshEvents.PersistEvent, [event_resource: event_resource, action: new_name]},
|
||||
transaction?: true
|
||||
}
|
||||
|
||||
|
|
4
mix.exs
4
mix.exs
|
@ -1,9 +1,9 @@
|
|||
defmodule AshEventSource.MixProject do
|
||||
defmodule AshEvents.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :ash_event_source,
|
||||
app: :ash_events,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.15",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
defmodule AshEventSourceTest do
|
||||
use ExUnit.Case
|
||||
|
||||
require Ash.Query
|
||||
|
||||
defmodule Api do
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
allow_unregistered?(true)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Event do
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets
|
||||
|
||||
actions do
|
||||
defaults [:read, :create, :update, :destroy]
|
||||
|
||||
update :process do
|
||||
change set_attribute(:processed, true)
|
||||
end
|
||||
end
|
||||
|
||||
ets do
|
||||
private?(true)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key(:id)
|
||||
attribute :input, :map, allow_nil?: false
|
||||
attribute :resource, :atom, allow_nil?: false
|
||||
attribute :action, :atom, allow_nil?: false
|
||||
attribute :processed, :boolean, allow_nil?: false, default: false
|
||||
attribute :timestamp, :utc_datetime_usec do
|
||||
default &DateTime.utc_now/0
|
||||
allow_nil? false
|
||||
writable? false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Profile do
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets,
|
||||
extensions: [AshEventSource]
|
||||
|
||||
event_source do
|
||||
event_resource(Event)
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
end
|
||||
|
||||
ets do
|
||||
private?(true)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
attribute :bio, :string
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :user, AshEventSourceTest.User do
|
||||
allow_nil? false
|
||||
attribute_writable? true
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for Api
|
||||
define :create
|
||||
end
|
||||
end
|
||||
|
||||
defmodule User do
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets,
|
||||
extensions: [AshEventSource]
|
||||
|
||||
event_source do
|
||||
event_resource(Event)
|
||||
end
|
||||
|
||||
ets do
|
||||
private?(true)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key(:id)
|
||||
attribute(:username, :string, allow_nil?: false)
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults([:read, :update, :destroy])
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
change after_action(fn _changeset, result ->
|
||||
Profile.create!(%{user_id: result.id, bio: "Initial Bio!"})
|
||||
|
||||
{:ok, result}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for(Api)
|
||||
define(:create)
|
||||
end
|
||||
|
||||
relationships do
|
||||
has_one :profile, Profile
|
||||
end
|
||||
end
|
||||
|
||||
test "test" do
|
||||
User.create!(%{username: "fred"})
|
||||
# IO.inspect("FIRST EVENT CREATED")
|
||||
Api.read!(Event) |> IO.inspect()
|
||||
Api.read!(User) |> IO.inspect()
|
||||
Api.read!(Profile) |> IO.inspect()
|
||||
# process()
|
||||
# IO.inspect("FIRST EVENT PROCESSED")
|
||||
# Api.read!(Event) |> IO.inspect()
|
||||
# Api.read!(User) |> IO.inspect()
|
||||
# Api.read!(Profile) |> IO.inspect()
|
||||
# process()
|
||||
# IO.inspect("SECOND EVENT PROCESSED")
|
||||
# Api.read!(Event) |> IO.inspect()
|
||||
# Api.read!(User) |> IO.inspect()
|
||||
# Api.read!(Profile) |> IO.inspect()
|
||||
end
|
||||
|
||||
defp process() do
|
||||
Event
|
||||
|> Ash.Query.sort(timestamp: :asc, id: :desc)
|
||||
|> Ash.Query.filter(processed == false)
|
||||
|> Api.read!()
|
||||
|> Enum.each(fn event ->
|
||||
event.resource
|
||||
|> Ash.Changeset.for_create(event.action, event.input)
|
||||
|> Api.create!()
|
||||
|
||||
event
|
||||
|> Ash.Changeset.for_update(:process)
|
||||
|> Api.update!()
|
||||
end)
|
||||
end
|
||||
end
|
147
test/ash_events_test.exs
Normal file
147
test/ash_events_test.exs
Normal file
|
@ -0,0 +1,147 @@
|
|||
defmodule AshEventsTest do
|
||||
use ExUnit.Case
|
||||
|
||||
require Ash.Query
|
||||
|
||||
defmodule Api do
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
allow_unregistered?(true)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Event do
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets
|
||||
|
||||
actions do
|
||||
defaults([:read, :create, :update, :destroy])
|
||||
|
||||
update :process do
|
||||
change(set_attribute(:processed, true))
|
||||
end
|
||||
end
|
||||
|
||||
ets do
|
||||
private?(true)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key(:id)
|
||||
attribute(:input, :map, allow_nil?: false)
|
||||
attribute(:resource, :atom, allow_nil?: false)
|
||||
attribute(:action, :atom, allow_nil?: false)
|
||||
attribute(:processed, :boolean, allow_nil?: false, default: false)
|
||||
|
||||
attribute :timestamp, :utc_datetime_usec do
|
||||
default(&DateTime.utc_now/0)
|
||||
allow_nil?(false)
|
||||
writable?(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Profile do
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets,
|
||||
extensions: [AshEvents]
|
||||
|
||||
events do
|
||||
event_resource(Event)
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults([:create, :read, :update, :destroy])
|
||||
end
|
||||
|
||||
ets do
|
||||
private?(true)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key(:id)
|
||||
attribute(:bio, :string)
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :user, AshEventsTest.User do
|
||||
allow_nil?(false)
|
||||
attribute_writable?(true)
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for(Api)
|
||||
define(:create)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule User do
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets,
|
||||
extensions: [AshEvents]
|
||||
|
||||
events do
|
||||
event_resource(Event)
|
||||
end
|
||||
|
||||
ets do
|
||||
private?(true)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key(:id)
|
||||
attribute(:username, :string, allow_nil?: false)
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults([:read, :update, :destroy])
|
||||
|
||||
create :create do
|
||||
primary?(true)
|
||||
|
||||
change(
|
||||
after_action(fn _changeset, result ->
|
||||
Profile.create!(%{user_id: result.id, bio: "Initial Bio!"})
|
||||
|
||||
{:ok, result}
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define_for(Api)
|
||||
define(:create)
|
||||
end
|
||||
|
||||
relationships do
|
||||
has_one(:profile, Profile)
|
||||
end
|
||||
end
|
||||
|
||||
test "test" do
|
||||
assert [] = Api.read!(Event)
|
||||
User.create!(%{username: "fred"})
|
||||
assert [_, _] = Api.read!(Event)
|
||||
assert [_] = Api.read!(User)
|
||||
assert [_] = Api.read!(Profile)
|
||||
end
|
||||
|
||||
# defp process() do
|
||||
# Event
|
||||
# |> Ash.Query.sort(timestamp: :asc, id: :desc)
|
||||
# |> Ash.Query.filter(processed == false)
|
||||
# |> Api.read!()
|
||||
# |> Enum.each(fn event ->
|
||||
# event.resource
|
||||
# |> Ash.Changeset.for_create(event.action, event.input)
|
||||
# |> Api.create!()
|
||||
|
||||
# event
|
||||
# |> Ash.Changeset.for_update(:process)
|
||||
# |> Api.update!()
|
||||
# end)
|
||||
# end
|
||||
end
|
Loading…
Reference in a new issue