defmodule ExState.Ecto.Query do @moduledoc """ `ExState.Ecto.Query` provides functions for querying workflow state in the database through Ecto. """ import Ecto.Query @doc """ where_state/2 takes a subject query and state and filters based on workflows that are in the exact state that is passed. Nested states can be passed as a list of states and will be converted to a valid state identifier in the query. Pass the state as the keyword list `not: state` in order to query the inverse. Examples: investment #=> %Investment{workflow: %Workflow{state: "subscribing.confirming_options"}} Investment |> where_state("subscribing.confirming_options") |> Repo.all() #=> [investment] Investment |> where_state([:subscribing, :confirming_options]) |> Repo.all() #=> [investment] Investment |> where_state(["subscribing", "confirming_options"]) |> Repo.all() #=> [investment] Investment |> where_state(:subscribing) |> Repo.all() #=> [] Investment |> where_state(not: [:subscribing, :confirming_suitability]) |> Repo.all() #=> [investment] Investment |> where_state(not: [:subscribing, :confirming_options]) |> Repo.all() #=> [] """ def where_state(subject_query, not: state) do subject_query |> join_workflow_maybe() |> where([workflow: workflow], workflow.state != ^to_state_id(state)) end def where_state(subject_query, state) do subject_query |> join_workflow_maybe() |> where([workflow: workflow], workflow.state == ^to_state_id(state)) end @doc """ where_state_in/2 takes a subject query and a list of states and filters based on workflows that are in one of the exact states that are passed. Nested states can be passed as a list of states and will be converted to a valid state identifier in the query. Pass the state as the keyword list `not: state` in order to query the inverse. Examples: investment1 #=> %Investment{workflow: %Workflow{state: "subscribing.confirming_options"}} investment2 #=> %Investment{workflow: %Workflow{state: "executed"}} Investment |> where_state_in([ [:subscribing, :confirming_options], :executed ]) |> Repo.all() #=> [investment1, investment2] Investment |> where_state_in(["subscribing.confirming_options"]) |> Repo.all() #=> [investment1] Investment |> where_state_in([:subscribing]) |> Repo.all() #=> [] Investment |> where_state_in(not: [[:subscribing, :confirming_options]]) |> Repo.all() #=> [investment2] Investment |> where_state_in(not: [:subscribing]) |> Repo.all() #=> [investment1, investment2] """ def where_state_in(subject_query, not: states) do subject_query |> join_workflow_maybe() |> where([workflow: workflow], workflow.state not in ^Enum.map(states, &to_state_id/1)) end def where_state_in(subject_query, states) do subject_query |> join_workflow_maybe() |> where([workflow: workflow], workflow.state in ^Enum.map(states, &to_state_id/1)) end @doc """ where_any_state/2 takes a subject query and a state and filters based on workflows that are equal to or in a child state of the given state. Nested states can be passed as a list of states and will be converted to a valid state identifier in the query. Examples: investment #=> %Investment{workflow: %Workflow{state: "subscribing.confirming_options"}} Investment |> where_any_state(:subscribing) |> Repo.all() #=> [investment] Investment |> where_any_state("subscribing") |> Repo.all() #=> [investment] Investment |> where_any_state([:subscribing, :confirming_options]) |> Repo.all() #=> [investment] Investment |> where_any_state(:resubmitting) |> Repo.all() #=> [] """ def where_any_state(subject_query, state) do state_id = to_state_id(state) subject_query |> join_workflow_maybe() |> where( [workflow: workflow], workflow.state == ^state_id or ilike(workflow.state, ^"#{state_id}.%") ) end def where_step_complete(q, s) when is_atom(s), do: where_step_complete(q, Atom.to_string(s)) def where_step_complete(subject_query, step_name) do subject_query |> join_workflow_maybe() |> join_workflow_steps_maybe() |> where([workflow_step: step], step.name == ^step_name and step.complete?) end def to_state_id(states) when is_list(states) do Enum.map(states, &to_state_id/1) |> Enum.join(".") end def to_state_id(state) when is_atom(state), do: Atom.to_string(state) def to_state_id(state) when is_bitstring(state), do: state def to_state_list(state) when is_bitstring(state) do String.split(state, ".") |> Enum.map(&String.to_atom/1) end def to_step_name(step) when is_atom(step), do: Atom.to_string(step) def to_step_name(step) when is_bitstring(step), do: step defp join_workflow_maybe(subject_query) do if has_named_binding?(subject_query, :workflow) do subject_query else join(subject_query, :inner, [sub], w in assoc(sub, :workflow), as: :workflow) end end defp join_workflow_steps_maybe(subject_query) do if has_named_binding?(subject_query, :workflow_step) do subject_query else join(subject_query, :inner, [workflow: w], s in assoc(w, :steps), as: :workflow_step) end end end