improvement: initial feature set

This commit is contained in:
Zach Daniel 2023-04-27 22:07:05 -04:00
parent 316a77a7f2
commit cad7a65fae
13 changed files with 718 additions and 56 deletions

View file

@ -1,12 +1,9 @@
# AshOban # AshStateMachine
**TODO: Add description** An oban extension for `Ash.Resource`
## Installation ## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ash_oban` to your list of dependencies in `mix.exs`:
```elixir ```elixir
def deps do def deps do
[ [
@ -15,7 +12,6 @@ def deps do
end end
``` ```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) ## Get Started
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ash_oban>.
Check out the [getting started guide](/documentation/tutorials/get-started-with-ash-oban.md).

View file

@ -0,0 +1,62 @@
# Get Started With Ash Oban
AshOban will likely grow to provide many more oban-related features, but for now the primary focus is on "triggers".
A trigger describes an action that is run periodically.
This guide will need to be expanded, this is primarily a placeholder with an example.
## Setup
First, follow the oban setup guide.
If you are using Oban Pro, set the following configuration:
```elixir
config :ash_oban, :pro?, true
```
Next, allow AshOban to alter your configuration
```elixir
# in your application
{Oban, AshOban.config([YourApi, YourOtherApi], your_oban_config)}
```
## Usage
Finally, configure your triggers in your resources.
Add the `AshOban` extension:
```elixir
use Ash.Resource, extensions: [AshOban]
```
For example:
```elixir
oban do
triggers do
api YourApi
# add a triggere called `:process`
trigger :process do
# this trigger calls the `process` action
action :process
# for any record that has `processed != true`
where expr(processed != true)
# checking for matches every minute
scheduler_cron "* * * * *"
end
end
end
```
See the DSL documentation for more: `AshOban`
## Changing Triggers
To remove or disable triggers, *do not just remove them from your resource*. Due to the way that oban implements cron jobs, if you just remove them from your resource, the cron will attempt to continue scheduling jobs. Instead, set `paused true` or `delete true` on the trigger. See the oban docs for more: https://getoban.pro/docs/pro/0.14.1/Oban.Pro.Plugins.DynamicCron.html#module-using-and-configuring

View file

@ -1,26 +1,96 @@
defmodule AshOban do defmodule AshOban do
@moduledoc """
Documentation for `AshOban`.
"""
defmodule Trigger do defmodule Trigger do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
name: atom,
action: atom, action: atom,
where: Ash.Expr.t() read_action: atom,
queue: atom,
scheduler_cron: String.t(),
scheduler_queue: atom,
max_attempts: pos_integer(),
max_scheduler_attempts: pos_integer(),
where: Ash.Expr.t(),
scheduler: module,
worker: module,
__identifier__: atom
} }
defstruct [:action, :where] defstruct [
:name,
:action,
:read_action,
:queue,
:scheduler_cron,
:scheduler_queue,
:max_attempts,
:max_scheduler_attempts,
:where,
:scheduler,
:worker,
:__identifier__
]
end end
@trigger %Spark.Dsl.Entity{ @trigger %Spark.Dsl.Entity{
name: :trigger, name: :trigger,
target: Trigger, target: Trigger,
args: [:action], args: [:name],
identifier: :name,
imports: [Ash.Filter.TemplateHelpers], imports: [Ash.Filter.TemplateHelpers],
schema: [ schema: [
name: [
type: :atom,
doc: "A unique identifier for this trigger."
],
scheduler_queue: [
type: :atom,
doc:
"The queue to place the scheduler job in. The trigger name plus \"_scheduler\" is used by default."
],
scheduler_cron: [
type: :string,
default: "* * * * *",
doc:
"A crontab configuration for when the job should run. Defaults to once per minute (\"* * * * *\")."
],
queue: [
type: :atom,
doc: "The queue to place the worker job in. The trigger name is used by default."
],
max_scheduler_attempts: [
type: :pos_integer,
default: 1,
doc: "How many times to attempt scheduling of the triggered action."
],
max_attempts: [
type: :pos_integer,
default: 1,
doc: "How many times to attempt the job."
],
state: [
type: {:one_of, [:active, :paused, :deleted]},
doc: """
Describes the state of the cron job.
See the getting started guide for an explanation on the need for this field.
The most important thing is that you *do not remove a trigger from a resource*.
Oban's cron jobs are persisted, meaning you will get repeated errors whenever the cron
job tries to fire.
"""
],
read_action: [
type: :atom,
required: true,
doc: """
The read action to use when querying records. Defaults to the primary read.
This action *must* support keyset pagination.
"""
],
action: [ action: [
type: :atom, type: :atom,
doc: "The action to be triggered" doc:
"The action to be triggered. Defaults to the identifier of the resource plus the name of the trigger"
], ],
where: [ where: [
type: :any, type: :any,
@ -36,13 +106,124 @@ defmodule AshOban do
@oban %Spark.Dsl.Section{ @oban %Spark.Dsl.Section{
name: :oban, name: :oban,
schema: [
api: [
type: {:behaviour, Ash.Api},
doc: "The Api module to use when calling actions on this resource",
required: true
]
],
sections: [@triggers] sections: [@triggers]
} }
@sections [@oban]
@moduledoc """
Dsl documentation for AshOban
<!--- ash-hq-hide-start --> <!--- -->
## DSL Documentation
### Index
#{Spark.Dsl.Extension.doc_index(@sections)}
### Docs
#{Spark.Dsl.Extension.doc(@sections)}
<!--- ash-hq-hide-stop --> <!--- -->
"""
use Spark.Dsl.Extension, use Spark.Dsl.Extension,
sections: [@oban], sections: [@oban],
verifiers: [ imports: [AshOban.Changes.BuiltinChanges],
# This is a bit dumb, a verifier probably shouldn't have side effects transformers: [
AshOban.Verifiers.DefineSchedulers AshOban.Transformers.SetDefaults,
AshOban.Transformers.DefineSchedulers
] ]
def config(apis, base \\ %{}) do
pro? = AshOban.Info.pro?()
cron_plugin =
if pro? do
Oban.Pro.Plugins.DynamicCron
else
Oban.Pro.Plugins.Cron
end
if pro? && base[:engine] != Oban.Pro.Queue.SmartEngine do
raise """
Expected oban engine to be Oban.Pro.Queue.SmartEngine, but got #{inspect(base[:engine])}.
This expectation is because you've set `config :ash_oban, pro?: true`.
"""
end
require_cron!(base, cron_plugin)
apis
|> Enum.flat_map(fn api ->
api
|> Ash.Api.Info.resources()
end)
|> Enum.uniq()
|> Enum.flat_map(fn resource ->
resource
|> AshOban.Info.oban_triggers()
|> Enum.map(&{resource, &1})
end)
|> Enum.reduce(base, fn {resource, trigger}, config ->
require_queues!(config, resource, trigger)
add_job(config, cron_plugin, resource, trigger)
end)
end
defp add_job(config, cron_plugin, _resource, trigger) do
Keyword.update!(config, :plugins, fn plugins ->
Keyword.update!(plugins, cron_plugin, fn plugins ->
opts =
case trigger.state do
:paused ->
[paused: true]
:deleted ->
[delete: true]
_ ->
[]
end
if(trigger.state = cron = {trigger.scheduler_cron, trigger.scheduler, []})
Keyword.update(plugins, :crontab, [cron], &[cron | &1])
end)
end)
end
defp require_queues!(config, resource, trigger) do
unless config[:queues][trigger.queue] do
raise """
Must configure the queue `:#{trigger.queue}`, requied for
the trigger `:#{trigger.name}` on #{inspect(resource)}
"""
end
unless config[:queues][trigger.scheduler_queue] do
raise """
Must configure the queue `:#{trigger.queue}`, required for
the scheduler of the trigger `:#{trigger.name}` on #{inspect(resource)}
"""
end
end
defp require_cron!(config, name) do
unless config[:plugins][name] do
raise """
Must configure cron plugin #{name}.
See oban's documentation for more. AshOban will
add cron jobs to the configuration, but will not
add the basic configuration for you.
"""
end
end
end end

View file

@ -0,0 +1,7 @@
defmodule AshOban.Changes.BuiltinChanges do
@moduledoc "Builtin changes for `AshOban`"
def run_oban_trigger(trigger_name) do
{AshOban.Changes.RunObanTrigger, trigger: trigger_name}
end
end

View file

@ -0,0 +1,20 @@
defmodule AshOban.Changes.RunObanTrigger do
use Ash.Resource.Change
def change(changeset, opts, _context) do
trigger = AshOban.Info.oban_trigger(changeset.resource, opts[:trigger])
primary_key = Ash.Resource.Info.primary_key(changeset.resource)
if !trigger do
raise "No such trigger #{opts[:trigger]} for resource #{inspect(changeset.resource)}"
end
Ash.Changeset.after_action(changeset, fn _changeset, result ->
%{primary_key: Map.take(result, primary_key)}
|> trigger.worker.new()
|> Oban.insert!()
{:ok, result}
end)
end
end

View file

@ -0,0 +1,28 @@
defmodule AshOban.Checks.AshObanInteraction do
@moduledoc """
This check is true if the context `private.ash_oban?` is set to true.
This context will only ever be set in code that is called internally by
`ash_oban`, allowing you to create a bypass in your policies on your
user/user_token resources.
```elixir
policies do
bypass AshObanInteraction do
authorize_if always()
end
end
```
"""
use Ash.Policy.SimpleCheck
@impl Ash.Policy.Check
def describe(_) do
"AshOban is performing this interaction"
end
@impl Ash.Policy.SimpleCheck
def match?(_, %{query: %{context: %{private: %{ash_oban?: true}}}}, _), do: true
def match?(_, %{changeset: %{context: %{private: %{ash_oban?: true}}}}, _), do: true
def match?(_, _, _), do: false
end

View file

@ -1,3 +1,13 @@
defmodule AshOban.Info do defmodule AshOban.Info do
use Spark.InfoGenerator, extension: AshOban, sections: [:oban] use Spark.InfoGenerator, extension: AshOban, sections: [:oban]
@pro Application.compile_env(:ash_oban, :pro?) || false
def pro?, do: @pro
@spec oban_trigger(Ash.Resource.t() | Spark.Dsl.t(), atom) :: nil | AshOban.Trigger.t()
def oban_trigger(resource, name) do
resource
|> oban_triggers()
|> Enum.find(&(&1.name == name))
end
end end

View file

@ -0,0 +1,289 @@
defmodule AshOban.Transformers.DefineSchedulers do
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer
def after?(AshOban.Transformers.SetDefaults), do: true
def after?(_), do: false
def transform(dsl) do
module = Transformer.get_persisted(dsl, :module)
dsl
|> AshOban.Info.oban_triggers()
|> Enum.reduce(dsl, fn trigger, dsl ->
scheduler_module_name = module_name(module, trigger, "Scheduler")
worker_module_name = module_name(module, trigger, "Worker")
dsl
|> Transformer.replace_entity([:oban, :triggers], %{
trigger
| scheduler: scheduler_module_name,
worker: worker_module_name
})
|> Transformer.async_compile(fn ->
define_worker(module, worker_module_name, trigger, dsl)
end)
|> Transformer.async_compile(fn ->
define_scheduler(module, scheduler_module_name, worker_module_name, trigger, dsl)
end)
end)
|> then(&{:ok, &1})
end
defp module_name(module, trigger, type) do
module
|> List.wrap()
|> Enum.concat(["AshOban", type])
|> Enum.concat([Macro.camelize(to_string(trigger.name))])
|> Module.concat()
end
defp define_scheduler(resource, scheduler_module_name, worker_module_name, trigger, dsl) do
api = AshOban.Info.oban_api!(dsl)
primary_key = Ash.Resource.Info.primary_key(dsl)
pro? = AshOban.Info.pro?()
filter =
if not is_nil(trigger.where) do
quote do
def filter(query) do
Ash.Query.do_filter(query, unquote(Macro.escape(trigger.where)))
end
end
end
stream =
if is_nil(trigger.where) do
quote do
def stream(resource) do
resource
|> Ash.Query.set_context(%{private: %{ash_oban?: true}})
|> Ash.Query.select(unquote(primary_key))
|> Ash.Query.for_read(unquote(trigger.read_action))
|> unquote(api).stream!()
end
end
else
quote do
def stream(resource) do
resource
|> Ash.Query.set_context(%{private: %{ash_oban?: true}})
|> Ash.Query.select(unquote(primary_key))
|> Ash.Query.for_read(unquote(trigger.read_action))
|> filter()
|> unquote(api).stream!()
end
end
end
insert =
if pro? do
quote do
def insert(stream) do
stream
|> Stream.chunk_every(100)
|> Stream.each(&Oban.insert_all!/1)
|> Stream.run()
end
end
else
quote do
def insert(stream) do
stream
|> Stream.each(&Oban.insert!())
|> Stream.run()
end
end
end
worker =
if pro? do
Oban.Pro.Worker
else
Oban.Worker
end
function_name =
if pro? do
:process
else
:perform
end
quoted =
quote do
use unquote(worker),
queue: unquote(trigger.scheduler_queue),
unique: [
period: :infinity,
states: [
:available,
:retryable,
:scheduled
]
],
max_attempts: unquote(trigger.max_scheduler_attempts)
require Logger
@impl unquote(worker)
if unquote(trigger.state != :active) do
def unquote(function_name)(%Oban.Job{}) do
{:discard, unquote(trigger.state)}
end
else
def unquote(function_name)(%Oban.Job{}) do
unquote(resource)
|> stream()
|> Stream.map(fn record ->
unquote(worker_module_name).new(%{
primary_key: Map.take(record, unquote(primary_key))
})
end)
|> insert()
end
end
unquote(stream)
unquote(filter)
unquote(insert)
end
Module.create(scheduler_module_name, quoted, Macro.Env.location(__ENV__))
end
defp define_worker(resource, worker_module_name, trigger, dsl) do
api = AshOban.Info.oban_api!(dsl)
pro? = AshOban.Info.pro?()
worker =
if pro? do
Oban.Pro.Worker
else
Oban.Worker
end
function_name =
if pro? do
:process
else
:perform
end
query =
if is_nil(trigger.where) do
quote do
def query do
unquote(resource)
end
end
else
quote do
def query do
Ash.Query.do_filter(unquote(resource), unquote(Macro.escape(trigger.where)))
end
end
end
Module.create(
worker_module_name,
quote do
use unquote(worker),
max_attempts: 3,
queue: unquote(trigger.queue),
unique: [
period: :infinity,
states: [
:available,
:retryable,
:scheduled
]
]
require Logger
@impl unquote(worker)
if unquote(trigger.state != :active) do
def unquote(function_name)(_) do
{:discard, unquote(trigger.state)}
end
else
def unquote(function_name)(%Oban.Job{args: %{"primary_key" => primary_key}}) do
if Ash.DataLayer.data_layer_can?(unquote(resource), :transact) do
Ash.DataLayer.transaction(
unquote(resource),
fn ->
opts = [
action: unquote(trigger.read_action),
context: %{private: %{ash_oban?: true}}
]
opts =
if Ash.DataLayer.data_layer_can?(unquote(resource), {:lock, :for_update}) do
opts
else
Keyword.put(opts, :lock, :for_update)
end
query()
|> unquote(api).read_one(primary_key, opts)
|> case do
{:ok, nil} ->
{:discard, :trigger_no_longer_applies}
{:ok, record} ->
nil
end
|> Ash.Changeset.new()
|> Ash.Changeset.set_context(%{private: %{ash_oban?: true}})
|> Ash.Changeset.for_update(unquote(trigger.action), %{})
|> unquote(api).update!()
end,
nil,
%{
type: :ash_oban_trigger,
metadata: %{
resource: unquote(resource),
trigger: unquote(trigger.name)
}
}
)
|> case do
{:ok, {:discard, reason}} ->
{:discard, reason}
{:ok, _} ->
:ok
other ->
other
end
else
opts = [
action: unquote(trigger.read_action),
context: %{private: %{ash_oban?: true}}
]
query()
|> unquote(api).read_one(primary_key, opts)
|> case do
{:ok, nil} ->
{:discard, :trigger_no_longer_applies}
{:ok, record} ->
nil
end
|> Ash.Changeset.new()
|> Ash.Changeset.set_context(%{private: %{ash_oban?: true}})
|> Ash.Changeset.for_update(unquote(trigger.action), %{})
|> unquote(api).update()
end
end
end
unquote(query)
end,
Macro.Env.location(__ENV__)
)
end
end

View file

@ -0,0 +1,62 @@
defmodule AshOban.Transformers.SetDefaults do
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer
def after?(AshOban.Transformers.DefineSchedulers), do: false
def after?(_), do: true
def before?(AshOban.Transformers.DefineSchedulers), do: true
def before?(_), do: false
def transform(dsl) do
module = Transformer.get_persisted(dsl, :module)
{:ok,
dsl
|> Transformer.get_entities([:oban, :triggers])
|> Enum.reduce(dsl, fn trigger, dsl ->
read_action =
case trigger.read_action do
nil ->
Ash.Resource.Info.primary_action(dsl, :read) ||
raise Spark.Error.DslError,
path: [
:oban,
:triggers,
trigger.name,
:read_action
],
module: module,
message: """
No read action was configured for this trigger, and no primary read action exists
"""
read_action ->
Ash.Resource.Info.action(dsl, read_action)
end
unless read_action.pagination && read_action.pagination.keyset? do
raise Spark.Error.DslError,
path: [:oban, :triggers, trigger.name, :read_action],
module: module,
message: """
The read action `:#{read_action.name}` must support keyset pagination in order to be
used by an AshOban trigger.
"""
end
queue = trigger.queue || default_queue_name(dsl, trigger)
Transformer.replace_entity(dsl, [:oban, :triggers], %{
trigger
| read_action: read_action.name,
queue: queue,
scheduler_queue: trigger.scheduler_queue || :"#{queue}_scheduler",
action: trigger.action || trigger.name
})
end)}
end
defp default_queue_name(dsl, trigger) do
:"#{Ash.Resource.Info.short_name(dsl)}_#{trigger.name}"
end
end

View file

@ -1,14 +0,0 @@
defmodule AshOban.Verifiers.DefineSchedulers do
use Spark.Dsl.Verifier
def verify(dsl_state) do
# TODO
# dsl_state
# |> AshOban.Info.oban_triggers()
# |> Enum.each(fn trigger ->
# IO.inspect(trigger)
# end)
:ok
end
end

26
mix.exs
View file

@ -97,29 +97,13 @@ defmodule AshOban.MixProject do
module: AshOban, module: AshOban,
name: "AshOban", name: "AshOban",
target: "Ash.Resource", target: "Ash.Resource",
type: "StateMachine Resource" type: "AshOban Resource"
},
%{
module: AshGraphql.Api,
name: "AshGraphql Api",
target: "Ash.Api",
type: "GraphQL Api"
} }
] ]
], ],
extras: extras(), extras: extras(),
groups_for_extras: groups_for_extras(), groups_for_extras: groups_for_extras(),
groups_for_modules: [ groups_for_modules: [
AshGraphql: [
AshGraphql
],
Introspection: [
AshGraphql.Resource.Info,
AshGraphql.Api.Info
],
Miscellaneous: [
AshGraphql.Resource.Helpers
],
Internals: ~r/.*/ Internals: ~r/.*/
] ]
] ]
@ -135,8 +119,12 @@ defmodule AshOban.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:ash, "~> 2.7"}, {:ash, github: "ash-project/ash"},
{:spark, ">= 1.0.9"} {:spark, ">= 1.1.3"},
{:oban, "~> 2.15"},
{:oban_pro, "~> 0.14", repo: "oban"},
{:oban_web, "~> 2.9", repo: "oban"}
# {:oban, path: "../oban"}
# {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
] ]

