mirror of
https://github.com/ash-project/ash_events.git
synced 2024-09-19 12:52:48 +12:00
WIP
This commit is contained in:
commit
1fe4a43ff8
12 changed files with 376 additions and 0 deletions
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where third-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
ash_event_source-*.tar
|
||||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
20
README.md
Normal file
20
README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# AshEventSource
|
||||
|
||||
**TODO: Add description**
|
||||
|
||||
## Installation
|
||||
|
||||
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`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:ash_event_source, "~> 0.1.0"}
|
||||
]
|
||||
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>.
|
6
config/config.exs
Normal file
6
config/config.exs
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Config
|
||||
|
||||
if Mix.env() == :test do
|
||||
config :ash, :validate_api_resource_inclusion?, false
|
||||
config :ash, :validate_api_config_inclusion?, false
|
||||
end
|
20
lib/ash_event_source.ex
Normal file
20
lib/ash_event_source.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
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
|
3
lib/info.ex
Normal file
3
lib/info.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule AshEventSource.Info do
|
||||
use Spark.InfoGenerator, extension: AshEventSource, sections: [:event_source]
|
||||
end
|
44
lib/persist_event.ex
Normal file
44
lib/persist_event.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defmodule AshEventSource.PersistEvent do
|
||||
def run(input, opts, context) do
|
||||
event =
|
||||
opts[:event_resource]
|
||||
|> Ash.Changeset.for_create(:create, %{input: input.params, resource: input.resource, action: opts[:action]}, 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
|
||||
end
|
54
lib/transformers/rewrite_actions.ex
Normal file
54
lib/transformers/rewrite_actions.ex
Normal file
|
@ -0,0 +1,54 @@
|
|||
defmodule AshEventSource.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)
|
||||
|
||||
dsl
|
||||
|> Ash.Resource.Info.actions()
|
||||
|> Enum.filter(&(&1.type in [:create, :update, :destroy]))
|
||||
|> Enum.reduce({:ok, dsl}, fn action, {:ok, dsl} ->
|
||||
new_name = :"#{action.name}_implementation"
|
||||
copied_action = %{action | name: new_name}
|
||||
|
||||
arguments =
|
||||
action.accept
|
||||
|> Enum.map(fn attr_name ->
|
||||
attr = Ash.Resource.Info.attribute(dsl, attr_name)
|
||||
|
||||
%Ash.Resource.Actions.Argument{
|
||||
name: attr.name,
|
||||
allow_nil?: attr.allow_nil?,
|
||||
type: attr.type,
|
||||
constraints: attr.constraints,
|
||||
default: attr.default,
|
||||
description: attr.description,
|
||||
sensitive?: attr.sensitive?
|
||||
}
|
||||
end)
|
||||
|
||||
arguments =
|
||||
action.arguments
|
||||
|> Enum.concat(arguments)
|
||||
|> Enum.uniq_by(&(&1.name))
|
||||
|
||||
action = %Ash.Resource.Actions.Action{
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
returns: :atom,
|
||||
constraints: [one_of: [:success, :failure]],
|
||||
arguments: arguments,
|
||||
run: {AshEventSource.PersistEvent, [event_resource: event_resource, action: new_name]},
|
||||
transaction?: true
|
||||
}
|
||||
|
||||
{:ok,
|
||||
dsl
|
||||
|> Spark.Dsl.Transformer.replace_entity([:actions], action, &(&1.name == action.name))
|
||||
|> Spark.Dsl.Transformer.add_entity([:actions], copied_action)}
|
||||
end)
|
||||
end
|
||||
end
|
29
mix.exs
Normal file
29
mix.exs
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule AshEventSource.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :ash_event_source,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.15",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:ash, "~> 2.14"}
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
]
|
||||
end
|
||||
end
|
16
mix.lock
Normal file
16
mix.lock
Normal file
|
@ -0,0 +1,16 @@
|
|||
%{
|
||||
"ash": {:hex, :ash, "2.14.12", "bb2e3dbe82b49407f07854560ba3174a2c479508da8f33eb296764c6445477fb", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "799edb6326bd539ede0582b3e62f015a74062d3291e7b2c21ce4b0a2f9ebe88c"},
|
||||
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
|
||||
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||
"sourceror": {:hex, :sourceror, "0.13.0", "c6ecc96ee3ae0e042e9082a9550a1989ea40182492dc29024a8d9d2b136e5014", [:mix], [], "hexpm", "d0a819491061cd26bfa4450d1c84301a410c19c1782a6577ce15853fc0e7e4e1"},
|
||||
"spark": {:hex, :spark, "1.1.25", "9a4836520b71a485f5dedfdab1909071c01375b0409eb4cc2e9cfa8cc28c0398", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "506dfb6e8a851362ed5bb056644f9f852ec7513df77c65c09ef8f71518a91640"},
|
||||
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
||||
}
|
153
test/ash_event_source_test.exs
Normal file
153
test/ash_event_source_test.exs
Normal file
|
@ -0,0 +1,153 @@
|
|||
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
|
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
|
@ -0,0 +1 @@
|
|||
ExUnit.start()
|
Loading…
Reference in a new issue