View file

@ -1,16 +1,33 @@
%{ %{
"ash": {:hex, :ash, "2.7.1", "e8915f4ebb4dbf8e8f689020430adf30f68ac2f4bd2613e416e6f69fd8f4dc69", [: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.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", "9b6e708fac305147f99d292823782bf8f7d2897efae7fe4fdf688e3c7cad777f"}, "ash": {:git, "https://github.com/ash-project/ash.git", "fbc341b3a00d1d0798a8966d831443f88f412390", []},
"castore": {:hex, :castore, "1.0.1", "240b9edb4e9e94f8f56ab39d8d2d0a57f49e46c56aced8f873892df8ff64ff5a", [:mix], [], "hexpm", "b4951de93c224d44fac71614beabd88b71932d0b1dea80d2f80fb9044e01bbb3"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [: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", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"}, "ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [: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", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"},
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
"elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"}, "elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"},
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"}, "ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"oban": {:hex, :oban, "2.15.0", "27b9c2845cdff30c98c8060b11a64318e79bbc1bd32b8dc95fa59a1580a8d90c", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "22e181c540335d1dd5c995be00435927075519207d62b3de32477d95dbf9dfd3"},
"oban_pro": {:hex, :oban_pro, "0.14.1", "729394796e1e79633bb1083633cb9db5c5ddf2a13ab1cbc081593978348f285f", [:mix], [{:ecto_sql, "~> 3.8", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.13", [hex: :libgraph, repo: "hexpm", optional: true]}, {:oban, "~> 2.15.0", [hex: :oban, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}], "oban", "ef5e448ec98faa7f07924548904718d3b7a5c1e3eddd9a5a0c11bf90b9af75c2"},
"oban_web": {:hex, :oban_web, "2.9.6", "40a77a41f13e9a1a712101eec0972616dc2a82ae5befe4020592e885977d41da", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.11", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.17.4 and < 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "oban", "be7917e91555050ff82f29921168ff7f4dd5d80692daaa987d78286de4b8b297"},
"phoenix": {:hex, :phoenix, "1.7.2", "c375ffb482beb4e3d20894f84dd7920442884f5f5b70b9f4528cbe0cedefec63", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1ebca94b32b4d0e097ab2444a9742ed8ff3361acad17365e4e6b2e79b4792159"},
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"sourceror": {:hex, :sourceror, "0.12.2", "2ae55efd149193572e0eb723df7c7a1bda9ab33c43373c82642931dbb2f4e428", [:mix], [], "hexpm", "7ad74ade6fb079c71f29fae10c34bcf2323542d8c51ee1bcd77a546cfa89d59c"}, "sourceror": {:hex, :sourceror, "0.12.2", "2ae55efd149193572e0eb723df7c7a1bda9ab33c43373c82642931dbb2f4e428", [:mix], [], "hexpm", "7ad74ade6fb079c71f29fae10c34bcf2323542d8c51ee1bcd77a546cfa89d59c"},
"spark": {:hex, :spark, "1.0.9", "6a98f4d0183a30a241de06d27604d5baa48a99b0fe99afc8a5bd82d4067a501e", [: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", "4e64cc73fb43d7d64e985e80b5e6300e94114782c59464c4476d272b0bdb9df1"}, "spark": {:hex, :spark, "1.1.3", "5577146f14f7af85c9c56da283d633377428377952cdafae862e55aabdbd5133", [: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", "4512bd2b9b9b20cc47450c83420d7e7e223fd08c4ac75ce0846d0f522d82e50b"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"},
"websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"},
} }

View file

@ -2,6 +2,14 @@ defmodule AshObanTest do
use ExUnit.Case use ExUnit.Case
doctest AshOban doctest AshOban
defmodule Api do
use Ash.Api
resources do
allow_unregistered? true
end
end
defmodule Triggered do defmodule Triggered do
use Ash.Resource, use Ash.Resource,
data_layer: Ash.DataLayer.Ets, data_layer: Ash.DataLayer.Ets,
@ -9,14 +17,22 @@ defmodule AshObanTest do
oban do oban do
triggers do triggers do
api Api
trigger :process do trigger :process do
action :process
where expr(processed != true) where expr(processed != true)
end end
end end
end end
actions do actions do
defaults [:read, :create] defaults [:create]
read :read do
primary? true
pagination keyset?: true
end
update :process do update :process do
change set_attribute(:processed, true) change set_attribute(:processed, true)