diff --git a/.formatter.exs b/.formatter.exs index c486770e..ec4b981b 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -24,7 +24,6 @@ locals_without_parens = [ col: :*, colgroup: :*, command: :*, - commands: :*, component: :*, data: :*, datalist: :*, @@ -42,14 +41,11 @@ locals_without_parens = [ endpoint: :*, enum: :*, event: :*, - events: :*, - feature: :*, features: :*, field: :*, fieldset: :*, figcaption: :*, figure: :*, - flows: :*, footer: :*, form: :*, h1: :*, @@ -58,7 +54,6 @@ locals_without_parens = [ h4: :*, h5: :*, h6: :*, - handlers: :*, has_many: :*, head: :*, header: :*, @@ -79,12 +74,10 @@ locals_without_parens = [ main: :*, map: :*, mapping: :*, - mappings: :*, mark: :*, meta: :*, meter: :*, model: :*, - models: :*, mount: :*, namespace: :*, namespaces: :*, @@ -106,7 +99,6 @@ locals_without_parens = [ progress: :*, publish: :*, q: :*, - queries: :*, query: :*, route: :*, routes: :*, @@ -115,7 +107,6 @@ locals_without_parens = [ ruby: :*, s: :*, samp: :*, - scopes: :*, script: :*, section: :*, select: :*, @@ -127,7 +118,6 @@ locals_without_parens = [ strong: :*, style: :*, sub: :*, - subscriptions: :*, summary: :*, sup: :*, svg: :*, @@ -151,7 +141,7 @@ locals_without_parens = [ var: :*, video: :*, view: :*, - wbr: :*, + wbr: :* ] [ diff --git a/.tool-versions b/.tool-versions index 9471d325..78a112a3 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ -elixir 1.17.2-otp-27 -erlang 27.0 +elixir 1.18.3-otp-27 +erlang 27.3 +nodejs 24.2.0 diff --git a/lib/mix/tasks/gen_migrations.ex b/lib/mix/tasks/gen_migrations.ex index 5296ef38..696609ab 100644 --- a/lib/mix/tasks/gen_migrations.ex +++ b/lib/mix/tasks/gen_migrations.ex @@ -20,13 +20,12 @@ defmodule Mix.Tasks.Momo.Gen.Migrations do repo = Keyword.fetch!(config, :repo) app = Keyword.fetch!(config, :app) migrations_dir = Mix.EctoSQL.source_repo_priv(repo) - features = app.features() dir = Path.join([migrations_dir, "migrations"]) dir |> Migrations.existing() - |> Migrations.missing(features) + |> Migrations.missing(app) |> case do %{steps: []} -> Mix.shell().info("No migrations to write") diff --git a/lib/momo.ex b/lib/momo.ex index 9c1b4b89..9eac3f4b 100644 --- a/lib/momo.ex +++ b/lib/momo.ex @@ -6,7 +6,6 @@ defmodule Momo do """ @dsls [ - Momo.Feature.Dsl, Momo.Command.Dsl, Momo.Model.Dsl, Momo.Query.Dsl, diff --git a/lib/momo/app.ex b/lib/momo/app.ex index c9cf4aab..5ff746d4 100644 --- a/lib/momo/app.ex +++ b/lib/momo/app.ex @@ -7,8 +7,158 @@ defmodule Momo.App do generators: [ Momo.App.Generator.Migrate, Momo.App.Generator.Application, - Momo.App.Generator.Roles + Momo.App.Generator.Roles, + Momo.App.Generator.Graph, + Momo.App.Generator.Map ] - defstruct [:name, :roles, :module, :repos, :endpoints, :features] + defstruct [ + :name, + :roles, + :module, + :repos, + :endpoints, + models: [], + commands: [], + queries: [], + events: [], + flows: [], + subscriptions: [], + mappings: [], + values: [], + scopes: [] + ] + + require Logger + + import Momo.Maps + + @doc """ + Map the input value, using a configured mapping or a generic one + """ + def map(app, from, to, input) do + mapping = app.mappings() |> Enum.find(&(&1.from() == from && &1.to() == to)) + + case mapping do + nil -> map(input, to) + mapping -> mapping.map(input) + end + end + + defp map(input, to) when is_map(input) do + input |> plain_map() |> to.new() + end + + defp map(input, to) when is_list(input) do + with items when is_list(items) <- + Enum.reduce_while(input, [], fn item, acc -> + case map(item, to) do + {:ok, item} -> {:cont, [item | acc]} + {:error, _} = error -> {:halt, error} + end + end), + do: {:ok, Enum.reverse(items)} + end + + @doc """ + Executes a command and publishes events + + This function does not take a context, which will disable permission checks + """ + def execute_command(command, params) do + if command.atomic?() do + repo = command.app().repo() + + repo.transaction(fn -> + with {:error, reason} <- do_execute_command(command, params) do + repo.rollback(reason) + end + end) + else + do_execute_command(command, params) + end + end + + @doc """ + Executes a command, publishes events, and returns a result + + This function takes a context, which will trigger checking for permissions based on policies, roles and scopes + """ + def execute_command(command, params, context) do + if command.atomic?() do + repo = command.app().repo() + + repo.transaction(fn -> + with {:error, reason} <- do_execute_command(command, params, context) do + repo.rollback(reason) + end + end) + else + do_execute_command(command, params, context) + end + end + + defp do_execute_command(command, params) do + with {:ok, params} <- params |> plain_map() |> command.params().validate(), + {:ok, result, events} <- command.execute(params), + :ok <- publish_events(events, command.app()) do + {:ok, result} + end + end + + defp do_execute_command(command, params, context) do + with {:ok, params} <- params |> plain_map() |> command.params().validate(), + context <- Map.put(context, :params, params), + :ok <- allow(command, context), + {:ok, result, events} <- command.execute(params, context), + :ok <- publish_events(events, command.app()) do + {:ok, result} + end + end + + defp allow(command, context) do + if command.allowed?(context) do + :ok + else + {:error, :unauthorized} + end + end + + @doc """ + Publish the given list of events + + Events are only published if there are subscriptions for them. + """ + def publish_events([], _app), do: :ok + + def publish_events(events, app) do + events + |> Enum.flat_map(&jobs(&1, app)) + |> Momo.Job.schedule_all() + + :ok + end + + defp jobs(event, app) do + jobs = + app.subscriptions() + |> Enum.filter(&(&1.event() == event.__struct__)) + |> Enum.map(&[event: event.__struct__, params: Jason.encode!(event), subscription: &1]) + + if jobs == [] do + Logger.warning("No subscriptions found for event", event: event.__struct__) + end + + jobs + end + + @doc """ + Executes a query that has parameters + """ + def execute_query(query, params, context), do: query.execute(params, context) + + @doc """ + Executes a query that has no parameters + """ + def execute_query(query, params), do: query.execute(params) end diff --git a/lib/momo/app/dsl.ex b/lib/momo/app/dsl.ex index 4d597afc..1f33865a 100644 --- a/lib/momo/app/dsl.ex +++ b/lib/momo/app/dsl.ex @@ -6,6 +6,14 @@ defmodule Momo.App.Dsl do tags: [ Momo.App.Dsl.Repos, Momo.App.Dsl.Endpoints, - Momo.App.Dsl.Features + Momo.App.Dsl.Models, + Momo.App.Dsl.Commands, + Momo.App.Dsl.Queries, + Momo.App.Dsl.Events, + Momo.App.Dsl.Flows, + Momo.App.Dsl.Subscriptions, + Momo.App.Dsl.Mappings, + Momo.App.Dsl.Values, + Momo.App.Dsl.Scopes ] end diff --git a/lib/momo/app/dsl/app.ex b/lib/momo/app/dsl/app.ex index cb4fe545..4113806a 100644 --- a/lib/momo/app/dsl/app.ex +++ b/lib/momo/app/dsl/app.ex @@ -6,6 +6,14 @@ defmodule Momo.App.Dsl.App do attribute :roles, kind: :string, required: true child :repos, min: 0, max: 1 child :endpoints, min: 0, max: 1 - child :features, min: 1, max: 1 + child :models, min: 0, max: 1 + child :commands, min: 0, max: 1 + child :queries, min: 0, max: 1 + child :events, min: 0, max: 1 + child :flows, min: 0, max: 1 + child :subscriptions, min: 0, max: 1 + child :mappings, min: 0, max: 1 + child :values, min: 0, max: 1 + child :scopes, min: 0, max: 1 end end diff --git a/lib/momo/app/dsl/commands.ex b/lib/momo/app/dsl/commands.ex new file mode 100644 index 00000000..5c1da9cc --- /dev/null +++ b/lib/momo/app/dsl/commands.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Commands do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/events.ex b/lib/momo/app/dsl/events.ex new file mode 100644 index 00000000..7eef7142 --- /dev/null +++ b/lib/momo/app/dsl/events.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Events do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/features.ex b/lib/momo/app/dsl/features.ex deleted file mode 100644 index 4d18c91f..00000000 --- a/lib/momo/app/dsl/features.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.App.Dsl.Features do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/app/dsl/flows.ex b/lib/momo/app/dsl/flows.ex new file mode 100644 index 00000000..53e2265a --- /dev/null +++ b/lib/momo/app/dsl/flows.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Flows do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/mappings.ex b/lib/momo/app/dsl/mappings.ex new file mode 100644 index 00000000..fc08197d --- /dev/null +++ b/lib/momo/app/dsl/mappings.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Mappings do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/models.ex b/lib/momo/app/dsl/models.ex new file mode 100644 index 00000000..0a64b426 --- /dev/null +++ b/lib/momo/app/dsl/models.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Models do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/queries.ex b/lib/momo/app/dsl/queries.ex new file mode 100644 index 00000000..8e646ba3 --- /dev/null +++ b/lib/momo/app/dsl/queries.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Queries do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/scopes.ex b/lib/momo/app/dsl/scopes.ex new file mode 100644 index 00000000..e63b402c --- /dev/null +++ b/lib/momo/app/dsl/scopes.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Scopes do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/subscriptions.ex b/lib/momo/app/dsl/subscriptions.ex new file mode 100644 index 00000000..0c6fbf29 --- /dev/null +++ b/lib/momo/app/dsl/subscriptions.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Subscriptions do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/dsl/values.ex b/lib/momo/app/dsl/values.ex new file mode 100644 index 00000000..48e3e0f8 --- /dev/null +++ b/lib/momo/app/dsl/values.ex @@ -0,0 +1,8 @@ +defmodule Momo.App.Dsl.Values do + @moduledoc false + use Diesel.Tag + + tag do + child kind: :module, min: 0 + end +end diff --git a/lib/momo/app/generator/application.ex b/lib/momo/app/generator/application.ex index 83de9394..eb335145 100644 --- a/lib/momo/app/generator/application.ex +++ b/lib/momo/app/generator/application.ex @@ -14,11 +14,20 @@ defmodule Momo.App.Generator.Application do @otp_app unquote(otp_app) @repos unquote(app.repos) @endpoints unquote(app.endpoints) - @features unquote(app.features) + @migrate __MODULE__.Migrate def repos, do: @repos - def features, do: @features + def repo, do: hd(@repos) + def models, do: unquote(app.models) + def commands, do: unquote(app.commands) + def queries, do: unquote(app.queries) + def events, do: unquote(app.events) + def flows, do: unquote(app.flows) + def subscriptions, do: unquote(app.subscriptions) + def mappings, do: unquote(app.mappings) + def values, do: unquote(app.values) + def scopes, do: unquote(app.scopes) @impl true def start(_type, _args) do diff --git a/lib/momo/feature/generator/graph.ex b/lib/momo/app/generator/graph.ex similarity index 92% rename from lib/momo/feature/generator/graph.ex rename to lib/momo/app/generator/graph.ex index ffd2cb31..64eb293a 100644 --- a/lib/momo/feature/generator/graph.ex +++ b/lib/momo/app/generator/graph.ex @@ -1,13 +1,13 @@ -defmodule Momo.Feature.Generator.Graph do +defmodule Momo.App.Generator.Graph do @moduledoc false @behaviour Diesel.Generator @impl true - def generate(feature, _) do + def generate(app, _) do graph = Graph.new() graph = - Enum.reduce(feature.models, graph, fn model, g -> + Enum.reduce(app.models, graph, fn model, g -> g |> with_model(model) |> with_attributes(model) @@ -18,7 +18,7 @@ defmodule Momo.Feature.Generator.Graph do graph_function(graph), get_shortest_path_function(), get_paths_function(), - diagram_function(feature, graph), + diagram_function(app, graph), simple_path_function(), vertex_function() ] @@ -96,9 +96,9 @@ defmodule Momo.Feature.Generator.Graph do end end - defp diagram_function(feature, graph) do + defp diagram_function(app, graph) do {:ok, dot} = Graph.to_dot(graph) - name = feature.name + name = app.name quote do @graph_dot unquote(dot) diff --git a/lib/momo/feature/generator/map.ex b/lib/momo/app/generator/map.ex similarity index 52% rename from lib/momo/feature/generator/map.ex rename to lib/momo/app/generator/map.ex index 7035f29a..d7c4345e 100644 --- a/lib/momo/feature/generator/map.ex +++ b/lib/momo/app/generator/map.ex @@ -1,11 +1,11 @@ -defmodule Momo.Feature.Generator.Map do +defmodule Momo.App.Generator.Map do @moduledoc false @behaviour Diesel.Generator @impl true def generate(_, _) do quote location: :keep do - def map(from, to, input), do: Momo.Feature.map(__MODULE__, from, to, input) + def map(from, to, input), do: Momo.App.map(__MODULE__, from, to, input) end end end diff --git a/lib/momo/app/parser.ex b/lib/momo/app/parser.ex index 9db1557b..05c35435 100644 --- a/lib/momo/app/parser.ex +++ b/lib/momo/app/parser.ex @@ -16,11 +16,18 @@ defmodule Momo.App.Parser do repos = for {:repos, _, repos} <- children, do: repos endpoints = for {:endpoints, _, endpoints} <- children, do: endpoints - features = for {:features, _, features} <- children, do: features + models = for {:models, _, models} <- children, do: models + commands = for {:commands, _, commands} <- children, do: commands + queries = for {:queries, _, queries} <- children, do: queries + events = for {:events, _, events} <- children, do: events + flows = for {:flows, _, flows} <- children, do: flows + subscriptions = for {:subscriptions, _, subscriptions} <- children, do: subscriptions + mappings = for {:mappings, _, mappings} <- children, do: mappings + values = for {:values, _, values} <- children, do: values + scopes = for {:scopes, _, scopes} <- children, do: scopes repos = List.flatten(repos) endpoints = List.flatten(endpoints) - features = List.flatten(features) name = caller_module |> Module.split() |> Enum.drop(-1) |> Module.concat() @@ -33,7 +40,15 @@ defmodule Momo.App.Parser do module: caller_module, repos: repos, endpoints: endpoints, - features: features + models: List.flatten(models), + commands: List.flatten(commands), + queries: List.flatten(queries), + events: List.flatten(events), + flows: List.flatten(flows), + subscriptions: List.flatten(subscriptions), + mappings: List.flatten(mappings), + values: List.flatten(values), + scopes: List.flatten(scopes) } end end diff --git a/lib/momo/command.ex b/lib/momo/command.ex index aa793f1b..9a256afb 100644 --- a/lib/momo/command.ex +++ b/lib/momo/command.ex @@ -13,7 +13,7 @@ defmodule Momo.Command do :name, :title, :fun_name, - :feature, + :app, :params, :returns, :many, @@ -24,6 +24,7 @@ defmodule Momo.Command do :path ] + require Logger import Momo.Maps defmodule Policy do @@ -37,19 +38,16 @@ defmodule Momo.Command do end def allowed?(command, context) do - case command.feature().app().roles_from_context(context) do - {:ok, []} -> - true - - {:ok, roles} -> - allowed(roles, command.policies(), context) - - _ -> - false + case command.app().roles_from_context(context) do + {:ok, roles} -> allowed?(roles, command.policies(), context) + {:error, _} -> false end end - defp allowed(roles, policies, context) do + defp allowed?([], _policies, _context), do: true + defp allowed?(_roles, policies, _context) when map_size(policies) == 0, do: true + + defp allowed?(roles, policies, context) do policies = roles |> Enum.map(&Map.get(policies, &1)) @@ -69,31 +67,61 @@ defmodule Momo.Command do end @doc """ - Executes the given command - - * `{:ok, term(), [events]}` - The command was executed successfully and returned a result, and a list of events - * `{:error, term()}` - The command failed to execute and the reason is provided. + Executes a command and publishes any events emitted """ def execute(command, params, context) do - with {:ok, result} <- execute_command(command, params, context), - {:ok, events} <- - maybe_create_events(command.feature(), command.events(), result, context) do - {:ok, result, events} + if command.atomic?() do + repo = command.app().repo() + + repo.transaction(fn -> + with {:error, reason} <- do_execute_command(command, params, context) do + repo.rollback(reason) + end + end) + |> then(fn + {:ok, {:ok, result}} -> {:ok, result} + {:ok, :ok} -> :ok + {:error, _} = error -> error + end) + else + do_execute_command(command, params, context) end end - defp execute_command(command, params, context) do - with :ok <- command.handle(params, context) do + defp do_execute_command(command, params, context) do + with {:ok, params} <- params |> plain_map() |> command.params().validate(), + context <- Map.put(context, :params, params), + :ok <- authorize(command, context), + {:ok, result} <- handle(command, params, context), + {:ok, events} <- events(command, result, context), + :ok <- publish_events(command, events) do + {:ok, result} + end + end + + defp authorize(_command, %{authorization: :skip}), do: :ok + + defp authorize(command, context) do + if allowed?(command, context) do + :ok + else + {:error, :unauthorized} + end + end + + defp handle(command, params, context) do + with :ok <- command.handler().execute(params, context) do {:ok, params} end end - defp maybe_create_events(_feature, [], _result, _context), do: {:ok, []} + defp events(command, result, context) do + app = command.app() + events = command.events() - defp maybe_create_events(feature, events, result, context) when is_list(events) do with events when is_list(events) <- Enum.reduce_while(events, [], fn event, acc -> - case maybe_create_events(feature, event, result, context) do + case maybe_create_events(app, event, result, context) do nil -> {:cont, acc} {:ok, new_events} when is_list(new_events) -> {:cont, new_events ++ acc} {:ok, new_event} -> {:cont, [new_event | acc]} @@ -103,10 +131,10 @@ defmodule Momo.Command do do: {:ok, Enum.reverse(events)} end - defp maybe_create_events(feature, event, result, context) when is_list(result) do + defp maybe_create_events(app, event, result, context) when is_list(result) do with events when is_list(events) <- Enum.reduce_while(result, [], fn item, acc -> - case maybe_create_event(feature, event, item, context) do + case maybe_create_event(app, event, item, context) do nil -> {:cont, acc} {:ok, event} -> {:cont, [event | acc]} {:error, reason} -> {:halt, {:error, reason}} @@ -115,36 +143,61 @@ defmodule Momo.Command do do: {:ok, Enum.reverse(events)} end - defp maybe_create_events(feature, event, result, context) do - maybe_create_events(feature, event, [result], context) + defp maybe_create_events(app, event, result, context) do + maybe_create_events(app, event, [result], context) end - defp maybe_create_event(feature, event, result, context) do + defp maybe_create_event(app, event, result, context) do if_expr = event.if unless_expr = event.unless - maybe_create_event(feature, event, result, context, if_expr, unless_expr) + maybe_create_event(app, event, result, context, if_expr, unless_expr) end - defp maybe_create_event(feature, event, result, context, nil, nil), - do: create_event(feature, event, result, context) + defp maybe_create_event(app, event, result, context, nil, nil), + do: create_event(app, event, result, context) - defp maybe_create_event(feature, event, result, context, if_expr, nil) do - if if_expr.execute(result, context), do: create_event(feature, event, result, context) + defp maybe_create_event(app, event, result, context, if_expr, nil) do + if if_expr.execute(result, context), do: create_event(app, event, result, context) end - defp maybe_create_event(feature, event, result, context, nil, unless_expr) do - if not unless_expr.execute(result, context), do: create_event(feature, event, result, context) + defp maybe_create_event(app, event, result, context, nil, unless_expr) do + if not unless_expr.execute(result, context), do: create_event(app, event, result, context) end - defp maybe_create_event(feature, event, result, context, if_expr, unless_expr) do + defp maybe_create_event(app, event, result, context, if_expr, unless_expr) do if not unless_expr.execute(result, context) && if_expr.execute(result, context), - do: create_event(feature, event, result, context) + do: create_event(app, event, result, context) end - defp create_event(feature, event, result, context) do + defp create_event(app, event, result, context) do data = result |> plain_map() |> Map.merge(context) - feature.map(event.source, event.module, data) + app.map(event.source, event.module, data) + end + + defp publish_events([], _feature), do: :ok + + defp publish_events(command, events) do + app = command.app() + + events + |> Enum.flat_map(&jobs(&1, app)) + |> Momo.Job.schedule_all() + + :ok + end + + defp jobs(event, app) do + jobs = + app.subscriptions() + |> Enum.filter(&(&1.event() == event.__struct__)) + |> Enum.map(&[event: event.__struct__, params: Jason.encode!(event), subscription: &1]) + + if jobs == [] do + Logger.warning("No subscriptions found for event", event: event.__struct__) + end + + jobs end end diff --git a/lib/momo/command/dsl/command.ex b/lib/momo/command/dsl/command.ex index 0884908b..466df5f8 100644 --- a/lib/momo/command/dsl/command.ex +++ b/lib/momo/command/dsl/command.ex @@ -8,6 +8,7 @@ defmodule Momo.Command.Dsl.Command do attribute :many, kind: :boolean, required: false, default: false attribute :atomic, kind: :boolean, required: false, default: false attribute :title, kind: :string, required: false + attribute :handler, kind: :module, required: true child :policy, min: 0 child :publish, min: 0 diff --git a/lib/momo/command/generator/execute.ex b/lib/momo/command/generator/execute.ex index 8ebd980d..c2e754e6 100644 --- a/lib/momo/command/generator/execute.ex +++ b/lib/momo/command/generator/execute.ex @@ -5,7 +5,8 @@ defmodule Momo.Command.Generator.Execute do @impl true def generate(_command, _opts) do quote do - def execute(params, context \\ %{}), do: Momo.Command.execute(__MODULE__, params, context) + def execute(params, context \\ %{authorization: :skip}), + do: Momo.Command.execute(__MODULE__, params, context) end end end diff --git a/lib/momo/command/generator/metadata.ex b/lib/momo/command/generator/metadata.ex index 449a5c8b..134e082d 100644 --- a/lib/momo/command/generator/metadata.ex +++ b/lib/momo/command/generator/metadata.ex @@ -6,11 +6,12 @@ defmodule Momo.Command.Generator.Metadata do def generate(command, _opts) do quote do def title, do: unquote(command.title) + def handler, do: unquote(command.handler) def fun_name, do: unquote(command.fun_name) def atomic?, do: unquote(command.atomic?) def params, do: unquote(command.params) def returns, do: unquote(command.returns) - def feature, do: unquote(command.feature) + def app, do: unquote(command.app) def policies, do: unquote(Macro.escape(command.policies)) def events, do: unquote(Macro.escape(command.events)) end diff --git a/lib/momo/command/parser.ex b/lib/momo/command/parser.ex index 10ba836b..13148dcf 100644 --- a/lib/momo/command/parser.ex +++ b/lib/momo/command/parser.ex @@ -6,7 +6,7 @@ defmodule Momo.Command.Parser do alias Momo.Command.Policy alias Momo.Command.Event - import Momo.Feature.Naming + import Momo.Naming def parse({:command, attrs, children}, opts) do name = Keyword.fetch!(opts, :caller_module) @@ -22,7 +22,7 @@ defmodule Momo.Command.Parser do title = attrs[:title] || default_title caller = Keyword.fetch!(opts, :caller_module) - feature = feature_module(caller) + app = app(caller) params = Keyword.fetch!(attrs, :params) returns = attrs[:returns] || params @@ -47,7 +47,7 @@ defmodule Momo.Command.Parser do module_last = module |> Module.split() |> List.last() |> Macro.underscore() source_last = source |> Module.split() |> List.last() |> Macro.underscore() mapping = Macro.camelize("#{module_last}_from_#{source_last}") - mapping = Module.concat([feature, "Mappings", mapping]) + mapping = Module.concat([app, "Mappings", mapping]) if_expr = Keyword.get(attrs, :if) unless_expr = Keyword.get(attrs, :unless) @@ -71,12 +71,14 @@ defmodule Momo.Command.Parser do |> String.to_atom() many = Keyword.get(attrs, :many, false) + handler = Keyword.fetch!(attrs, :handler) %Command{ name: name, title: title, fun_name: fun_name, - feature: feature, + handler: handler, + app: app, params: params, returns: returns, many: many, diff --git a/lib/momo/evaluate.ex b/lib/momo/evaluate.ex index 6f196317..2f652257 100644 --- a/lib/momo/evaluate.ex +++ b/lib/momo/evaluate.ex @@ -27,7 +27,7 @@ defmodule Momo.Evaluate do end def evaluate(%{__struct__: model} = context, {:path, [:**, ancestor | rest]}) do - case model.feature().get_shortest_path(model.name(), ancestor) do + case model.app().get_shortest_path(model.name(), ancestor) do [] -> nil @@ -105,7 +105,7 @@ defmodule Momo.Evaluate do key = {id, field} with nil <- Process.get(key) do - rel = context |> model.feature().repo().preload(field) |> Map.get(field) + rel = context |> model.app().repo().preload(field) |> Map.get(field) Process.put(key, rel) rel end diff --git a/lib/momo/event.ex b/lib/momo/event.ex index e9ed852d..f9e2a320 100644 --- a/lib/momo/event.ex +++ b/lib/momo/event.ex @@ -19,7 +19,7 @@ defmodule Momo.Event do defstruct [:name, :type, :many, :required, :default, :allowed_values] end - defstruct [:name, :version, :fields, :feature] + defstruct [:name, :version, :fields, :app] import Ecto.Changeset diff --git a/lib/momo/event/generator/metadata.ex b/lib/momo/event/generator/metadata.ex index 905fff13..1775ddf1 100644 --- a/lib/momo/event/generator/metadata.ex +++ b/lib/momo/event/generator/metadata.ex @@ -7,7 +7,7 @@ defmodule Momo.Event.Generator.Metadata do quote do def fields, do: unquote(Macro.escape(event.fields)) def version, do: unquote(event.version) - def feature, do: unquote(event.feature) + def app, do: unquote(event.app) def name, do: unquote(event.name) end end diff --git a/lib/momo/event/parser.ex b/lib/momo/event/parser.ex index 3b837dc0..a5194f68 100644 --- a/lib/momo/event/parser.ex +++ b/lib/momo/event/parser.ex @@ -5,12 +5,12 @@ defmodule Momo.Event.Parser do alias Momo.Event alias Momo.Event.Field - import Momo.Feature.Naming + import Momo.Naming def parse({:event, attrs, children}, opts) do name = Keyword.fetch!(opts, :caller_module) caller = Keyword.fetch!(opts, :caller_module) - feature = feature_module(caller) + app = app(caller) version = Keyword.get(attrs, :version, 1) @@ -28,7 +28,7 @@ defmodule Momo.Event.Parser do %Event{ name: name, - feature: feature, + app: app, version: version, fields: fields } diff --git a/lib/momo/feature.ex b/lib/momo/feature.ex deleted file mode 100644 index d8a83a61..00000000 --- a/lib/momo/feature.ex +++ /dev/null @@ -1,172 +0,0 @@ -defmodule Momo.Feature do - @moduledoc false - use Diesel, - otp_app: :momo, - dsl: Momo.Feature.Dsl, - parsers: [ - Momo.Feature.Parser - ], - generators: [ - Momo.Feature.Generator.Metadata, - Momo.Feature.Generator.Roles, - Momo.Feature.Generator.Graph, - Momo.Feature.Generator.Map, - Momo.Feature.Generator.CreateFunctions, - Momo.Feature.Generator.UpdateFunctions, - Momo.Feature.Generator.Commands, - Momo.Feature.Generator.Queries, - Momo.Feature.Generator.Flows - ] - - defstruct [ - :app, - :name, - :repo, - scopes: [], - models: [], - handlers: [], - commands: [], - queries: [], - events: [], - flows: [], - subscriptions: [], - values: [], - mappings: [] - ] - - require Logger - - import Momo.Maps - - @doc """ - Map the input value, using a configured mapping or a generic one - """ - def map(feature, from, to, input) do - mapping = feature.mappings() |> Enum.find(&(&1.from() == from && &1.to() == to)) - - case mapping do - nil -> map(input, to) - mapping -> mapping.map(input) - end - end - - defp map(input, to) when is_map(input) do - input |> plain_map() |> to.new() - end - - defp map(input, to) when is_list(input) do - with items when is_list(items) <- - Enum.reduce_while(input, [], fn item, acc -> - case map(item, to) do - {:ok, item} -> {:cont, [item | acc]} - {:error, _} = error -> {:halt, error} - end - end), - do: {:ok, Enum.reverse(items)} - end - - @doc """ - Executes a command and publishes events - - This function does not take a context, which will disable permission checks - """ - def execute_command(command, params) do - if command.atomic?() do - repo = command.feature().repo() - - repo.transaction(fn -> - with {:error, reason} <- do_execute_command(command, params) do - repo.rollback(reason) - end - end) - else - do_execute_command(command, params) - end - end - - @doc """ - Executes a command, publishes events, and returns a result - - This function takes a context, which will trigger checking for permissions based on policies, roles and scopes - """ - def execute_command(command, params, context) do - if command.atomic?() do - repo = command.feature().repo() - - repo.transaction(fn -> - with {:error, reason} <- do_execute_command(command, params, context) do - repo.rollback(reason) - end - end) - else - do_execute_command(command, params, context) - end - end - - defp do_execute_command(command, params) do - with {:ok, params} <- params |> plain_map() |> command.params().validate(), - {:ok, result, events} <- command.execute(params), - :ok <- publish_events(events, command.feature()) do - {:ok, result} - end - end - - defp do_execute_command(command, params, context) do - with {:ok, params} <- params |> plain_map() |> command.params().validate(), - context <- Map.put(context, :params, params), - :ok <- allow(command, context), - {:ok, result, events} <- command.execute(params, context), - :ok <- publish_events(events, command.feature()) do - {:ok, result} - end - end - - defp allow(command, context) do - if command.allowed?(context) do - :ok - else - {:error, :unauthorized} - end - end - - @doc """ - Publish the given list of events - - Events are only published if there are subscriptions for them. - """ - def publish_events([], _feature), do: :ok - - def publish_events(events, feature) do - app = feature.app() - - events - |> Enum.flat_map(&jobs(&1, app)) - |> Momo.Job.schedule_all() - - :ok - end - - defp jobs(event, app) do - jobs = - app.features() - |> Enum.flat_map(& &1.subscriptions()) - |> Enum.filter(&(&1.event() == event.__struct__)) - |> Enum.map(&[event: event.__struct__, params: Jason.encode!(event), subscription: &1]) - - if jobs == [] do - Logger.warning("No subscriptions found for event", event: event.__struct__) - end - - jobs - end - - @doc """ - Executes a query that has parameters - """ - def execute_query(query, params, context), do: query.execute(params, context) - - @doc """ - Executes a query that has no parameters - """ - def execute_query(query, params), do: query.execute(params) -end diff --git a/lib/momo/feature/ast.ex b/lib/momo/feature/ast.ex deleted file mode 100644 index 9696221f..00000000 --- a/lib/momo/feature/ast.ex +++ /dev/null @@ -1,141 +0,0 @@ -defmodule Momo.Feature.Ast do - @moduledoc """ - Feature specific ast helpers - """ - - import Momo.Naming - - @doc """ - Returns a list of pattern matched variables for each one of the parents of the given model - """ - def function_parent_args(model) do - for rel <- model.parents() do - quote do - %unquote(rel.target.module){} = unquote(var(rel.name)) - end - end - end - - @doc """ - Populate a context map with the values for the parent models of a given model - """ - def context_with_parents(model) do - context = var(:context) - - for rel <- model.parents() do - var = var(rel.name) - - quote do - unquote(context) <- Map.put(unquote(context), unquote(rel.name), unquote(var)) - end - end - end - - @doc """ - Populate a context map with the params given to an action - """ - def context_with_params do - context = var(:context) - attrs = var(:attrs) - - quote do - unquote(context) <- Map.merge(unquote(attrs), unquote(context)) - end - end - - @doc """ - Populate a context map with a given model - """ - def context_with_model(model) do - model_name = model.name() - context = var(:context) - model_var = var(model_name) - - quote do - unquote(context) <- Map.put(unquote(context), unquote(model_name), unquote(model_var)) - end - end - - @doc """ - Populates the map of attributes, with the ids from required parents - """ - def attrs_with_required_parents(model) do - attrs = var(:attrs) - - for %{required?: true} = rel <- model.parents() do - column = rel.column_name - var = var(rel.name) - - quote do - unquote(attrs) <- Map.put(unquote(attrs), unquote(column), unquote(attrs).unquote(var).id) - end - end - end - - @doc """ - Populates the map of attributes, with the ids from optional parents - """ - def attrs_with_optional_parents(model) do - attrs = var(:attrs) - - for %{required?: false} = rel <- model.parents() do - column = rel.column_name - var = var(rel.name) - - quote do - unquote(attrs) <- Map.put(unquote(attrs), unquote(column), maybe_id(unquote(var))) - end - end - end - - @doc """ - Collects computeed attributes values and sets them into the map of attributes - """ - def attrs_with_computed_attributes(model) do - attrs = var(:attrs) - context = var(:context) - - for %{computed?: true, using: mod} = attr <- model.attributes() do - quote do - unquote(attrs) <- - compute_attribute(unquote(attrs), unquote(attr.name), unquote(mod), unquote(context)) - end - end - end - - @doc """ - Sets the map of attrs into the context, for authorization purposes - """ - def context_with_args do - context = var(:context) - attrs = var(:attrs) - - quote do - unquote(context) <- Map.put(unquote(context), :args, unquote(attrs)) - end - end - - def allowed?(model, action) do - action_name = action.name - model_name = model.name() - context = var(:context) - - quote do - :ok <- allow(unquote(model_name), unquote(action_name), unquote(context)) - end - end - - @doc """ - Generats the code that fetches the model and sets its as a variable inside a with clause - """ - def fetch_model(model) do - model_name = model.name() - model_var = var(model_name) - id = var(:id) - context = var(:context) - - quote do - {:ok, unquote(model_var)} <- unquote(model).fetch(unquote(id), unquote(context)) - end - end -end diff --git a/lib/momo/feature/dsl.ex b/lib/momo/feature/dsl.ex deleted file mode 100644 index 85244451..00000000 --- a/lib/momo/feature/dsl.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Momo.Feature.Dsl do - @moduledoc false - use Diesel.Dsl, - otp_app: :momo, - root: Momo.Feature.Dsl.Feature, - tags: [ - Momo.Feature.Dsl.Commands, - Momo.Feature.Dsl.Handlers, - Momo.Feature.Dsl.Models, - Momo.Feature.Dsl.Queries, - Momo.Feature.Dsl.Scopes, - Momo.Feature.Dsl.Events, - Momo.Feature.Dsl.Subscriptions, - Momo.Feature.Dsl.Flows, - Momo.Feature.Dsl.Mappings - ] -end diff --git a/lib/momo/feature/dsl/commands.ex b/lib/momo/feature/dsl/commands.ex deleted file mode 100644 index f31b1d87..00000000 --- a/lib/momo/feature/dsl/commands.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Commands do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/events.ex b/lib/momo/feature/dsl/events.ex deleted file mode 100644 index 27a254d5..00000000 --- a/lib/momo/feature/dsl/events.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Events do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/feature.ex b/lib/momo/feature/dsl/feature.ex deleted file mode 100644 index ec51f5bb..00000000 --- a/lib/momo/feature/dsl/feature.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Momo.Feature.Dsl.Feature do - @moduledoc false - use Diesel.Tag - - tag do - child :models, min: 0, max: 1 - child :scopes, min: 0, max: 1 - child :commands, min: 0, max: 1 - child :queries, min: 0, max: 1 - child :handlers, min: 0, max: 1 - child :events, min: 0, max: 1 - child :subscriptions, min: 0, max: 1 - child :mappings, min: 0, max: 1 - child :flows, min: 0, max: 1 - end -end diff --git a/lib/momo/feature/dsl/flows.ex b/lib/momo/feature/dsl/flows.ex deleted file mode 100644 index 9939ccf2..00000000 --- a/lib/momo/feature/dsl/flows.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Flows do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/handlers.ex b/lib/momo/feature/dsl/handlers.ex deleted file mode 100644 index d25cb9a3..00000000 --- a/lib/momo/feature/dsl/handlers.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Handlers do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/mappings.ex b/lib/momo/feature/dsl/mappings.ex deleted file mode 100644 index f3168ebf..00000000 --- a/lib/momo/feature/dsl/mappings.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Mappings do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/models.ex b/lib/momo/feature/dsl/models.ex deleted file mode 100644 index f5e8079b..00000000 --- a/lib/momo/feature/dsl/models.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Models do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/queries.ex b/lib/momo/feature/dsl/queries.ex deleted file mode 100644 index de5be6fa..00000000 --- a/lib/momo/feature/dsl/queries.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Queries do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/scopes.ex b/lib/momo/feature/dsl/scopes.ex deleted file mode 100644 index e950ce50..00000000 --- a/lib/momo/feature/dsl/scopes.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Scopes do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/dsl/subscriptions.ex b/lib/momo/feature/dsl/subscriptions.ex deleted file mode 100644 index ef5e7223..00000000 --- a/lib/momo/feature/dsl/subscriptions.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Momo.Feature.Dsl.Subscriptions do - @moduledoc false - use Diesel.Tag - - tag do - child kind: :module, min: 1 - end -end diff --git a/lib/momo/feature/generator/commands.ex b/lib/momo/feature/generator/commands.ex deleted file mode 100644 index d0b1ba5d..00000000 --- a/lib/momo/feature/generator/commands.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule Momo.Feature.Generator.Commands do - @moduledoc false - - @behaviour Diesel.Generator - - @impl true - def generate(feature, _opts) do - for command <- feature.commands do - fun_name = command.fun_name() - - quote do - def unquote(fun_name)(params), - do: Momo.Feature.execute_command(unquote(command), params) - - def unquote(fun_name)(params, context), - do: Momo.Feature.execute_command(unquote(command), params, context) - end - end - end -end diff --git a/lib/momo/feature/generator/create_functions.ex b/lib/momo/feature/generator/create_functions.ex deleted file mode 100644 index ad33ea7f..00000000 --- a/lib/momo/feature/generator/create_functions.ex +++ /dev/null @@ -1,152 +0,0 @@ -defmodule Momo.Feature.Generator.CreateFunctions do - @moduledoc false - @behaviour Diesel.Generator - - import Momo.Naming - - @impl true - def generate(feature, _) do - create_funs(feature) ++ - do_create_funs(feature) ++ bulk_create_funs(feature) ++ create_children_funs(feature) - end - - defp create_funs(feature) do - for model <- feature.models do - model_name = model.name() - action_fun_name = String.to_atom("create_#{model_name}") - do_action_fun_name = String.to_atom("do_create_#{model_name}") - children_action_fun_name = String.to_atom("create_#{model_name}_children") - - fun_with_map_args = - quote location: :keep do - def unquote(action_fun_name)(attrs, context \\ %{}) - - def unquote(action_fun_name)(attrs, context) when is_map(attrs) do - repo = repo() - - repo.transaction(fn -> - with {:ok, model} <- unquote(do_action_fun_name)(attrs, context), - :ok <- unquote(children_action_fun_name)(model, attrs, context) do - model - else - {:error, reason} -> - repo.rollback(reason) - end - end) - end - end - - fun_with_kw_args = - quote location: :keep do - def unquote(action_fun_name)(attrs, context) when is_list(attrs) do - attrs - |> Map.new() - |> unquote(action_fun_name)() - end - end - - [fun_with_map_args, fun_with_kw_args] - end - end - - defp do_create_funs(feature) do - for model <- feature.models do - model_name = model.name() - action_fun_name = String.to_atom("do_create_#{model_name}") - - attr_names = - for attr when not attr.computed? <- model.attributes(), - do: attr.name - - parent_fields = - for rel when not rel.computed? <- model.parents(), - into: %{}, - do: {rel.name, rel.column_name} - - default_values = - for attr when not is_nil(attr.default) <- model.attributes(), into: %{} do - {attr.name, attr.default} - end - - quote location: :keep do - defp unquote(action_fun_name)(attrs, context) do - attrs - |> Map.take(unquote(attr_names)) - |> collect_ids(attrs, unquote(Macro.escape(parent_fields))) - |> Momo.Feature.Helpers.set_default_values(unquote(Macro.escape(default_values))) - |> Momo.Maps.string_keys() - |> Map.put_new_lazy("id", &Ecto.UUID.generate/0) - |> unquote(model).create() - end - end - end - end - - defp create_children_funs(feature) do - for model <- feature.models do - model_name = model.name() - action_fun_name = String.to_atom("create_#{model_name}_children") - - child_fields = - for rel when not rel.computed? <- model.children(), - do: {rel.name, rel.inverse.name, String.to_atom("create_#{rel.target.name}")} - - quote location: :keep do - defp unquote(action_fun_name)(model, attrs, context) do - context = Map.put(context, unquote(model_name), model) - - unquote(Macro.escape(child_fields)) - |> Enum.reduce([], fn {child_name, inverse_name, create_fun_name}, acc -> - case Map.get(attrs, child_name) do - children when is_list(children) -> - Enum.map(children, &{Map.put(&1, inverse_name, model), create_fun_name}) - - _ -> - [] - end - end) - |> List.flatten() - |> Enum.map(fn {child, create_fun_name} -> - apply(__MODULE__, create_fun_name, [child, context]) - end) - |> Enum.reduce_while(:ok, fn - {:ok, _}, _ -> {:cont, :ok} - {:error, _} = error, _ -> {:halt, error} - end) - end - end - end - end - - defp bulk_create_funs(feature) do - for model <- feature.models do - single_fun_name = String.to_atom("create_#{model.name()}") - bulk_fun_name = String.to_atom("create_#{model.plural()}") - - quote location: :keep do - def unquote(bulk_fun_name)(items, context \\ %{}) when is_list(items) do - repo = repo() - - with {:ok, :ok} <- - repo.transaction(fn -> - items - |> Enum.reduce_while(nil, fn item, _ -> - case unquote(single_fun_name)(item, context) do - {:ok, _} -> - {:cont, :ok} - - {:error, _} = error -> - {:halt, error} - end - end) - |> then(fn - :ok -> :ok - {:error, reason} -> repo.rollback(reason) - end) - end), - do: :ok - end - end - end - end -end diff --git a/lib/momo/feature/generator/flows.ex b/lib/momo/feature/generator/flows.ex deleted file mode 100644 index 4da92ddb..00000000 --- a/lib/momo/feature/generator/flows.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Momo.Feature.Generator.Flows do - @moduledoc false - - @behaviour Diesel.Generator - - @impl true - def generate(feature, _opts) do - for flow <- feature.flows do - fun_name = flow.fun_name() - - quote location: :keep do - def unquote(fun_name)(params, context \\ %{}), - do: Momo.Flow.execute(unquote(flow), params, context) - end - end - end -end diff --git a/lib/momo/feature/generator/metadata.ex b/lib/momo/feature/generator/metadata.ex deleted file mode 100644 index f08b6894..00000000 --- a/lib/momo/feature/generator/metadata.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Momo.Feature.Generator.Metadata do - @moduledoc false - @behaviour Diesel.Generator - - @impl true - def generate(feature, _) do - quote do - import Ecto.Query - import Momo.Feature.Helpers - - @repo unquote(feature.repo) - - def repo, do: @repo - def app, do: unquote(feature.app) - def name, do: unquote(feature.name) - - def models, do: unquote(feature.models) - def events, do: unquote(feature.events) - def mappings, do: unquote(feature.mappings) - def commands, do: unquote(feature.commands) - def queries, do: unquote(feature.queries) - def scopes, do: unquote(feature.scopes) - def subscriptions, do: unquote(feature.subscriptions) - def values, do: unquote(feature.values) - end - end -end diff --git a/lib/momo/feature/generator/queries.ex b/lib/momo/feature/generator/queries.ex deleted file mode 100644 index 5542cb76..00000000 --- a/lib/momo/feature/generator/queries.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Momo.Feature.Generator.Queries do - @moduledoc false - - @behaviour Diesel.Generator - - @impl true - def generate(feature, _opts) do - for query <- feature.queries do - fun_name = Momo.Query.fun_name(query) - params_module = query.params() - - if params_module != nil do - quote do - def unquote(fun_name)(params, context \\ %{}), - do: Momo.Feature.execute_query(unquote(query), params, context) - end - else - quote do - def unquote(fun_name)(context \\ %{}), - do: Momo.Feature.execute_query(unquote(query), context) - end - end - end - end -end diff --git a/lib/momo/feature/generator/roles.ex b/lib/momo/feature/generator/roles.ex deleted file mode 100644 index e5bf8ced..00000000 --- a/lib/momo/feature/generator/roles.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Momo.Feature.Generator.Roles do - @moduledoc false - - @behaviour Diesel.Generator - - @impl true - def generate(feature, _), do: roles_fun(feature.scopes, feature) - - defp roles_fun([], _) do - quote do - def roles(_params), do: [] - end - end - - defp roles_fun(_, _) do - quote do - def roles(_context) do - [] - end - end - end -end diff --git a/lib/momo/feature/generator/update_functions.ex b/lib/momo/feature/generator/update_functions.ex deleted file mode 100644 index 948b70fe..00000000 --- a/lib/momo/feature/generator/update_functions.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Momo.Feature.Generator.UpdateFunctions do - @moduledoc false - @behaviour Diesel.Generator - - import Momo.Naming - - @impl true - def generate(feature, _) do - for model <- feature.models do - model_name = model.name() - action_fun_name = String.to_atom("update_#{model_name}") - do_action_fun_name = String.to_atom("do_update_#{model_name}") - - attr_names = - for attr when attr.mutable? and not attr.computed? <- model.attributes(), - do: attr.name - - parent_fields = - for rel when rel.mutable? and not rel.computed? <- model.parents(), - into: %{}, - do: {rel.name, rel.column_name} - - do_update_fun = - quote do - defp unquote(do_action_fun_name)(model, attrs, context) do - fields = - attrs - |> Map.take(unquote(attr_names)) - |> collect_ids(attrs, unquote(Macro.escape(parent_fields))) - - unquote(model).edit(model, fields) - end - end - - fun_with_map_args = - quote do - def unquote(action_fun_name)(model, attrs, context \\ %{}) - - def unquote(action_fun_name)(model, attrs, context) when is_map(attrs) do - context = attrs |> Map.merge(context) |> Map.put(unquote(model_name), model) - repo = repo() - unquote(do_action_fun_name)(model, attrs, context) - end - end - - fun_with_kw_args = - quote do - def unquote(action_fun_name)(model, attrs, context) when is_list(attrs) do - attrs = Map.new(attrs) - - unquote(action_fun_name)(model, attrs, context) - end - end - - [do_update_fun, fun_with_map_args, fun_with_kw_args] - end - end -end diff --git a/lib/momo/feature/helpers.ex b/lib/momo/feature/helpers.ex deleted file mode 100644 index 145bb247..00000000 --- a/lib/momo/feature/helpers.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule Momo.Feature.Helpers do - @moduledoc false - - alias Momo.QueryBuilder - - def maybe_filter(query, model, context) do - case Map.get(context, :query) do - nil -> - query - - filters -> - builder = QueryBuilder.from_simple_map(model, filters) - - QueryBuilder.build(query, builder) - end - end - - def collect_ids(dest, source, fields) do - Enum.reduce(fields, dest, fn {field, new_key}, acc -> - case Map.get(source, field) do - %{id: id} -> Map.put(acc, new_key, id) - _ -> acc - end - end) - end - - def collect_values(dest, source, fields) do - Enum.reduce(fields, dest, fn field, acc -> - case Map.get(source, field) do - values when is_list(values) -> Map.put(acc, field, values) - _ -> acc - end - end) - end - - def set_default_values(attrs, defaults) do - Enum.reduce(defaults, attrs, fn {key, default_value}, acc -> - Map.put_new(acc, key, default_value) - end) - end - - def tasks_to_execute(_, _, %{skip_tasks: true}), do: [] - - def tasks_to_execute(tasks, model, _context) do - tasks - |> Enum.filter(fn - {_module, nil} -> - true - - {_module, conditions} when is_list(conditions) -> - Enum.all?(conditions, fn {field, value} -> Map.get(model, field) == value end) - - {_module, condition} -> - condition.execute(model) - end) - |> Enum.map(fn {module, _} -> module end) - end - - def tasks_to_execute(_, _, _, %{skip_tasks: true}), do: [] - - def tasks_to_execute(tasks, model, updated, _context) do - tasks - |> Enum.filter(fn - {_module, nil} -> - true - - {_module, conditions} when is_list(conditions) -> - Enum.all?(conditions, fn {field, value} -> - Map.get(updated, field) == value && Map.get(model, field) != value - end) - - {_module, condition} -> - condition.execute(model, updated) - end) - |> Enum.map(fn {module, _} -> module end) - end -end diff --git a/lib/momo/feature/naming.ex b/lib/momo/feature/naming.ex deleted file mode 100644 index ea2f2386..00000000 --- a/lib/momo/feature/naming.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Momo.Feature.Naming do - @moduledoc false - - def feature_module(caller) do - case caller |> Module.split() |> Enum.reverse() do - [_, kind | rest] - when kind in [ - "Commands", - "Handlers", - "Queries", - "Events", - "Subscriptions", - "Mappings", - "Flows" - ] -> - rest |> Enum.reverse() |> Module.concat() - - other -> - raise "Invalid module name #{inspect(caller)}: #{inspect(other)}" - end - end -end diff --git a/lib/momo/feature/parser.ex b/lib/momo/feature/parser.ex deleted file mode 100644 index 91dcacf1..00000000 --- a/lib/momo/feature/parser.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Momo.Feature.Parser do - @moduledoc false - @behaviour Diesel.Parser - - alias Momo.Feature - import Momo.Naming - - @impl true - def parse({:feature, _, children}, opts) do - caller_module = Keyword.fetch!(opts, :caller_module) - name = name(caller_module) - repo = repo(caller_module) - app = app(caller_module) - - %Feature{name: name, repo: repo, app: app} - |> with_modules(children, :scopes) - |> with_modules(children, :models) - |> with_modules(children, :commands) - |> with_modules(children, :handlers) - |> with_modules(children, :queries) - |> with_modules(children, :events) - |> with_modules(children, :flows) - |> with_modules(children, :subscriptions) - |> with_modules(children, :mappings) - |> with_modules(children, :values) - |> ensure_command_event_compatibility!() - end - - defp with_modules(feature, children, kind) do - mods = for {^kind, _, mods} <- children, do: mods - mods = List.flatten(mods) - - Map.put(feature, kind, mods) - end - - defp ensure_command_event_compatibility!(feature) do - for command <- feature.commands, event <- command.events() do - event_module = event.module - input = Enum.find(feature.mappings, &(&1 == event.mapping)) || command.returns() - input_fields = with fields when is_map(fields) <- input.fields(), do: Map.values(fields) - - for field when field.required <- event_module.fields() do - if input_fields |> Enum.find(&(&1.name == field.name)) |> is_nil() do - raise """ - Event #{inspect(event_module)} cannot be mapped from the output of command #{inspect(command)} because field #{inspect(field.name)} does not exist in #{inspect(input)}. - """ - end - end - end - - feature - end -end diff --git a/lib/momo/feature/policies.ex b/lib/momo/feature/policies.ex deleted file mode 100644 index fe523445..00000000 --- a/lib/momo/feature/policies.ex +++ /dev/null @@ -1,84 +0,0 @@ -# defmodule Momo.Feature.Policies do -# @moduledoc false - -# def resolve!(model, action, scopes) do -# for {role, policy} <- action.policies, into: %{} do -# {role, policy_with_scope!(model, action, role, policy, scopes)} -# end -# end - -# def reduce(policies, roles) do -# roles -# |> Enum.map(&Map.get(policies, &1)) -# |> Enum.reject(&is_nil/1) -# |> case do -# [] -> nil -# [policy] -> policy -# policies -> combine(policies, :one) -# end -# end - -# def combine(policies, op) when op in [:one, :all] do -# args = for policy <- policies, do: policy.scope - -# %Momo.Model.Policy{ -# scope: %Momo.Scopes.Scope{ -# expression: %Momo.Scopes.Expression{ -# op: op, -# args: args -# } -# } -# } -# end - -# defp policy_with_scope!(_model, _action, _role, %{scope: nil} = policy, _), do: policy - -# defp policy_with_scope!(model, action, role, policy, all_scopes) do -# case scope(all_scopes, policy.scope) do -# {:ok, scope} -> -# %{policy | scope: scope} - -# {:error, reason} -> -# raise """ -# Error resolving scope: - -# #{inspect(reason)} - -# in: - -# * action: #{inspect(action.name)} -# * model: #{inspect(model)} -# * role: #{inspect(role)} - -# Available scopes: - -# #{all_scopes |> Map.keys() |> inspect()} -# """ -# end -# end - -# defp scope(all_scopes, name) when is_atom(name) do -# case Map.get(all_scopes, name) do -# nil -> {:error, "unknown scope #{inspect(name)}"} -# scope -> {:ok, scope} -# end -# end - -# defp scope(all_scopes, {op, scopes}) do -# with scopes when is_list(scopes) <- -# Enum.reduce_while(scopes, [], fn scope, acc -> -# case scope(all_scopes, scope) do -# {:ok, scope} -> {:cont, [scope | acc]} -# {:error, _} = error -> {:halt, error} -# end -# end) do -# {:ok, -# %Momo.Scopes.Scope{ -# expression: %Momo.Scopes.Expression{ -# op: op, -# args: Enum.reverse(scopes) -# } -# }} -# end -# end -# end diff --git a/lib/momo/feature/scopes.ex b/lib/momo/feature/scopes.ex deleted file mode 100644 index 893c8366..00000000 --- a/lib/momo/feature/scopes.ex +++ /dev/null @@ -1,12 +0,0 @@ -# defmodule Momo.Feature.Scopes do -# @moduledoc false - -# @doc "Returns all the scopes defined for the given feature" -# def all(_feature) do -# [] -# # Enum.reduce(feature.scopes, %{}, fn scopes, acc -> -# # Map.merge(acc, scopes.scopes()) -# # end) -# end -# end -# # diff --git a/lib/momo/flow.ex b/lib/momo/flow.ex index 6ccd0d43..de657354 100644 --- a/lib/momo/flow.ex +++ b/lib/momo/flow.ex @@ -13,7 +13,7 @@ defmodule Momo.Flow do import Momo.Maps - defstruct [:fun_name, :create_model_fun_name, :feature, :model, :params, :event, :steps] + defstruct [:fun_name, :create_model_fun_name, :app, :model, :params, :event, :steps] defmodule Step do @moduledoc false @@ -24,19 +24,18 @@ defmodule Momo.Flow do Execute the given flow, with the given parameters and context. """ def execute(flow, params, _context) do - feature = flow.feature() - create_model_fun_name = flow.create_model_fun_name() + model = flow.model() count = flow.steps() |> Enum.count() attrs = params |> plain_map() |> Map.put(:steps_pending, count) - with {:ok, model} <- apply(feature, create_model_fun_name, [attrs]) do + with {:ok, result} <- model.create(attrs) do params = Jason.encode!(params) flow.steps() - |> Enum.map(&[command: &1.command, params: params, flow: flow, id: model.id]) + |> Enum.map(&[command: &1.command, params: params, flow: flow, id: result.id]) |> Momo.Job.schedule_all() - {:ok, model} + {:ok, result} else {:error, _} = error -> error other -> {:error, {:invalid_result, other}} @@ -51,8 +50,8 @@ defmodule Momo.Flow do model <- flow.model(), event <- flow.event(), {:ok, input} <- model.fetch(id), - {:ok, event} <- flow.feature().map(model, event, input) do - Momo.Feature.publish_events([event], step.feature()) + {:ok, event} <- flow.app().map(model, event, input) do + Momo.App.publish_events([event], step.app()) else n when n > 0 -> :ok {:error, _} = error -> error @@ -62,7 +61,7 @@ defmodule Momo.Flow do import Ecto.Query defp decrement_steps_pending(flow, id) do - repo = flow.feature().repo() + repo = flow.app().repo() model = flow.model() case model diff --git a/lib/momo/flow/generator/metadata.ex b/lib/momo/flow/generator/metadata.ex index 20f1c875..e0a41e1a 100644 --- a/lib/momo/flow/generator/metadata.ex +++ b/lib/momo/flow/generator/metadata.ex @@ -5,7 +5,7 @@ defmodule Momo.Flow.Generator.Metadata do @impl true def generate(flow, _opts) do quote do - def feature, do: unquote(flow.feature) + def app, do: unquote(flow.app) def fun_name, do: unquote(flow.fun_name) def create_model_fun_name, do: unquote(flow.create_model_fun_name) def model, do: unquote(flow.model) diff --git a/lib/momo/flow/parser.ex b/lib/momo/flow/parser.ex index 824c27b6..07d6387f 100644 --- a/lib/momo/flow/parser.ex +++ b/lib/momo/flow/parser.ex @@ -9,7 +9,7 @@ defmodule Momo.Flow.Parser do def parse({:flow, attrs, children}, opts) do caller = Keyword.fetch!(opts, :caller_module) - feature = feature_module(caller) + app = app(caller) model = Keyword.fetch!(attrs, :model) params = Keyword.get(attrs, :params, model) event = Keyword.fetch!(attrs, :publish) @@ -25,7 +25,7 @@ defmodule Momo.Flow.Parser do |> List.flatten() %Flow{ - feature: feature, + app: app, fun_name: fun_name, create_model_fun_name: create_model_fun_name, model: model, diff --git a/lib/momo/job.ex b/lib/momo/job.ex index c3bc4c00..4936911f 100644 --- a/lib/momo/job.ex +++ b/lib/momo/job.ex @@ -40,10 +40,9 @@ defmodule Momo.Job do ) do flow = Module.concat([flow]) command = Module.concat([command]) - feature = command.feature() with {:ok, params} <- Jason.decode(params), - {:ok, _} <- apply(feature, command.fun_name(), [params]), + {:ok, _} <- command.execute(params, %{authorization: :skip}), :ok <- flow.step_completed(id, command) do handle_success(flow: flow) else diff --git a/lib/momo/mapping.ex b/lib/momo/mapping.ex index 6b5b22be..9ca4dd33 100644 --- a/lib/momo/mapping.ex +++ b/lib/momo/mapping.ex @@ -17,7 +17,7 @@ defmodule Momo.Mapping do defstruct [:name, :expression] end - defstruct [:name, :feature, :from, :to, :fields] + defstruct [:name, :app, :from, :to, :fields] alias Momo.Evaluate diff --git a/lib/momo/mapping/generator/metadata.ex b/lib/momo/mapping/generator/metadata.ex index 2c8363a6..ea0dbe02 100644 --- a/lib/momo/mapping/generator/metadata.ex +++ b/lib/momo/mapping/generator/metadata.ex @@ -8,7 +8,7 @@ defmodule Momo.Mapping.Generator.Metadata do def from, do: unquote(mapping.from) def to, do: unquote(mapping.to) def fields, do: unquote(Macro.escape(mapping.fields)) - def feature, do: unquote(mapping.feature) + def app, do: unquote(mapping.app) def name, do: unquote(mapping.name) end end diff --git a/lib/momo/mapping/parser.ex b/lib/momo/mapping/parser.ex index 9b56562e..2e2a16c6 100644 --- a/lib/momo/mapping/parser.ex +++ b/lib/momo/mapping/parser.ex @@ -5,12 +5,12 @@ defmodule Momo.Mapping.Parser do alias Momo.Mapping alias Momo.Mapping.Field - import Momo.Feature.Naming + import Momo.Naming def parse({:mapping, attrs, children}, opts) do name = Keyword.fetch!(opts, :caller_module) caller = Keyword.fetch!(opts, :caller_module) - feature = feature_module(caller) + app = app(caller) from = Keyword.fetch!(attrs, :from) to = Keyword.fetch!(attrs, :to) @@ -25,7 +25,7 @@ defmodule Momo.Mapping.Parser do %Mapping{ name: name, - feature: feature, + app: app, from: from, to: to, fields: fields diff --git a/lib/momo/migrations.ex b/lib/momo/migrations.ex index 483688f2..d427da85 100644 --- a/lib/momo/migrations.ex +++ b/lib/momo/migrations.ex @@ -13,14 +13,14 @@ defmodule Momo.Migrations do |> Enum.map(&File.read!(&1)) end - def missing(existing, features) do + def missing(existing, app) do existing = existing |> Enum.map(&Code.string_to_quoted!(&1)) |> Enum.map(&Migration.decode/1) |> Enum.reject(& &1.skip) - new_state = state_from_features(features) + new_state = state_from_app(app) old_state = state_from_migrations(existing) next_version = next_version(existing) @@ -33,17 +33,10 @@ defmodule Momo.Migrations do |> Enum.reduce(State.new(), &Migration.aggregate/2) end - defp state_from_features(features) do - state = Enum.reduce(features, State.new(), &state_with_schema/2) - - features - |> Enum.flat_map(& &1.models()) + defp state_from_app(app) do + app.models() |> Enum.reject(& &1.virtual?()) - |> Enum.reduce(state, &state_with_model/2) - end - - defp state_with_schema(feature, state) do - State.add_schema(state, feature.name()) + |> Enum.reduce(State.new(), &state_with_model/2) end defp state_with_model(model, state) do @@ -55,14 +48,14 @@ defmodule Momo.Migrations do defp state_with_table(state, model) do table = Table.from_model(model) - State.add!(state, table.prefix, :tables, table) + State.add!(state, :tables, table) end defp state_with_constraints(state, model) do model.parents() |> Enum.map(&Constraint.from_relation/1) |> Enum.reduce(state, fn constraint, state -> - State.add!(state, constraint.prefix, :constraints, constraint) + State.add!(state, :constraints, constraint) end) end @@ -70,7 +63,7 @@ defmodule Momo.Migrations do model.keys() |> Enum.map(&Index.from_key/1) |> Enum.reduce(state, fn index, state -> - State.add!(state, index.prefix, :indexes, index) + State.add!(state, :indexes, index) end) end diff --git a/lib/momo/migrations/constraint.ex b/lib/momo/migrations/constraint.ex index 45bb058d..e48c6431 100644 --- a/lib/momo/migrations/constraint.ex +++ b/lib/momo/migrations/constraint.ex @@ -7,7 +7,6 @@ defmodule Momo.Migrations.Constraint do defstruct [ :name, :table, - :prefix, :column, :target, type: :uuid, @@ -19,7 +18,6 @@ defmodule Momo.Migrations.Constraint do new( table: rel.model.table_name(), - prefix: rel.model.feature().name(), column: rel.column_name, target: target_model.table_name(), type: target_model.primary_key().storage diff --git a/lib/momo/migrations/index.ex b/lib/momo/migrations/index.ex index ce2190ce..ff0aa261 100644 --- a/lib/momo/migrations/index.ex +++ b/lib/momo/migrations/index.ex @@ -5,14 +5,13 @@ defmodule Momo.Migrations.Index do @type t() :: %__MODULE__{} - defstruct [:name, :table, :prefix, columns: [], unique: false] + defstruct [:name, :table, columns: [], unique: false] def from_key(%Key{} = key) do table_name = key.model.table_name() column_names = Enum.map(key.fields, & &1.column_name) - prefix = key.model.feature().name() - from_opts(unique: key.unique?, columns: column_names, table: table_name, prefix: prefix) + from_opts(unique: key.unique?, columns: column_names, table: table_name, prefix: nil) end def from_opts(opts) do diff --git a/lib/momo/migrations/migration.ex b/lib/momo/migrations/migration.ex index 2ecffa2b..9ad9e61b 100644 --- a/lib/momo/migrations/migration.ex +++ b/lib/momo/migrations/migration.ex @@ -4,15 +4,13 @@ defmodule Momo.Migrations.Migration do import Momo.Naming @mutations [ - Momo.Migrations.Step.CreateSchema, Momo.Migrations.Step.CreateTable, Momo.Migrations.Step.AlterTable, Momo.Migrations.Step.CreateConstraint, Momo.Migrations.Step.CreateIndex, Momo.Migrations.Step.DropConstraint, Momo.Migrations.Step.DropIndex, - Momo.Migrations.Step.DropTable, - Momo.Migrations.Step.DropSchema + Momo.Migrations.Step.DropTable ] defstruct [ diff --git a/lib/momo/migrations/schema.ex b/lib/momo/migrations/schema.ex deleted file mode 100644 index 93d393a8..00000000 --- a/lib/momo/migrations/schema.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule Momo.Migrations.Schema do - @moduledoc """ - Keeps tables, constraints and indexes of the same database schema together, during migration parsing and diffing - """ - - @type t :: %__MODULE__{} - - defstruct [:name, tables: %{}, constraints: %{}, indexes: %{}] - - def new(name), do: %__MODULE__{name: name} - - def has?(schema, kind, name) do - schema - |> Map.fetch!(kind) - |> Map.has_key?(name) - end - - def find(schema, kind, name) do - schema - |> Map.fetch!(kind) - |> Map.get(name) - end - - def find!(schema, kind, name) do - schema - |> Map.fetch!(kind) - |> Map.fetch!(name) - end - - def add!(schema, kind, item) do - if has?(schema, kind, item.name) do - raise "Cannot add #{inspect(item)} into #{kind} of schema #{schema.name} (already exists)" - else - replace!(schema, kind, item) - end - end - - def remove!(schema, kind, item) do - if has?(schema, kind, item.name) do - items = schema |> Map.fetch!(kind) |> Map.drop([item.name]) - Map.put(schema, kind, items) - else - raise "Cannot remove #{inspect(item)} from #{kind} of schema #{schema.name} (does not exist)" - end - end - - def replace!(schema, kind, item) do - items = - schema - |> Map.fetch!(kind) - |> Map.put(item.name, item) - - Map.put(schema, kind, items) - end -end diff --git a/lib/momo/migrations/state.ex b/lib/momo/migrations/state.ex index 4c0e9a69..d0f5428d 100644 --- a/lib/momo/migrations/state.ex +++ b/lib/momo/migrations/state.ex @@ -1,106 +1,55 @@ defmodule Momo.Migrations.State do @moduledoc """ - Manages a set of schemas during migrations + Keeps tables, constraints and indexes of the same database schema together, during migration parsing and diffing """ - alias Momo.Migrations.Schema - @type t :: %__MODULE__{} - defstruct schemas: %{} + defstruct tables: %{}, constraints: %{}, indexes: %{} def new, do: %__MODULE__{} - def add_schema(state, name) do - schema = Schema.new(name) - schemas = Map.put(state.schemas, name, schema) - - %{state | schemas: schemas} - end - - def remove_schema(state, name) do - schemas = Map.drop(state.schemas, [name]) - - %{state | schemas: schemas} - end - - def has_schema?(state, name), do: Map.has_key?(state.schemas, name) - - def has?(state, schema, kind, name) do - case Map.get(state.schemas, schema) do - nil -> false - schema -> Schema.has?(schema, kind, name) - end - end - - def find(state, schema, kind, name) do - with_schema(state, schema, fn schema -> - Schema.find(schema, kind, name) - end) + def has?(schema, kind, name) do + schema + |> Map.fetch!(kind) + |> Map.has_key?(name) end - def find!(state, schema, kind, name) do - with_schema!(state, schema, fn schema -> - Schema.find!(schema, kind, name) - end) + def find(schema, kind, name) do + schema + |> Map.fetch!(kind) + |> Map.get(name) end - def add!(state, schema, kind, item) do - with_new_or_existing_schema(state, schema, fn schema -> - schema = Schema.add!(schema, kind, item) - schemas = Map.put(state.schemas, schema.name, schema) - - %{state | schemas: schemas} - end) - end - - def remove!(state, schema, kind, item) do - with_schema!(state, schema, fn schema -> - schema = Schema.remove!(schema, kind, item) - schemas = Map.put(state.schemas, schema.name, schema) - - %{state | schemas: schemas} - end) + def find!(schema, kind, name) do + schema + |> Map.fetch!(kind) + |> Map.fetch!(name) end - def replace!(state, schema, kind, item) do - with_schema!(state, schema, fn schema -> - schema = Schema.replace!(schema, kind, item) - schemas = Map.put(state.schemas, schema.name, schema) - - %{state | schemas: schemas} - end) - end - - defp with_schema!(state, schema, fun) do - case Map.get(state.schemas, schema) do - nil -> - schemas = state.schemas |> Map.keys() |> inspect() - schema = inspect(schema) - raise "No such schema #{schema} in #{schemas}" - - schema -> - fun.(schema) + def add!(schema, kind, item) do + if has?(schema, kind, item.name) do + raise "Cannot add #{inspect(item)} into #{kind} of schema #{schema.name} (already exists)" + else + replace!(schema, kind, item) end end - defp with_new_or_existing_schema(state, schema, fun) do - case Map.get(state.schemas, schema) do - nil -> - state - |> add_schema(schema) - |> Map.fetch!(:schemas) - |> Map.fetch!(schema) - |> fun.() - - schema -> - fun.(schema) + def remove!(schema, kind, item) do + if has?(schema, kind, item.name) do + items = schema |> Map.fetch!(kind) |> Map.drop([item.name]) + Map.put(schema, kind, items) + else + raise "Cannot remove #{inspect(item)} from #{kind} of schema #{schema.name} (does not exist)" end end - defp with_schema(state, schema, fun) do - with %Schema{} = schema <- Map.get(state.schemas, schema) do - fun.(schema) - end + def replace!(schema, kind, item) do + items = + schema + |> Map.fetch!(kind) + |> Map.put(item.name, item) + + Map.put(schema, kind, items) end end diff --git a/lib/momo/migrations/step/alter_table.ex b/lib/momo/migrations/step/alter_table.ex index 71af865d..75361ab9 100644 --- a/lib/momo/migrations/step/alter_table.ex +++ b/lib/momo/migrations/step/alter_table.ex @@ -6,26 +6,23 @@ defmodule Momo.Migrations.Step.AlterTable do import Momo.Naming - defstruct [:table, :prefix, add: %{}, remove: %{}, modify: %{}] + defstruct [:table, add: %{}, remove: %{}, modify: %{}] @impl true - def decode({:alter, _, [{:table, _, [table, opts]}, [do: {:__block__, _, columns}]]}) + def decode({:alter, _, [{:table, _, [table]}, [do: {:__block__, _, columns}]]}) when is_list(columns) do - prefix = Keyword.fetch!(opts, :prefix) columns = columns |> Enum.map(&decode/1) |> Enum.reject(&is_nil/1) - new(table, prefix, columns) + new(table, columns) end - def decode({:alter, _, [{:table, _, [table, opts]}, [do: column]]}) do - prefix = Keyword.fetch!(opts, :prefix) - + def decode({:alter, _, [{:table, _, [table]}, [do: column]]}) do columns = case decode(column) do nil -> [] column -> [column] end - new(table, prefix, columns) + new(table, columns) end def decode({:add, _, col}), do: {:add, Column.new(col)} @@ -35,14 +32,13 @@ defmodule Momo.Migrations.Step.AlterTable do @impl true def encode(step) do - opts = [prefix: step.prefix] add = step.add |> Map.values() |> Enum.map(&{:add, [line: 1], Column.encode(&1)}) remove = step.remove |> Map.values() |> Enum.map(&{:remove, [line: 1], [&1.name]}) modify = step.modify |> Map.values() |> Enum.map(&{:modify, [line: 1], Column.encode(&1)}) {:alter, [line: 1], [ - {:table, [line: 1], [step.table, opts]}, + {:table, [line: 1], [step.table]}, [do: {:__block__, [], add ++ remove ++ modify}] ]} end @@ -51,17 +47,16 @@ defmodule Momo.Migrations.Step.AlterTable do def aggregate(step, state) do table = state - |> State.find!(step.prefix, :tables, step.table) + |> State.find!(:tables, step.table) |> apply_changes(step) - State.replace!(state, table.prefix, :tables, table) + State.replace!(state, :tables, table) end @impl true def diff(old_state, new_state) do - for {schema_name, schema} <- new_state.schemas, - {table_name, table} <- schema.tables do - with %Table{} = old_table <- State.find(old_state, schema_name, :tables, table_name) do + for {table_name, table} <- new_state.tables do + with %Table{} = old_table <- State.find(old_state, :tables, table_name) do diff_tables(old_table, table) else _ -> nil @@ -76,7 +71,7 @@ defmodule Momo.Migrations.Step.AlterTable do case map_size(add) + map_size(remove) + map_size(modify) do 0 -> nil - _ -> new(new_table.name, new_table.prefix, add, remove, modify) + _ -> new(new_table.name, add, remove, modify) end end @@ -125,7 +120,7 @@ defmodule Momo.Migrations.Step.AlterTable do defp modified_columns(old_table, new_table) do new_columns = Map.keys(new_table.columns) old_columns = Map.keys(old_table.columns) - in_common = new_columns -- new_columns -- old_columns + in_common = new_columns -- (new_columns -- old_columns) in_common |> Enum.map(fn name -> @@ -138,8 +133,8 @@ defmodule Momo.Migrations.Step.AlterTable do |> indexed() end - defp new(table, prefix, columns) do - step = %__MODULE__{table: table, prefix: prefix} + defp new(table, columns) do + step = %__MODULE__{table: table} Enum.reduce(columns, step, fn {bag, column}, step -> columns = step |> Map.get(bag) |> Map.put(column.name, column) @@ -147,7 +142,7 @@ defmodule Momo.Migrations.Step.AlterTable do end) end - defp new(table, prefix, add, remove, modify) do - struct(__MODULE__, table: table, prefix: prefix, add: add, remove: remove, modify: modify) + defp new(table, add, remove, modify) do + struct(__MODULE__, table: table, add: add, remove: remove, modify: modify) end end diff --git a/lib/momo/migrations/step/create_constraint.ex b/lib/momo/migrations/step/create_constraint.ex index 22ea596e..4547e7b2 100644 --- a/lib/momo/migrations/step/create_constraint.ex +++ b/lib/momo/migrations/step/create_constraint.ex @@ -10,15 +10,13 @@ defmodule Momo.Migrations.Step.CreateConstraint do def decode( {:alter, _, [ - {:table, _, [table, table_opts]}, + {:table, _, [table]}, [do: {:modify, _, [column, {:references, _, [other, opts]}]}] ]} ) do - prefix = Keyword.fetch!(table_opts, :prefix) - constraint = opts - |> Keyword.merge(table: table, prefix: prefix, column: column, target: other) + |> Keyword.merge(table: table, column: column, target: other) |> Constraint.new() %__MODULE__{constraint: constraint} @@ -28,11 +26,9 @@ defmodule Momo.Migrations.Step.CreateConstraint do @impl true def encode(step) do - opts = [prefix: step.constraint.prefix] - {:alter, [line: 1], [ - {:table, [line: 1], [step.constraint.table, opts]}, + {:table, [line: 1], [step.constraint.table]}, [ do: {:modify, [line: 1], @@ -53,13 +49,12 @@ defmodule Momo.Migrations.Step.CreateConstraint do @impl true def aggregate(step, state), - do: State.add!(state, step.constraint.prefix, :constraints, step.constraint) + do: State.add!(state, :constraints, step.constraint) @impl true def diff(old_state, new_state) do - for {schema_name, schema} <- new_state.schemas, - {constraint_name, constraint} <- schema.constraints do - if !State.has?(old_state, schema_name, :constraints, constraint_name) do + for {constraint_name, constraint} <- new_state.constraints do + if !State.has?(old_state, :constraints, constraint_name) do %__MODULE__{constraint: constraint} else nil diff --git a/lib/momo/migrations/step/create_index.ex b/lib/momo/migrations/step/create_index.ex index cc4ed5e6..4407de81 100644 --- a/lib/momo/migrations/step/create_index.ex +++ b/lib/momo/migrations/step/create_index.ex @@ -9,20 +9,18 @@ defmodule Momo.Migrations.Step.CreateIndex do @impl true def decode({:create, _, [{:unique_index, _, [table, columns, opts]}]}) do name = Keyword.fetch!(opts, :name) - prefix = Keyword.fetch!(opts, :prefix) index = - Index.from_opts(table: table, prefix: prefix, columns: columns, name: name, unique: true) + Index.from_opts(table: table, columns: columns, name: name, unique: true) %__MODULE__{index: index} end def decode({:create, _, [{:index, _, [table, columns, opts]}]}) do name = Keyword.fetch!(opts, :name) - prefix = Keyword.fetch!(opts, :prefix) index = - Index.from_opts(table: table, prefix: prefix, columns: columns, name: name, unique: false) + Index.from_opts(table: table, columns: columns, name: name, unique: false) %__MODULE__{index: index} end @@ -32,7 +30,7 @@ defmodule Momo.Migrations.Step.CreateIndex do @impl true def encode(%__MODULE__{} = step) do kind = if step.index.unique, do: :unique_index, else: :index - opts = [name: step.index.name, prefix: step.index.prefix] + opts = [name: step.index.name] {:create, [line: 1], [ @@ -42,12 +40,12 @@ defmodule Momo.Migrations.Step.CreateIndex do @impl true def aggregate(%__MODULE__{} = step, state), - do: State.add!(state, step.index.prefix, :indexes, step.index) + do: State.add!(state, :indexes, step.index) @impl true def diff(old_state, new_state) do - for {schema_name, schema} <- new_state.schemas, {index_name, index} <- schema.indexes do - if !State.has?(old_state, schema_name, :indexes, index_name) do + for {index_name, index} <- new_state.indexes do + if !State.has?(old_state, :indexes, index_name) do %__MODULE__{index: index} else nil diff --git a/lib/momo/migrations/step/create_schema.ex b/lib/momo/migrations/step/create_schema.ex deleted file mode 100644 index c7afc71f..00000000 --- a/lib/momo/migrations/step/create_schema.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Momo.Migrations.Step.CreateSchema do - @moduledoc false - @behaviour Momo.Migrations.Step - - alias Momo.Migrations.State - - defstruct [:schema] - - @impl true - def decode({:execute, _, ["CREATE SCHEMA " <> schema]}) do - schema = String.to_atom(schema) - - %__MODULE__{schema: schema} - end - - def decode(_), do: nil - - @impl true - def encode(step), do: {:execute, [line: 1], ["CREATE SCHEMA #{step.schema}"]} - - @impl true - def aggregate(step, state), do: State.add_schema(state, step.schema) - - @impl true - def diff(old_state, new_state) do - for {schema_name, _} <- new_state.schemas do - if !State.has_schema?(old_state, schema_name) do - %__MODULE__{schema: schema_name} - else - nil - end - end - end -end diff --git a/lib/momo/migrations/step/create_table.ex b/lib/momo/migrations/step/create_table.ex index fd3d11c1..fc6bd330 100644 --- a/lib/momo/migrations/step/create_table.ex +++ b/lib/momo/migrations/step/create_table.ex @@ -11,18 +11,16 @@ defmodule Momo.Migrations.Step.CreateTable do defstruct [:table] @impl true - def decode({:create, _, [{:table, _, [name, opts]}, [do: {:__block__, _, columns}]]}) do - prefix = Keyword.fetch!(opts, :prefix) + def decode({:create, _, [{:table, _, [name, _opts]}, [do: {:__block__, _, columns}]]}) do columns = columns |> Column.decode() |> indexed() - table = %Table{name: name, prefix: prefix, columns: columns} + table = %Table{name: name, columns: columns} %__MODULE__{table: table} end - def decode({:create, _, [{:table, _, [name, opts]}, [do: column]]}) do - prefix = Keyword.fetch!(opts, :prefix) + def decode({:create, _, [{:table, _, [name, _opts]}, [do: column]]}) do columns = indexed([Column.decode(column)]) - table = %Table{name: name, prefix: prefix, columns: columns} + table = %Table{name: name, columns: columns} %__MODULE__{table: table} end @@ -31,7 +29,7 @@ defmodule Momo.Migrations.Step.CreateTable do @impl true def encode(%__MODULE__{} = step) do - opts = [prefix: step.table.prefix, primary_key: false] + opts = [primary_key: false] columns = step.table.columns @@ -50,12 +48,12 @@ defmodule Momo.Migrations.Step.CreateTable do end @impl true - def aggregate(step, state), do: State.add!(state, step.table.prefix, :tables, step.table) + def aggregate(step, state), do: State.add!(state, :tables, step.table) @impl true def diff(old_state, new_state) do - for {schema_name, schema} <- new_state.schemas, {table_name, table} <- schema.tables do - if !State.has?(old_state, schema_name, :tables, table_name) do + for {table_name, table} <- new_state.tables do + if !State.has?(old_state, :tables, table_name) do %__MODULE__{table: table} else nil diff --git a/lib/momo/migrations/step/drop_constraint.ex b/lib/momo/migrations/step/drop_constraint.ex index 0248bfad..2c7472d9 100644 --- a/lib/momo/migrations/step/drop_constraint.ex +++ b/lib/momo/migrations/step/drop_constraint.ex @@ -7,9 +7,8 @@ defmodule Momo.Migrations.Step.DropConstraint do defstruct [:constraint] @impl true - def decode({:drop_if_exists, _, [{:constraint, _, [table, name, opts]}]}) do - prefix = Keyword.fetch!(opts, :prefix) - constraint = Constraint.new(name: name, table: table, prefix: prefix) + def decode({:drop_if_exists, _, [{:constraint, _, [table, name]}]}) do + constraint = Constraint.new(name: name, table: table) %__MODULE__{constraint: constraint} end @@ -18,21 +17,18 @@ defmodule Momo.Migrations.Step.DropConstraint do @impl true def encode(step) do - opts = [prefix: step.constraint.prefix] - {:drop_if_exists, [line: 1], - [{:constraint, [line: 1], [step.constraint.table, step.constraint.name, opts]}]} + [{:constraint, [line: 1], [step.constraint.table, step.constraint.name]}]} end @impl true def aggregate(step, state), - do: State.remove!(state, step.constraint.prefix, :constraints, step.constraint) + do: State.remove!(state, :constraints, step.constraint) @impl true def diff(old_state, new_state) do - for {schema_name, schema} <- old_state.schemas, - {constraint_name, constraint} <- schema.constraints do - if !State.has?(new_state, schema_name, :constraints, constraint_name) do + for {constraint_name, constraint} <- old_state.constraints do + if !State.has?(new_state, :constraints, constraint_name) do %__MODULE__{constraint: constraint} else nil diff --git a/lib/momo/migrations/step/drop_index.ex b/lib/momo/migrations/step/drop_index.ex index 1d0b6e82..54a77f54 100644 --- a/lib/momo/migrations/step/drop_index.ex +++ b/lib/momo/migrations/step/drop_index.ex @@ -8,8 +8,7 @@ defmodule Momo.Migrations.Step.DropIndex do @impl true def decode({:drop_if_exists, _, [{:index, _, [table, _, opts]}]}) do name = Keyword.fetch!(opts, :name) - prefix = Keyword.fetch!(opts, :prefix) - index = Index.from_opts(name: name, table: table, prefix: prefix) + index = Index.from_opts(name: name, table: table) %__MODULE__{index: index} end @@ -18,19 +17,19 @@ defmodule Momo.Migrations.Step.DropIndex do @impl true def encode(%__MODULE__{index: index}) do - opts = [name: index.name, prefix: index.prefix] + opts = [name: index.name] {:drop_if_exists, [line: 1], [{:index, [line: 1], [index.table, [], opts]}]} end @impl true def aggregate(%__MODULE__{} = step, state), - do: State.remove!(state, step.index.prefix, :indexes, step.index) + do: State.remove!(state, :indexes, step.index) @impl true def diff(old_state, new_state) do - for {schema_name, schema} <- old_state.schemas, {index_name, index} <- schema.indexes do - if !State.has?(new_state, schema_name, :indexes, index_name) do + for {index_name, index} <- old_state.indexes do + if !State.has?(new_state, :indexes, index_name) do %__MODULE__{index: index} else nil diff --git a/lib/momo/migrations/step/drop_schema.ex b/lib/momo/migrations/step/drop_schema.ex deleted file mode 100644 index 521bef8f..00000000 --- a/lib/momo/migrations/step/drop_schema.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Momo.Migrations.Step.DropSchema do - @moduledoc false - @behaviour Momo.Migrations.Step - - alias Momo.Migrations.State - - defstruct [:schema] - - @impl true - def decode({:execute, _, ["DROP SCHEMA " <> schema]}) do - schema = String.to_atom(schema) - - %__MODULE__{schema: schema} - end - - def decode(_), do: nil - - @impl true - def encode(step), do: {:execute, [line: 1], ["DROP SCHEMA #{step.schema}"]} - - @impl true - def aggregate(step, state), do: State.remove_schema(state, step.schema) - - @impl true - def diff(old_state, new_state) do - for {schema_name, _} <- old_state.schemas do - if !State.has_schema?(new_state, schema_name) do - %__MODULE__{schema: schema_name} - else - nil - end - end - end -end diff --git a/lib/momo/migrations/step/drop_table.ex b/lib/momo/migrations/step/drop_table.ex index 319d3070..ad3652b4 100644 --- a/lib/momo/migrations/step/drop_table.ex +++ b/lib/momo/migrations/step/drop_table.ex @@ -7,9 +7,8 @@ defmodule Momo.Migrations.Step.DropTable do defstruct [:table] @impl true - def decode({:drop_if_exists, _, [{:table, _, [name, opts]}]}) do - prefix = Keyword.fetch!(opts, :prefix) - table = %Table{prefix: prefix, name: name} + def decode({:drop_if_exists, _, [{:table, _, [name]}]}) do + table = %Table{name: name} %__MODULE__{table: table} end @@ -20,17 +19,17 @@ defmodule Momo.Migrations.Step.DropTable do def encode(%__MODULE__{} = step) do {:drop_if_exists, [line: 1], [ - {:table, [line: 1], [step.table.name, [prefix: step.table.prefix]]} + {:table, [line: 1], [step.table.name]} ]} end @impl true - def aggregate(step, state), do: State.remove!(state, step.table.prefix, :tables, step.table) + def aggregate(step, state), do: State.remove!(state, :tables, step.table) @impl true def diff(old_state, new_state) do - for {schema_name, schema} <- old_state.schemas, {table_name, table} <- schema.tables do - if !State.has?(new_state, schema_name, :tables, table_name) do + for {table_name, table} <- old_state.tables do + if !State.has?(new_state, :tables, table_name) do %__MODULE__{table: table} else nil diff --git a/lib/momo/migrations/table.ex b/lib/momo/migrations/table.ex index 2ff1b75c..c4e6f94b 100644 --- a/lib/momo/migrations/table.ex +++ b/lib/momo/migrations/table.ex @@ -15,7 +15,6 @@ defmodule Momo.Migrations.Table do @timestamps [:inserted_at, :updated_at] def from_model(model) do - prefix = model.feature().name() table_name = model.table_name() attribute_columns = @@ -27,7 +26,7 @@ defmodule Momo.Migrations.Table do columns = indexed(attribute_columns ++ parent_columns) - %__MODULE__{name: table_name, prefix: prefix, columns: columns} + %__MODULE__{name: table_name, prefix: nil, columns: columns} end def column!(table, name) do diff --git a/lib/momo/model.ex b/lib/momo/model.ex index 0cad0521..61302286 100644 --- a/lib/momo/model.ex +++ b/lib/momo/model.ex @@ -24,7 +24,8 @@ defmodule Momo.Model do defstruct [ :module, - :feature, + :app, + :prefix, :repo, :name, :plural, diff --git a/lib/momo/model/generator/changesets.ex b/lib/momo/model/generator/changesets.ex index 156d0e8f..3c33112f 100644 --- a/lib/momo/model/generator/changesets.ex +++ b/lib/momo/model/generator/changesets.ex @@ -9,6 +9,7 @@ defmodule Momo.Model.Generator.Changesets do [ validate_changeset(model), insert_changeset(model), + batch_insert_changeset(model), update_changeset(model), delete_changeset(model) ] @@ -57,6 +58,34 @@ defmodule Momo.Model.Generator.Changesets do end end + defp batch_insert_changeset(model) do + primary_key_constraint = String.to_atom("#{model.plural}_pkey") + unique_constraints = unique_constraints(model) + inclusion_validations = inclusion_validations(model) + parents_contraints = parents_constraints(model) + uuid_validations = uuid_validations(model) + computed_changes = computed_changes(model) + + quote location: :keep do + def batch_insert_changeset(attrs, opts) do + changes = + %__MODULE__{} + |> cast(attrs, @fields_on_insert) + |> maybe_add_id() + |> maybe_add_inserted_at() + |> maybe_add_updated_at() + |> validate_required(@required_fields) + |> unique_constraint([:id], name: unquote(primary_key_constraint)) + + unquote(computed_changes) + unquote_splicing(uuid_validations) + unquote_splicing(unique_constraints) + unquote_splicing(inclusion_validations) + unquote_splicing(parents_contraints) + end + end + end + defp update_changeset(model) do inclusion_validations = inclusion_validations(model) parents_contraints = parents_constraints(model) @@ -132,8 +161,8 @@ defmodule Momo.Model.Generator.Changesets do end defp inclusion_validations(model) do - for %{name: name, in: enum} when not is_nil(nil) <- model.attributes do - allowed_values = enum.values() + for %{name: name, in: enum} when not is_nil(enum) and enum != [] <- model.attributes do + allowed_values = if is_list(enum), do: enum, else: enum.values() quote do changes = validate_inclusion(changes, unquote(name), unquote(allowed_values)) diff --git a/lib/momo/model/generator/create_function.ex b/lib/momo/model/generator/create_function.ex index ac4f5352..1696cd06 100644 --- a/lib/momo/model/generator/create_function.ex +++ b/lib/momo/model/generator/create_function.ex @@ -9,8 +9,9 @@ defmodule Momo.Model.Generator.CreateFunction do def generate(model, _) do [ with_defaults(), - with_map_args(model), + with_struct_args(model), with_keyword_args(model), + with_map_args(model), batch_fun(model) ] end @@ -24,19 +25,19 @@ defmodule Momo.Model.Generator.CreateFunction do defp with_map_args(model) do conflict_opts = on_conflict_opts(model) || [] - quote do + quote location: :keep do def create(attrs, opts) when is_map(attrs) do opts = Keyword.merge(unquote(conflict_opts), opts) %__MODULE__{} |> insert_changeset(attrs, opts) - |> unquote(model.feature).repo().insert(opts) + |> __MODULE__.app().repo().insert(opts) end end end defp with_keyword_args(_model) do - quote do + quote location: :keep do def create(attrs, opts) when is_list(attrs) do attrs |> Map.new() @@ -45,25 +46,37 @@ defmodule Momo.Model.Generator.CreateFunction do end end + defp with_struct_args(_model) do + quote location: :keep do + def create(attrs, opts) when is_struct(attrs) do + attrs + |> Map.from_struct() + |> create(opts) + end + end + end + defp batch_fun(model) do conflict_opts = on_conflict_opts(model) || [] - quote do + quote location: :keep do def create_many(items, opts \\ []) when is_list(items) do opts = Keyword.merge(unquote(conflict_opts), opts) now = DateTime.utc_now() + unique_by = opts[:unique_by] || :id items = - for item <- items do + items + |> Enum.map(fn item -> item - |> Map.new() - |> atom_keys() - |> Map.put_new_lazy(:id, &Ecto.UUID.generate/0) - |> Map.put_new(:inserted_at, now) - |> Map.put_new(:updated_at, now) - end + |> batch_insert_changeset(opts) + |> apply_changes() + |> Map.from_struct() + |> Map.drop(@relation_field_names ++ [:__meta__]) + end) + |> Enum.uniq_by(&Map.fetch!(&1, unique_by)) - unquote(model.feature).repo().insert_all(__MODULE__, items, opts) + @repo.insert_all(__MODULE__, items, opts) :ok end diff --git a/lib/momo/model/generator/delete_function.ex b/lib/momo/model/generator/delete_function.ex index d031cfe8..9a0840c8 100644 --- a/lib/momo/model/generator/delete_function.ex +++ b/lib/momo/model/generator/delete_function.ex @@ -3,13 +3,13 @@ defmodule Momo.Model.Generator.DeleteFunction do @behaviour Diesel.Generator @impl true - def generate(model, _) do + def generate(_model, _) do quote do def delete(model) do with {:ok, _} <- model |> delete_changeset() - |> unquote(model.feature).repo().delete(), + |> __MODULE__.app().repo().delete(), do: :ok end end diff --git a/lib/momo/model/generator/ecto_schema.ex b/lib/momo/model/generator/ecto_schema.ex index 0935ca70..3a9c9c70 100644 --- a/lib/momo/model/generator/ecto_schema.ex +++ b/lib/momo/model/generator/ecto_schema.ex @@ -2,8 +2,6 @@ defmodule Momo.Model.Generator.EctoSchema do @moduledoc false @behaviour Diesel.Generator - alias Momo.Naming - @impl true def generate(model, _) do quote do @@ -12,7 +10,6 @@ defmodule Momo.Model.Generator.EctoSchema do import Ecto.Query unquote(primary_key(model)) - unquote(prefix(model)) schema unquote(table_name(model)) do (unquote_splicing( @@ -40,14 +37,6 @@ defmodule Momo.Model.Generator.EctoSchema do end end - def prefix(model) do - prefix = Naming.name(model.feature) - - quote do - @schema_prefix unquote(prefix) - end - end - defp attributes(model) do attrs = model.attributes diff --git a/lib/momo/model/generator/edit_function.ex b/lib/momo/model/generator/edit_function.ex index 771d3280..0bcac461 100644 --- a/lib/momo/model/generator/edit_function.ex +++ b/lib/momo/model/generator/edit_function.ex @@ -10,14 +10,14 @@ defmodule Momo.Model.Generator.EditFunction do ] end - defp with_map_args(model) do + defp with_map_args(_model) do quote do def edit(model, attrs, opts \\ []) def edit(model, attrs, opts) when is_map(attrs) do model |> update_changeset(attrs, opts) - |> unquote(model.feature).repo().update() + |> __MODULE__.app().repo().update() end end end diff --git a/lib/momo/model/generator/fetch_function.ex b/lib/momo/model/generator/fetch_function.ex index d6f39dd1..7a313ac0 100644 --- a/lib/momo/model/generator/fetch_function.ex +++ b/lib/momo/model/generator/fetch_function.ex @@ -18,7 +18,7 @@ defmodule Momo.Model.Generator.FetchFunction do defp fetch_function(model) do preloads = default_preloads(model) - quote do + quote location: :keep do def fetch(id, opts \\ []) do preload = Keyword.get(opts, :preload, unquote(preloads)) diff --git a/lib/momo/model/generator/metadata.ex b/lib/momo/model/generator/metadata.ex index c3cbfa08..bbee8d72 100644 --- a/lib/momo/model/generator/metadata.ex +++ b/lib/momo/model/generator/metadata.ex @@ -17,8 +17,9 @@ defmodule Momo.Model.Generator.Metadata do parent_field_names = Enum.map(parents, & &1.name) attribute_field_names = Enum.map(attributes, & &1.name) field_names = attribute_field_names ++ parent_field_names + relation_field_names = Enum.map(relations, & &1.name) - quote do + quote location: :keep do @repo unquote(model.repo) def repo, do: @repo @@ -26,6 +27,7 @@ defmodule Momo.Model.Generator.Metadata do @parents unquote(Macro.escape(parents)) @attribute_field_names unquote(attribute_field_names) @parent_field_names unquote(parent_field_names) + @relation_field_names unquote(relation_field_names) @field_names unquote(field_names) @children unquote(Macro.escape(children)) @fields unquote(Macro.escape(fields)) @@ -33,7 +35,7 @@ defmodule Momo.Model.Generator.Metadata do @keys unquote(Macro.escape(keys)) @actions unquote(Macro.escape(actions)) - def feature, do: unquote(model.feature) + def app, do: unquote(model.app) def name, do: unquote(model.name) def plural, do: unquote(model.plural) def table_name, do: unquote(model.table_name) @@ -50,6 +52,7 @@ defmodule Momo.Model.Generator.Metadata do def parent_field_names, do: @parent_field_names def attribute_field_names, do: @attribute_field_names + def relation_field_names, do: @relation_field_names def field_names, do: @field_names def field(name) when is_atom(name) do diff --git a/lib/momo/model/helpers.ex b/lib/momo/model/helpers.ex index e81902af..af32a9e3 100644 --- a/lib/momo/model/helpers.ex +++ b/lib/momo/model/helpers.ex @@ -28,4 +28,22 @@ defmodule Momo.Model.Helpers do Ecto.Changeset.put_change(changeset, :id, Ecto.UUID.generate()) end end + + def maybe_add_inserted_at(changeset) do + if changed?(changeset, :inserted_at) do + changeset + else + now = DateTime.utc_now() + Ecto.Changeset.put_change(changeset, :inserted_at, now) + end + end + + def maybe_add_updated_at(changeset) do + if changed?(changeset, :updated_at) do + changeset + else + now = DateTime.utc_now() + Ecto.Changeset.put_change(changeset, :updated_at, now) + end + end end diff --git a/lib/momo/model/parser.ex b/lib/momo/model/parser.ex index 422e0e78..210df00c 100644 --- a/lib/momo/model/parser.ex +++ b/lib/momo/model/parser.ex @@ -28,11 +28,12 @@ defmodule Momo.Model.Parser do end defp model(caller, attrs) do - feature = feature(caller) + app = app(caller) %Model{ - feature: feature, - repo: repo(feature), + app: app, + prefix: prefix(app), + repo: repo(app), name: name(caller), plural: plural(caller), module: caller, @@ -59,7 +60,7 @@ defmodule Momo.Model.Parser do computation = if opts[:computed] do computation = Macro.camelize("#{model.name}_#{attr_name}") - Module.concat([model.feature, Computations, computation]) + Module.concat([model.prefix, Computations, computation]) end %Attribute{ @@ -94,7 +95,6 @@ defmodule Momo.Model.Parser do parent_module = relation_target_module(attrs, children) preloaded = Keyword.get(attrs, :preloaded, false) - ensure_same_feature!(model.module, parent_module, :belongs_to) required = Keyword.get(attrs, :required, true) name = name(parent_module) @@ -135,8 +135,6 @@ defmodule Momo.Model.Parser do child_module = relation_target_module(attrs, children) preloaded = Keyword.get(attrs, :preloaded, false) - ensure_same_feature!(model.module, child_module, :has_many) - name = plural(child_module) inverse = %Relation{ @@ -353,15 +351,6 @@ defmodule Momo.Model.Parser do %{model | attributes: attributes} end - defp ensure_same_feature!(from, to, kind) do - from_feature = feature(from) - to_feature = feature(to) - - if from_feature != to_feature do - raise "invalid relation of kind #{inspect(kind)} from #{inspect(from)} (in feature#{inspect(from_feature)}) to #{inspect(to)} (in feature #{inspect(to_feature)}). Both models must be in the same feature." - end - end - defp summary_model(%Model{} = model), do: summary_model(model.module) defp summary_model(module) do @@ -370,7 +359,7 @@ defmodule Momo.Model.Parser do name: name(module), table_name: table_name(module), plural: plural(module), - feature: feature(module) + app: app(module) } end end diff --git a/lib/momo/naming.ex b/lib/momo/naming.ex index f081577d..e4f01d79 100644 --- a/lib/momo/naming.ex +++ b/lib/momo/naming.ex @@ -3,7 +3,33 @@ defmodule Momo.Naming do Naming conventions """ - @doc false + @doc """ + Returns the prefix for the given module name. + + The prefix is the top level module, in most of the cases, your application module. + + ## Examples + + iex> Momo.Naming.prefix(MyApp.MyModel) + MyApp + """ + def prefix(module) do + module + |> Module.split() + |> List.first() + end + + @doc """ + Returns the name for the given module. + + The name is the last module name, in most of the cases, the model name. + + ## Examples + + iex> Momo.Naming.name(MyApp.User) + :user + + """ def name(model) do model |> last_module() @@ -59,51 +85,23 @@ defmodule Momo.Naming do end @doc false - def feature(model) do - model - |> Module.split() - |> Enum.reverse() - |> tl() - |> Enum.reverse() - |> Module.concat() - end + def app(model) do + prefix = + model + |> Module.split() + |> List.first() - @doc false - def feature_module(caller) do - case caller |> Module.split() |> Enum.reverse() do - [_, kind | rest] - when kind in [ - "Commands", - "Handlers", - "Queries", - "Events", - "Subscriptions", - "Mappings", - "Flows" - ] -> - rest |> Enum.reverse() |> Module.concat() - - other -> - raise "Invalid module name #{inspect(caller)}: #{inspect(other)}" - end + Module.concat([prefix, "App"]) end @doc false - def repo(feature) do - feature - |> Module.split() - |> Enum.drop(-1) - |> Kernel.++([Repo]) - |> Module.concat() - end + def repo(app) do + prefix = + app + |> Module.split() + |> List.first() - @doc false - def app(feature) do - feature - |> Module.split() - |> Enum.drop(-1) - |> Kernel.++([App]) - |> Module.concat() + Module.concat([prefix, "Repo"]) end @doc false diff --git a/lib/momo/query.ex b/lib/momo/query.ex index e937be96..a403891f 100644 --- a/lib/momo/query.ex +++ b/lib/momo/query.ex @@ -6,7 +6,6 @@ defmodule Momo.Query do parser: Momo.Query.Parser, generators: [ Momo.Query.Generator.Execute, - Momo.Query.Generator.Handle, Momo.Query.Generator.Metadata, Momo.Query.Generator.Scope ] @@ -15,7 +14,7 @@ defmodule Momo.Query do defstruct [ :name, - :feature, + :app, :params, :sorting, :model, @@ -23,7 +22,8 @@ defmodule Momo.Query do :limit, :many, :custom, - :debug + :debug, + :handler ] defmodule Policy do @@ -61,7 +61,7 @@ defmodule Momo.Query do model = query.model() with false <- Enum.empty?(query.policies()), - {:ok, [_ | _] = roles} <- query.feature().app().roles_from_context(context) do + {:ok, [_ | _] = roles} <- query.app().roles_from_context(context) do scope(model, roles, query.policies(), context) else true -> model @@ -101,30 +101,19 @@ defmodule Momo.Query do with {:ok, params} <- query.params().validate(params), context <- Map.put(context, :params, params) do - if query.custom?() do - params - |> query.handle(context) - |> maybe_map_result(query) - |> wrap_result() - else - context - |> query.scope() - |> apply_filters(query, params) - |> apply_preloads(query) - |> apply_sorting(query) - |> query.handle(params, context) - |> call_repo(query, context) - end + do_execute(query, params, context) end end @doc """ - Executes the query that has no params + Executes the query with no params and just a context """ def execute(query, context) do - if query.custom?() do + handler = query.handler() + + if handler do context - |> query.handle() + |> handler.execute() |> maybe_map_result(query) |> wrap_result() else @@ -132,7 +121,24 @@ defmodule Momo.Query do |> query.scope() |> apply_preloads(query) |> apply_sorting(query) - |> query.handle(context) + |> call_repo(query, context) + end + end + + defp do_execute(query, params, context) do + handler = query.handler() + + if handler do + params + |> handler.execute(context) + |> maybe_map_result(query) + |> wrap_result() + else + context + |> query.scope() + |> apply_filters(query, params) + |> apply_preloads(query) + |> apply_sorting(query) |> call_repo(query, context) end end @@ -142,7 +148,7 @@ defmodule Momo.Query do defp params(params) when is_map(params), do: params defp call_repo(queriable, query, _context) do - repo = query.feature().repo() + repo = query.app().repo() Logger.debug("Executing query", query: query, computed: inspect(queriable)) if query.many?() do @@ -171,7 +177,7 @@ defmodule Momo.Query do if same_model?(item, model) do item else - with {:ok, mapped} <- query.feature().map(Map, model, item) do + with {:ok, mapped} <- query.app().map(Map, model, item) do mapped end end @@ -188,7 +194,7 @@ defmodule Momo.Query do @doc """ Builds a query by taking the parameters and adding them as filters """ - def apply_filters(q, _query, params) do + def apply_filters(q, _query, params) when map_size(params) > 0 do filters = for {field, value} <- plain_map(params) do {field, :eq, value} @@ -197,6 +203,8 @@ defmodule Momo.Query do Momo.QueryBuilder.filter(q, filters) end + def apply_filters(q, _query, _params), do: q + @doc """ Applies sorting to the query """ diff --git a/lib/momo/query/dsl/query.ex b/lib/momo/query/dsl/query.ex index 10c7b333..b7f550dd 100644 --- a/lib/momo/query/dsl/query.ex +++ b/lib/momo/query/dsl/query.ex @@ -9,6 +9,8 @@ defmodule Momo.Query.Dsl.Query do attribute :many, kind: :boolean, required: false, default: false attribute :custom, kind: :boolean, required: false, default: false attribute :debug, kind: :boolean, required: false, default: false + attribute :handler, kind: :module, required: false + child :policy, min: 0 child :sort, min: 0 end diff --git a/lib/momo/query/generator/execute.ex b/lib/momo/query/generator/execute.ex index a32a0a54..4a0fe9e7 100644 --- a/lib/momo/query/generator/execute.ex +++ b/lib/momo/query/generator/execute.ex @@ -7,6 +7,7 @@ defmodule Momo.Query.Generator.Execute do quote do def execute(params, context), do: Momo.Query.execute(__MODULE__, params, context) def execute(context), do: Momo.Query.execute(__MODULE__, context) + def execute(), do: Momo.Query.execute(__MODULE__, %{}) end end end diff --git a/lib/momo/query/generator/handle.ex b/lib/momo/query/generator/handle.ex deleted file mode 100644 index 108ece2b..00000000 --- a/lib/momo/query/generator/handle.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Momo.Query.Generator.Handle do - @moduledoc false - @behaviour Diesel.Generator - - @impl true - def generate(_query, _opts) do - quote do - def handle(q, _params, _context), do: q - def handle(q, _context), do: q - - defoverridable handle: 2, handle: 3 - end - end -end diff --git a/lib/momo/query/generator/metadata.ex b/lib/momo/query/generator/metadata.ex index 76b597cc..a022062a 100644 --- a/lib/momo/query/generator/metadata.ex +++ b/lib/momo/query/generator/metadata.ex @@ -7,13 +7,14 @@ defmodule Momo.Query.Generator.Metadata do quote do def params, do: unquote(query.params) def model, do: unquote(query.model) - def feature, do: unquote(query.feature) + def app, do: unquote(query.app) def policies, do: unquote(Macro.escape(query.policies)) def limit, do: unquote(query.limit) def sorting, do: unquote(Macro.escape(query.sorting)) def custom?, do: unquote(query.custom) def many?, do: unquote(query.many) def debug?, do: unquote(query.debug) + def handler, do: unquote(query.handler) end end end diff --git a/lib/momo/query/parser.ex b/lib/momo/query/parser.ex index 1a94701c..f49920bb 100644 --- a/lib/momo/query/parser.ex +++ b/lib/momo/query/parser.ex @@ -6,12 +6,12 @@ defmodule Momo.Query.Parser do alias Momo.Query.Policy alias Momo.Query.Sort - import Momo.Feature.Naming + import Momo.Naming def parse({:query, attrs, children}, opts) do name = Keyword.fetch!(opts, :caller_module) caller = Keyword.fetch!(opts, :caller_module) - feature = feature_module(caller) + app = app(caller) params = Keyword.fetch!(attrs, :params) model = Keyword.fetch!(attrs, :returns) @@ -19,6 +19,7 @@ defmodule Momo.Query.Parser do many = Keyword.get(attrs, :many, false) debug = Keyword.get(attrs, :debug, false) custom = Keyword.get(attrs, :custom, false) + handler = Keyword.get(attrs, :handler) policies = for {:policy, attrs, _scopes} <- children do @@ -44,12 +45,13 @@ defmodule Momo.Query.Parser do %Query{ name: name, debug: debug, - feature: feature, + app: app, params: params, model: model, policies: policies, limit: limit, many: many, + handler: handler, custom: custom, sorting: sorting } diff --git a/lib/momo/scope.ex b/lib/momo/scope.ex index 2ab2a5f9..35bc6994 100644 --- a/lib/momo/scope.ex +++ b/lib/momo/scope.ex @@ -115,7 +115,7 @@ defmodule Momo.Scope do end defp do_filter(model, binding, [:**, field | rest], op, value, builder) do - case model.feature().get_shortest_path(model.name(), field) do + case model.app().get_shortest_path(model.name(), field) do [] -> raise ArgumentError, "no path to #{inspect(field)} in model #{inspect(model)}" diff --git a/lib/momo/subscription.ex b/lib/momo/subscription.ex index ea0e0ecc..f5e60743 100644 --- a/lib/momo/subscription.ex +++ b/lib/momo/subscription.ex @@ -12,13 +12,11 @@ defmodule Momo.Subscription do Momo.Subscription.Generator.Execute ] - defstruct [:name, :feature, :event, :action] + defstruct [:name, :app, :event, :action] def execute(subscription, params) do params = Map.from_struct(params) - feature = subscription.feature() - fun = subscription.action().fun_name() - - apply(feature, fun, [params]) + command = subscription.action() + command.execute(params, %{authorization: :skip}) end end diff --git a/lib/momo/subscription/generator/metadata.ex b/lib/momo/subscription/generator/metadata.ex index a7eeb6e1..1d9b23bb 100644 --- a/lib/momo/subscription/generator/metadata.ex +++ b/lib/momo/subscription/generator/metadata.ex @@ -7,7 +7,7 @@ defmodule Momo.Subscription.Generator.Metadata do quote do def event, do: unquote(subscription.event) def action, do: unquote(subscription.action) - def feature, do: unquote(subscription.feature) + def app, do: unquote(subscription.app) def name, do: unquote(subscription.name) end end diff --git a/lib/momo/subscription/parser.ex b/lib/momo/subscription/parser.ex index 7a353316..5297429f 100644 --- a/lib/momo/subscription/parser.ex +++ b/lib/momo/subscription/parser.ex @@ -4,19 +4,19 @@ defmodule Momo.Subscription.Parser do alias Momo.Subscription - import Momo.Feature.Naming + import Momo.Naming def parse({:subscription, attrs, _children}, opts) do name = Keyword.fetch!(opts, :caller_module) caller = Keyword.fetch!(opts, :caller_module) - feature = feature_module(caller) + app = app(caller) event = Keyword.fetch!(attrs, :on) action = Keyword.get(attrs, :perform) %Subscription{ name: name, - feature: feature, + app: app, event: event, action: action } diff --git a/lib/momo/ui.ex b/lib/momo/ui.ex index 9513040f..279691d9 100644 --- a/lib/momo/ui.ex +++ b/lib/momo/ui.ex @@ -21,4 +21,26 @@ defmodule Momo.Ui do More specific namespaces are listed first. """ def namespaces(ui), do: Enum.sort_by(ui.namespaces, &byte_size(&1.path()), :desc) + + @doc """ + Returns all routes configured for the given ui. + + Each route is a tuple {method, path, handler} + """ + def routes(ui) do + ui + |> namespaces() + |> Enum.flat_map(fn ns -> + prefix = ns.path() + + for route <- ns.routes() do + path = route.path() + method = route.method() + handler = Module.concat(route, Handler) + path = String.replace(prefix <> path, "//", "/") + + {method, path, handler} + end + end) + end end diff --git a/lib/momo/ui/form/generator/route.ex b/lib/momo/ui/form/generator/route.ex index 3a0b27b9..8fbe73d6 100644 --- a/lib/momo/ui/form/generator/route.ex +++ b/lib/momo/ui/form/generator/route.ex @@ -13,8 +13,12 @@ defmodule Momo.Ui.Form.Generator.Route do {:view, [name: view, for: "default"], []} ]} + route_module = Module.concat(caller, Route) + + IO.inspect(route_module: route_module) + quote do - defmodule Route do + defmodule unquote(route_module) do use Momo.Ui.Route @definition unquote(Macro.escape(definition)) diff --git a/lib/momo/ui/form/generator/view.ex b/lib/momo/ui/form/generator/view.ex index 1b7f94f4..7932b611 100644 --- a/lib/momo/ui/form/generator/view.ex +++ b/lib/momo/ui/form/generator/view.ex @@ -61,10 +61,10 @@ defmodule Momo.Ui.Form.Generator.View do defp type(_), do: "text" defp form_fields(field, form) do - cond do - field.in -> + case field do + %{in: enum} when not is_nil(enum) -> options = - for {label, value} <- field.in.options() do + for {label, value} <- enum.options() do {:option, [ value: value, @@ -74,7 +74,7 @@ defmodule Momo.Ui.Form.Generator.View do [{:select, [name: field.name], options}] - true -> + _ -> type = type(field.kind) [ diff --git a/lib/momo/ui/generator/routes.ex b/lib/momo/ui/generator/routes.ex index 1e5f18b6..f628bbfb 100644 --- a/lib/momo/ui/generator/routes.ex +++ b/lib/momo/ui/generator/routes.ex @@ -4,13 +4,8 @@ defmodule Momo.Ui.Generator.Routes do @impl true def generate(ui, _opts) do - namespaces = Momo.Ui.namespaces(ui) - routes = - for ns <- namespaces, {method, path} <- ns.routes() do - path = ns.path() <> path - path = String.replace(path, "//", "/") - + for {method, path, _} <- Momo.Ui.routes(ui) do {method, path} end diff --git a/lib/momo/ui/namespace.ex b/lib/momo/ui/namespace.ex index e4f7a1f6..5215ffe0 100644 --- a/lib/momo/ui/namespace.ex +++ b/lib/momo/ui/namespace.ex @@ -4,8 +4,8 @@ defmodule Momo.Ui.Namespace do otp_app: :momo, dsl: Momo.Ui.Namespace.Dsl, generators: [ - Momo.Ui.Namespace.Generator.Router, - Momo.Ui.Namespace.Generator.Metadata + Momo.Ui.Namespace.Generator.Metadata, + Momo.Ui.Namespace.Generator.Router ] defstruct [:path, :routes] diff --git a/lib/momo/ui/namespace/generator/metadata.ex b/lib/momo/ui/namespace/generator/metadata.ex index a3db5c43..399887f8 100644 --- a/lib/momo/ui/namespace/generator/metadata.ex +++ b/lib/momo/ui/namespace/generator/metadata.ex @@ -4,14 +4,14 @@ defmodule Momo.Ui.Namespace.Generator.Metadata do @impl true def generate(ns, _opts) do - routes = - for route <- ns.routes do - {route.method(), route.path()} - end + # routes = + # for route <- ns.routes do + # {route.method(), route.path()} + # end quote do def path, do: unquote(ns.path) - def routes, do: unquote(routes) + def routes, do: unquote(ns.routes) end end end diff --git a/mix.exs b/mix.exs index 1e97d4b9..996ad650 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,8 @@ defmodule Momo.MixProject do [ app: :momo, version: @version, - elixir: "~> 1.14", + elixir: "~> 1.18", + aliases: aliases(), elixirc_paths: elixirc_paths(), start_permanent: Mix.env() == :prod, consolidate_protocols: Mix.env() != :test, @@ -25,6 +26,12 @@ defmodule Momo.MixProject do ] end + def aliases do + [ + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] + ] + end + # Run "mix help compile.app" to learn about applications. def application do [ @@ -41,8 +48,9 @@ defmodule Momo.MixProject do {:calendar, "~> 1.0"}, {:diesel, "~> 0.8"}, {:ecto, "~> 3.9"}, + {:ecto_libsql, "~> 0.9"}, {:ecto_sql, "~> 3.9"}, - {:estree, "~> 2.7"}, + {:uuidv7, "~> 1.0"}, {:ex_doc, ">= 0.0.0"}, {:file_system, "~> 1.0"}, {:floki, "~> 0.34.0"}, @@ -50,8 +58,6 @@ defmodule Momo.MixProject do {:jason, "~> 1.2"}, {:libgraph, "~> 0.16"}, {:oban, "~> 2.18"}, - {:paginator, "~> 1.2.0"}, - {:postgrex, ">= 0.0.0"}, {:predicator, "~> 3.3"}, {:plug, "~> 1.14"}, {:slugify, "~> 1.3"}, diff --git a/mix.lock b/mix.lock index 5436797d..19916055 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,17 @@ %{ "bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"}, + "barrel_docdb": {:git, "https://github.com/barrel-db/barrel_docdb.git", "467945397e8fe7025248f44022b1e585a1b36745", []}, "bbmustache": {:hex, :bbmustache, "1.12.2", "0cabdce0db9fe6d3318131174b9f2b351328a4c0afbeb3e6e99bb0e02e9b621d", [:rebar3], [], "hexpm", "688b33a4d5cc2d51f575adf0b3683fc40a38314a2f150906edcfc77f5b577b3b"}, "bind": {:hex, :bind, "0.3.0", "252e1add788c5338e375f53041293079fc21d78b5aa1e34cd5c11acf145aa511", [:mix], [{:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "14558ad5647fec36540d61e96e93da8d44989aa50ee7bdbd795835cfa4517c09"}, + "bitmap": {:git, "https://gitlab.com/barrel-db/bitmap.git", "258469168c9c61f884ef37c99ce44dab49678901", [branch: "master"]}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, - "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, + "certifi": {:hex, :certifi, "2.16.0", "a4edfc1d2da3424d478a3271133bf28e0ec5e6fd8c009aab5a4ae980cb165ce9", [:rebar3], [], "hexpm", "8a64f6669d85e9cc0e5086fcf29a5b13de57a13efa23d3582874b9a19303f184"}, + "cf": {:hex, :cf, "0.2.2", "7f2913fff90abcabd0f489896cfeb0b0674f6c8df6c10b17a83175448029896c", [:rebar3], [], "hexpm", "48283b3019bc7fad56e7b23028a5da4d3e6cd598a553ab2a99a2153bf5f19b21"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, @@ -20,20 +25,23 @@ "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, - "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, + "hackney": {:hex, :hackney, "3.2.1", "600fba0d2a5d6b20f2bc2aa90a3f3e082f18cac50d648cde3bdef501a36a5090", [:rebar3], [{:certifi, "~> 2.16.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 0.10.2", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "1d9260c31b7c910b63e6b3929d296f18ba3d8217ef01800989afcfcc77776afe"}, + "hlc": {:hex, :hlc, "3.0.3", "c7d514e4bfdcc6934046bdce049dc0c610903078a2b5aa8b484c6d5b45e42cdc", [:rebar3], [], "hexpm", "bd664983ca1fe7dedc03101c66da62a163d3aa8e7f19937641ca5471f1b0478a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_builder": {:git, "https://github.com/pedro-gutierrez/html_builder", "bda9f9ba1093c1ef8c571a1963ba171a4765a963", []}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "match_trie": {:hex, :match_trie, "1.0.0", "993a2ac8c0b796bc40d6d99e92f9194faa9c8bcde3d0bb199148ba595f2f9d96", [:rebar3], [], "hexpm", "6a679d5bdf1cc0f1b77a0a62508d6846e69ad90467843ee1b1e4dd655ed73cad"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "oban": {:hex, :oban, "2.20.1", "39d0b68787e5cf251541c0d657a698f6142a24d8744e1e40b2cf045d4fa232a6", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17a45277dbeb41a455040b41dd8c467163fad685d1366f2f59207def3bcdd1d8"}, "paginator": {:hex, :paginator, "1.2.0", "f59c5da6238950b902b2fc074ffbf138d8766c058d0bd96069790dca5e3d82c9", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "df462f015aa91021430ba5f0ed2ee100de696a925d42f6926e276dbee35fbe1d"}, @@ -42,6 +50,11 @@ "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, "predicator": {:hex, :predicator, "3.3.0", "9b5bb2be6723c60e5840be9c760c861e8c90d2f464f1e2dc286226e252cb18bc", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9abb5f35d01abf3c552af7307ad2016c3374f12fa8df38ab78be174e20632be3"}, + "prometheus": {:hex, :prometheus, "4.11.0", "b95f8de8530f541bd95951e18e355a840003672e5eda4788c5fa6183406ba29a", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "719862351aabf4df7079b05dc085d2bbcbe3ac0ac3009e956671b1d5ab88247d"}, + "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, + "quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "rocksdb": {:git, "https://github.com/EnkiMultimedia/erlang-rocksdb.git", "105872d90bfc317af234b7b739da81e80a591653", [tag: "2.5.0"]}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "sneeze": {:hex, :sneeze, "1.2.1", "cf514e43d95a5be3595fc6d3eb3d5f47f76f518ec1146f75392ac040217d3618", [:make, :mix], [{:html_entities, "~> 0.5", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "3161385b1f79c7d1c6655e82c6752ed5869ceedb1d592b6fa1204aef1786fce5"}, "solid": {:hex, :solid, "0.15.2", "6921af98a3a862041bb6af72b5f6e094dbf0242366b142f98a92cabe4ed30d2a", [:mix], [{:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "18b062b04948b7f7b99ac4a9360681dac7e0bd142df5e62a7761696c7384be45"}, diff --git a/priv/templates/ui.js b/priv/templates/ui.js deleted file mode 100644 index 93da55f5..00000000 --- a/priv/templates/ui.js +++ /dev/null @@ -1,161 +0,0 @@ -(function() { - const route = S.data({}); - - function updateRoute() { - let loc = Hash.getQuery(); - loc.scope = Hash.getValue() || 'undefined' - route(loc) - } - - window.addEventListener('hashchange', updateRoute) - - function payload(obj) { - let payload = {} - for (const key in obj) { - let value = obj[key]; - if (typeof value === 'object') { - payload[key] = value.id; - } else payload[key] = value; - } - return payload; - } - - function scope(opts) { - let page = opts.page || 1 - let limit = opts.page_size || 10 - let offset = (page - 1) * limit - let q = opts.query || '' - return `?q=${q}&limit=${limit}&offset=${offset}` - } - - async function readItem(collection, id) { - return await fetch(`/api/${collection}/${id}`, { - method: 'GET' - }).then((response) => { - if (response.ok) return response.json(); - return Promise.reject(response); - }).then((data) => { - return { item: data, error: null } - }).catch((error) => { - return { item: null, error: error } - }) - } - - async function createItem(collection, data) { - return await fetch(`/api/${collection}`, { - method: 'POST', - headers: { - 'content-type': 'application/json; charset=UTF-8' - }, - body: JSON.stringify(payload(data)) - }).then((response) => { - if (response.ok) return response.json(); - return Promise.reject(response); - }).then((data) => { - return { item: data, error: null } - }).catch((error) => { - return { item: data, error: error } - }) - } - - async function updateItem(collection, id, data) { - return await fetch(`/api/${collection}/${id}`, { - method: 'PATCH', - headers: { - 'content-type': 'application/json; charset=UTF-8' - }, - body: JSON.stringify(payload(data)) - }).then((response) => { - if (response.ok) return response.json(); - return Promise.reject(response); - }).then((data) => { - return { item: data, error: null } - }).catch((error) => { - return { item: data, error: error } - }) - } - - async function deleteItem(collection, id) { - return await fetch(`/api/${collection}/${id}`, { - method: 'DELETE' - }).then((response) => { - if (response.ok) return {}; - return Promise.reject(response); - }).then((data) => { - return { item: data, error: null } - }).catch((error) => { - return { item: null, error: error } - }) - } - - async function searchItems(collection, opts) { - return await fetch(`/api/${collection}${scope(opts)}`, { - method: 'GET', - }).then((response) => { - if (response.ok) return response.json(); - return Promise.reject(response); - }).then((data) => { - return { items: data, error: null } - }).catch((error) => { - return { items: [], error: error } - }) - } - - function prop(object, path) { - let temp = object; - path.split(".").forEach(subPath => { - temp = temp ? (temp[subPath] || null) : null - }); - return temp; - } - - function format(str, obj) { - return str.replace(/\${(.*?)}/g, (x, g) => obj[g]); - } - - function bindFields(el, item) { - el - .querySelectorAll('[data-field]') - .forEach((child) => { - let path = child.dataset.field; - value = prop(item, path); - child.textContent = value; - }); - } - - function bindHrefs(el, item, onclick) { - el - .querySelectorAll('[data-href]') - .forEach((child) => { - let pattern = child.dataset.href; - let newLocation = format(pattern, item); - child.onclick = function() { - onclick(); - window.location = newLocation; - } - }); - } - - function debounce(func, wait) { - let timeout; - - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }; - - - - - S.root(() => { - <%= app_bindings %> - }) - - updateRoute() -})() diff --git a/test/migrations/20240907192955_momo_v1.exs b/test/migrations/20240907192955_momo_v1.exs deleted file mode 100644 index 4aab6976..00000000 --- a/test/migrations/20240907192955_momo_v1.exs +++ /dev/null @@ -1,94 +0,0 @@ -defmodule Momo.Migration.V1 do - use Ecto.Migration - - def up do - execute("CREATE SCHEMA accounts") - execute("CREATE SCHEMA notifications") - execute("CREATE SCHEMA publishing") - - create(table(:users, prefix: :accounts, primary_key: false)) do - add(:email, :string, null: false) - add(:id, :binary_id, primary_key: true, null: false) - add(:public, :boolean, null: false) - timestamps(type: :utc_datetime_usec) - end - - create(table(:authors, prefix: :publishing, primary_key: false)) do - add(:id, :binary_id, primary_key: true, null: false) - add(:name, :string, null: false) - timestamps(type: :utc_datetime_usec) - end - - create(table(:blogs, prefix: :publishing, primary_key: false)) do - add(:author_id, :binary_id, null: false) - add(:id, :binary_id, primary_key: true, null: false) - add(:name, :string, null: false) - add(:published, :boolean, null: false) - add(:theme_id, :binary_id, null: true) - timestamps(type: :utc_datetime_usec) - end - - create(table(:comments, prefix: :publishing, primary_key: false)) do - add(:author_id, :binary_id, null: false) - add(:body, :string, null: false) - add(:id, :binary_id, primary_key: true, null: false) - add(:post_id, :binary_id, null: false) - timestamps(type: :utc_datetime_usec) - end - - create(table(:posts, prefix: :publishing, primary_key: false)) do - add(:author_id, :binary_id, null: false) - add(:blog_id, :binary_id, null: false) - add(:deleted, :boolean, null: false) - add(:id, :binary_id, primary_key: true, null: false) - add(:locked, :boolean, null: false) - add(:published, :boolean, null: false) - add(:published_at, :utc_datetime, null: true) - add(:title, :string, null: false) - timestamps(type: :utc_datetime_usec) - end - - create(table(:themes, prefix: :publishing, primary_key: false)) do - add(:id, :binary_id, primary_key: true, null: false) - add(:name, :string, null: false) - timestamps(type: :utc_datetime_usec) - end - - alter(table(:blogs, prefix: :publishing)) do - modify(:author_id, references(:authors, type: :binary_id, on_delete: :nothing)) - end - - alter(table(:blogs, prefix: :publishing)) do - modify(:theme_id, references(:themes, type: :binary_id, on_delete: :nothing)) - end - - alter(table(:comments, prefix: :publishing)) do - modify(:author_id, references(:authors, type: :binary_id, on_delete: :nothing)) - end - - alter(table(:comments, prefix: :publishing)) do - modify(:post_id, references(:posts, type: :binary_id, on_delete: :nothing)) - end - - alter(table(:posts, prefix: :publishing)) do - modify(:author_id, references(:authors, type: :binary_id, on_delete: :nothing)) - end - - alter(table(:posts, prefix: :publishing)) do - modify(:blog_id, references(:blogs, type: :binary_id, on_delete: :nothing)) - end - - create( - unique_index(:blogs, [:author_id, :name], - name: :blogs_author_id_name_idx, - prefix: :publishing - ) - ) - - create(unique_index(:themes, [:name], name: :themes_name_idx, prefix: :publishing)) - end - - def down do - [] - end -end diff --git a/test/migrations/20240912173759_momo_v2.exs b/test/migrations/20240912173759_momo_v2.exs deleted file mode 100644 index 4f1d5fea..00000000 --- a/test/migrations/20240912173759_momo_v2.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Momo.Migration.V2 do - use Ecto.Migration - - def up do - alter(table(:blogs, prefix: :publishing)) do - add(:public, :boolean, null: true) - end - end - - def down do - [] - end -end diff --git a/test/migrations/20241025180446_momo_v3.exs b/test/migrations/20241025180446_momo_v3.exs deleted file mode 100644 index 05de8049..00000000 --- a/test/migrations/20241025180446_momo_v3.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Momo.Migration.V3 do - use Ecto.Migration - - def up do - create(unique_index(:users, [:email], name: :users_email_idx, prefix: :accounts)) - end - - def down do - [] - end -end diff --git a/test/migrations/20241026183959_momo_v4.exs b/test/migrations/20241026183959_momo_v4.exs deleted file mode 100644 index 544f3128..00000000 --- a/test/migrations/20241026183959_momo_v4.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Momo.Migration.V4 do - use Ecto.Migration - - def up do - alter(table(:authors, prefix: :publishing)) do - add(:profile, :string, null: false) - end - end - - def down do - [] - end -end diff --git a/test/migrations/20241030180646_momo_v5.exs b/test/migrations/20241030180646_momo_v5.exs deleted file mode 100644 index ec619239..00000000 --- a/test/migrations/20241030180646_momo_v5.exs +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Momo.Migration.V5 do - use Ecto.Migration - - def up do - alter(table(:users, prefix: :accounts)) do - modify(:public, :boolean, []) - end - - alter(table(:blogs, prefix: :publishing)) do - modify(:public, :boolean, []) - modify(:published, :boolean, []) - end - - alter(table(:authors, prefix: :publishing)) do - modify(:profile, :string, default: "publisher") - end - - alter(table(:posts, prefix: :publishing)) do - modify(:published, :boolean, []) - modify(:deleted, :boolean, []) - modify(:locked, :boolean, []) - end - end - - def down do - [] - end -end diff --git a/test/migrations/20250101000000_momo_v1.exs b/test/migrations/20250101000000_momo_v1.exs new file mode 100644 index 00000000..df2b0ddd --- /dev/null +++ b/test/migrations/20250101000000_momo_v1.exs @@ -0,0 +1,125 @@ +defmodule Momo.Migration.V1 do + use Ecto.Migration + + def up do + Oban.Migration.up() + + create(table(:credentials, prefix: nil, primary_key: false)) do + add(:enabled, :boolean, null: false) + add(:id, :binary_id, primary_key: true, null: false) + add(:name, :string, null: false) + add(:type, :integer, null: false) + add(:value, :string, null: false) + add(:user_id, :binary_id, null: false) + timestamps(type: :utc_datetime_usec) + end + + create(table(:comments, prefix: nil, primary_key: false)) do + add(:id, :binary_id, primary_key: true, null: false) + add(:body, :string, null: false) + add(:author_id, :binary_id, null: false) + add(:post_id, :binary_id, null: false) + timestamps(type: :utc_datetime_usec) + end + + create(table(:users, prefix: nil, primary_key: false)) do + add(:id, :binary_id, primary_key: true, null: false) + add(:public, :boolean, default: false, null: false) + add(:email, :string, null: false) + add(:external_id, :binary_id, null: false) + timestamps(type: :utc_datetime_usec) + end + + create(table(:onboardings, prefix: nil, primary_key: false)) do + add(:id, :binary_id, primary_key: true, null: false) + add(:state, :string, null: true) + add(:user_id, :binary_id, null: false) + add(:steps_pending, :integer, null: false) + timestamps(type: :utc_datetime_usec) + end + + create(table(:blogs, prefix: nil, primary_key: false)) do + add(:id, :binary_id, primary_key: true, null: false) + add(:name, :string, null: false) + add(:public, :boolean, default: false, null: true) + add(:author_id, :binary_id, null: false) + add(:published, :boolean, default: false, null: false) + add(:theme_id, :binary_id, null: true) + timestamps(type: :utc_datetime_usec) + end + + create(table(:authors, prefix: nil, primary_key: false)) do + add(:id, :binary_id, primary_key: true, null: false) + add(:name, :string, null: false) + add(:profile, :string, default: "publisher", null: false) + timestamps(type: :utc_datetime_usec) + end + + create(table(:posts, prefix: nil, primary_key: false)) do + add(:id, :binary_id, primary_key: true, null: false) + add(:title, :string, null: false) + add(:author_id, :binary_id, null: false) + add(:published, :boolean, default: false, null: false) + add(:blog_id, :binary_id, null: false) + add(:deleted, :boolean, default: false, null: false) + add(:locked, :boolean, default: false, null: false) + add(:published_at, :utc_datetime, null: true) + timestamps(type: :utc_datetime_usec) + end + + create(table(:themes, prefix: nil, primary_key: false)) do + add(:id, :binary_id, primary_key: true, null: false) + add(:name, :string, null: false) + timestamps(type: :utc_datetime_usec) + end + + alter(table(:blogs, prefix: nil)) do + modify(:author_id, references(:authors, type: :binary_id, on_delete: :nothing)) + end + + alter(table(:blogs, prefix: nil)) do + modify(:theme_id, references(:themes, type: :binary_id, on_delete: :nothing)) + end + + alter(table(:comments, prefix: nil)) do + modify(:author_id, references(:authors, type: :binary_id, on_delete: :nothing)) + end + + alter(table(:comments, prefix: nil)) do + modify(:post_id, references(:posts, type: :binary_id, on_delete: :nothing)) + end + + alter(table(:credentials, prefix: nil)) do + modify(:user_id, references(:users, type: :binary_id, on_delete: :nothing)) + end + + alter(table(:posts, prefix: nil)) do + modify(:author_id, references(:authors, type: :binary_id, on_delete: :nothing)) + end + + alter(table(:posts, prefix: nil)) do + modify(:blog_id, references(:blogs, type: :binary_id, on_delete: :nothing)) + end + + create(unique_index(:users, [:email], name: :users_email_idx, prefix: nil)) + + create( + unique_index(:credentials, [:user_id, :name], + name: :credentials_user_id_name_idx, + prefix: nil + ) + ) + + create(unique_index(:onboardings, [:user_id], name: :onboardings_user_id_idx, prefix: nil)) + + create( + unique_index(:blogs, [:author_id, :name], name: :blogs_author_id_name_idx, prefix: nil) + ) + + create(unique_index(:themes, [:name], name: :themes_name_idx, prefix: nil)) + end + + def down do + [] + end +end diff --git a/test/migrations/20250727105850_momo_v6.exs b/test/migrations/20250727105850_momo_v6.exs deleted file mode 100644 index 6cf4db12..00000000 --- a/test/migrations/20250727105850_momo_v6.exs +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Momo.Migration.V6 do - use Ecto.Migration - - def up do - alter(table(:users, prefix: :accounts)) do - modify(:public, :boolean, default: false) - end - - alter(table(:blogs, prefix: :publishing)) do - modify(:public, :boolean, default: false) - modify(:published, :boolean, default: false) - end - - alter(table(:posts, prefix: :publishing)) do - modify(:published, :boolean, default: false) - modify(:locked, :boolean, default: false) - modify(:deleted, :boolean, default: false) - end - end - - def down do - [] - end -end diff --git a/test/migrations/20250802082857_momo_v7.exs b/test/migrations/20250802082857_momo_v7.exs deleted file mode 100644 index c490730c..00000000 --- a/test/migrations/20250802082857_momo_v7.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Momo.Migration.V7 do - use Ecto.Migration - - def up do - alter(table(:users, prefix: :accounts)) do - add(:external_id, :binary_id, null: false) - end - end - - def down do - [] - end -end diff --git a/test/migrations/20250802082858_momo_v8.exs b/test/migrations/20250802082858_momo_v8.exs deleted file mode 100644 index dbe2c605..00000000 --- a/test/migrations/20250802082858_momo_v8.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Momo.Migration.V8 do - use Ecto.Migration - - def up do - Oban.Migration.up(version: 12) - end - - # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if - # necessary, regardless of which version we've migrated `up` to. - def down do - Oban.Migration.down(version: 1) - end -end diff --git a/test/migrations/20250813221021_momo_v9.exs b/test/migrations/20250813221021_momo_v9.exs deleted file mode 100644 index 8ea31500..00000000 --- a/test/migrations/20250813221021_momo_v9.exs +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Momo.Migration.V9 do - use Ecto.Migration - - def up do - create(table(:onboardings, prefix: :accounts, primary_key: false)) do - add(:id, :binary_id, primary_key: true, null: false) - add(:user_id, :binary_id, null: false) - add(:steps_pending, :integer, null: false) - timestamps(type: :utc_datetime_usec) - end - end - - def down do - [] - end -end \ No newline at end of file diff --git a/test/migrations/20250815082537_momo_v10.exs b/test/migrations/20250815082537_momo_v10.exs deleted file mode 100644 index 5877fc5f..00000000 --- a/test/migrations/20250815082537_momo_v10.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Momo.Migration.V10 do - use Ecto.Migration - - def up do - create( - unique_index(:onboardings, [:user_id], name: :onboardings_user_id_idx, prefix: :accounts) - ) - end - - def down do - [] - end -end \ No newline at end of file diff --git a/test/migrations/20250822180945_momo_v11.exs b/test/migrations/20250822180945_momo_v11.exs deleted file mode 100644 index 3914c5eb..00000000 --- a/test/migrations/20250822180945_momo_v11.exs +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Momo.Migration.V11 do - use Ecto.Migration - - def up do - create(table(:credentials, prefix: :accounts, primary_key: false)) do - add(:enabled, :boolean, null: false) - add(:id, :binary_id, primary_key: true, null: false) - add(:name, :string, null: false) - add(:value, :string, null: false) - add(:user_id, :binary_id, null: false) - timestamps(type: :utc_datetime_usec) - end - - alter(table(:credentials, prefix: :accounts)) do - modify(:user_id, references(:users, type: :binary_id, on_delete: :nothing)) - end - - create( - unique_index(:credentials, [:user_id, :name], - name: :credentials_user_id_name_idx, - prefix: :accounts - ) - ) - end - - def down do - [] - end -end \ No newline at end of file diff --git a/test/migrations/20250903222037_momo_v12.exs b/test/migrations/20250903222037_momo_v12.exs deleted file mode 100644 index 03d1fc2e..00000000 --- a/test/migrations/20250903222037_momo_v12.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Momo.Migration.V12 do - use Ecto.Migration - - def up do - alter(table(:credentials, prefix: :accounts)) do - add(:type, :integer, null: false) - end - end - - def down do - [] - end -end \ No newline at end of file diff --git a/test/migrations/20250903222428_momo_v13.exs b/test/migrations/20250903222428_momo_v13.exs deleted file mode 100644 index f47244fd..00000000 --- a/test/migrations/20250903222428_momo_v13.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Momo.Migration.V13 do - use Ecto.Migration - - def up do - alter(table(:onboardings, prefix: :accounts)) do - add(:state, :string, null: true) - end - end - - def down do - [] - end -end \ No newline at end of file diff --git a/test/momo/api/create_decoder_test.exs b/test/momo/api/create_decoder_test.exs deleted file mode 100644 index ccf9756c..00000000 --- a/test/momo/api/create_decoder_test.exs +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Momo.Api.CreateDecoderTest do - use Momo.DataCase - - alias Blogs.Publishing.Post - - setup [:comments, :post_api_params] - - describe "create api decoder" do - test "decodes attributes and relations", context do - params = context.post_api_params - - assert {:ok, data} = Post.ApiCreateDecoder.decode(params) - - assert data.id == params["id"] - assert data.title == params["title"] - assert data.blog == context.blog - - assert DateTime.to_iso8601(data.published_at) == params["published_at"] - end - - test "detects ids that are not uuids", context do - params = - context.post_api_params - |> Map.put("id", "foo") - |> Map.put("blog", %{"id" => "bar"}) - - assert {:error, errors} = Post.ApiCreateDecoder.decode(params) - - assert errors == %{ - "id" => ["not a valid uuid"], - "blog.id" => ["not a valid uuid"] - } - end - - test "detects missing fields and invalid types", context do - params = - context.post_api_params - |> Map.put("id", nil) - |> Map.put("published_at", nil) - |> Map.put("locked", "true") - - assert {:error, errors} = Post.ApiCreateDecoder.decode(params) - - assert errors == %{ - "locked" => ["expected boolean received binary"], - "id" => ["is required"] - } - end - - test "detects unknown relations", context do - params = Map.put(context.post_api_params, "blog", %{"id" => Ecto.UUID.generate()}) - - assert {:error, errors} = Post.ApiCreateDecoder.decode(params) - - assert errors == %{"blog.id" => ["was not found"]} - end - end -end diff --git a/test/momo/api/create_handler_test.exs b/test/momo/api/create_handler_test.exs deleted file mode 100644 index ef360e52..00000000 --- a/test/momo/api/create_handler_test.exs +++ /dev/null @@ -1,125 +0,0 @@ -defmodule Momo.Api.CreateHandlerTest do - use Momo.DataCase - - alias Blogs.Publishing.Author - alias Blogs.Publishing.Blog - - setup [:comments] - - describe "create handler" do - test "validates params" do - params = %{ - "name" => "john" - } - - errors = - :post - |> conn("/", params) - |> Author.ApiCreateHandler.execute([]) - |> json_response!(400) - - assert %{ - "id" => ["is required"] - } == errors - end - - test "denies access if no role matches" do - params = %{ - "id" => Ecto.UUID.generate(), - "name" => "john" - } - - errors = - :post - |> conn("/", params) - |> assign(:current_user, %{roles: [:other]}) - |> Author.ApiCreateHandler.execute([]) - |> json_response!(403) - - assert %{ - "reason" => "forbidden" - } == errors - end - - test "allows access if no roles are defined" do - params = %{ - "id" => Ecto.UUID.generate(), - "name" => "john" - } - - :post - |> conn("/", params) - |> assign(:current_user, %{roles: []}) - |> Author.ApiCreateHandler.execute([]) - |> json_response!(201) - end - - test "creates top level models" do - id = Ecto.UUID.generate() - - params = %{ - "id" => id, - "name" => "john" - } - - guest = %{roles: [:guest]} - - resp = - :post - |> conn("/", params) - |> assign(:current_user, guest) - |> Author.ApiCreateHandler.execute([]) - |> json_response!(201) - - assert %{ - "name" => "john", - "id" => id, - "profile" => "publisher" - } == resp - end - - test "creates child models", context do - id = Ecto.UUID.generate() - - params = %{ - "id" => id, - "name" => "new blog", - "published" => true, - "author" => %{"id" => context.author.id} - } - - guest = %{roles: [:user], id: context.author.id} - - resp = - :post - |> conn("/", params) - |> assign(:current_user, guest) - |> Blog.ApiCreateHandler.execute([]) - |> json_response!(201) - - author_id = context.author.id - - assert %{ - "name" => "new blog", - "published" => true, - "id" => ^id, - "author" => %{"id" => ^author_id} - } = resp - end - - test "detects conflicts", context do - params = %{ - "id" => context.author.id, - "name" => "john" - } - - guest = %{roles: [:guest]} - - :post - |> conn("/", params) - |> assign(:current_user, guest) - |> Author.ApiCreateHandler.execute([]) - |> json_response!(409) - end - end -end diff --git a/test/momo/api/delete_decoder_test.exs b/test/momo/api/delete_decoder_test.exs deleted file mode 100644 index baa32ff7..00000000 --- a/test/momo/api/delete_decoder_test.exs +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Momo.Api.DeleteDecoderTest do - use Momo.DataCase - - alias Blogs.Publishing.Post - - describe "delete api decoder" do - test "decodes the model id" do - id = Ecto.UUID.generate() - params = %{"id" => id} - - assert {:ok, data} = Post.ApiDeleteDecoder.decode(params) - assert data.id == id - end - end -end diff --git a/test/momo/api/delete_handler_test.exs b/test/momo/api/delete_handler_test.exs deleted file mode 100644 index 539b8b1e..00000000 --- a/test/momo/api/delete_handler_test.exs +++ /dev/null @@ -1,72 +0,0 @@ -defmodule Momo.Api.DeleteHandlerTest do - use Momo.DataCase - - alias Blogs.Publishing.{Blog, Comment} - - setup [:comments] - - describe "delete handler" do - test "does authorization", context do - params = %{ - "id" => context.blog.id - } - - guest = %{roles: [:guest]} - - params - |> test_conn() - |> assign(:current_user, guest) - |> Blog.ApiDeleteHandler.execute([]) - |> json_response!(403) - end - - test "returns not found codes", context do - params = %{ - "id" => Ecto.UUID.generate() - } - - user = %{roles: [:user], id: context.author.id} - - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiDeleteHandler.execute([]) - |> json_response!(404) - end - - test "does not delete models with children", context do - params = %{ - "id" => context.blog.id - } - - user = %{roles: [:user], id: context.author.id} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiDeleteHandler.execute([]) - |> json_response!(412) - - assert %{ - "blog_id" => ["has children"] - } == resp - end - - test "deletes models", context do - params = %{ - "id" => context.comment1.id - } - - user = %{roles: [:user], id: context.author.id} - - params - |> test_conn() - |> assign(:current_user, user) - |> Comment.ApiDeleteHandler.execute([]) - |> json_response!(204) - - assert {:error, :not_found} = Blog.fetch(context.comment1.id) - end - end -end diff --git a/test/momo/api/encoder_test.exs b/test/momo/api/encoder_test.exs deleted file mode 100644 index 33bcc29d..00000000 --- a/test/momo/api/encoder_test.exs +++ /dev/null @@ -1,60 +0,0 @@ -defmodule Momo.Api.EncoderTest do - use Momo.DataCase - - alias Momo.Api.Encoder - alias Blogs.Repo - - setup [:comments, :post_api_params] - - describe "json api model encoder" do - test "renders single models", context do - post_id = context.post.id - published_at = context.post.published_at - blog_id = context.blog.id - author_id = context.author.id - - assert %{ - published_at: ^published_at, - title: "first post", - deleted: false, - locked: false, - published: true, - id: ^post_id, - blog: %{ - id: ^blog_id - }, - author: %{ - id: ^author_id - } - } = Encoder.encode(context.post) - end - - test "renders full relations if they are loaded", context do - post = Repo.preload(context.post, [:blog]) - - post_id = context.post.id - published_at = context.post.published_at - blog_id = context.blog.id - author_id = context.author.id - - assert %{ - published_at: ^published_at, - title: "first post", - deleted: false, - locked: false, - published: true, - id: ^post_id, - blog: %{ - name: "elixir blog", - published: true, - id: ^blog_id, - author: %{id: ^author_id}, - theme: nil - }, - author: %{ - id: ^author_id - } - } = Encoder.encode(post) - end - end -end diff --git a/test/momo/api/error_encoder_test.exs b/test/momo/api/error_encoder_test.exs deleted file mode 100644 index 3ea44377..00000000 --- a/test/momo/api/error_encoder_test.exs +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Momo.Api.ErrorEncoderTest do - use Momo.DataCase - - alias Momo.Api.ErrorEncoder - alias Blogs.Publishing.Post - - describe "json api error encoder" do - test "encodes validation errors" do - errors = %{ - "field" => ["error one", "error two"] - } - - assert {:error, errors} == ErrorEncoder.encode_errors(errors) - end - - test "encodes simple errors" do - assert {:error, error} = ErrorEncoder.encode_errors(:forbidden) - - assert %{reason: :forbidden} == error - end - - test "encodes ecto changeset errors" do - errors = %Post{} |> Post.insert_changeset(%{}) - - assert {:error, errors} = ErrorEncoder.encode_errors(errors) - - assert errors - |> Map.values() - |> Enum.all?(&(&1 == ["can't be blank"])) - end - end -end diff --git a/test/momo/api/list_by_parent_decoder_test.exs b/test/momo/api/list_by_parent_decoder_test.exs deleted file mode 100644 index 8d63ab91..00000000 --- a/test/momo/api/list_by_parent_decoder_test.exs +++ /dev/null @@ -1,59 +0,0 @@ -defmodule Momo.Api.ListByParentDecoderTest do - use Momo.DataCase - - alias Blogs.Publishing.Blog - - setup [:comments] - - describe "list by parent json api decoder" do - test "decodes parent and includes params", context do - params = %{"id" => context.author.id, "include" => "author"} - - assert {:ok, data} = Blog.ApiListByAuthorDecoder.decode(params) - assert data.author == context.author - assert data.preload == [:author] - end - - test "decodes queries attributes", context do - params = %{"id" => context.author.id, "query" => "name:elixir,published:true"} - - assert {:ok, data} = Blog.ApiListByAuthorDecoder.decode(params) - assert data.query.name == "elixir" - assert data.query.published == true - - params = %{"id" => context.author.id, "query" => "name:elixir,published:false"} - - assert {:ok, data} = Blog.ApiListByAuthorDecoder.decode(params) - assert data.query.name == "elixir" - assert data.query.published == false - - params = %{"id" => context.author.id, "query" => "published:false"} - - assert {:ok, data} = Blog.ApiListByAuthorDecoder.decode(params) - assert data.query.published == false - end - - test "detects missing parent" do - params = %{"include" => "author"} - - assert {:error, error} = Blog.ApiListByAuthorDecoder.decode(params) - assert error["id"] == ["is required"] - end - - test "detects unknown parents" do - params = %{"include" => "author", "id" => Ecto.UUID.generate()} - - assert {:error, error} = Blog.ApiListByAuthorDecoder.decode(params) - assert error["id"] == ["was not found"] - end - - test "decodes before and after cursors", context do - params = %{"before" => "a", "after" => "b", "limit" => "1", "id" => context.author.id} - - assert {:ok, data} = Blog.ApiListByAuthorDecoder.decode(params) - assert data.limit == 1 - assert data.before == "a" - assert data.after == "b" - end - end -end diff --git a/test/momo/api/list_by_parent_handler_test.exs b/test/momo/api/list_by_parent_handler_test.exs deleted file mode 100644 index 2ba8dde0..00000000 --- a/test/momo/api/list_by_parent_handler_test.exs +++ /dev/null @@ -1,50 +0,0 @@ -defmodule Momo.Api.ListByParentHandlerTest do - use Momo.DataCase - - alias Blogs.Publishing.Blog - - setup [:comments] - - describe "list by parent handler" do - test "returns items", context do - params = %{"id" => context.author.id} - - user = %{id: context.author.id, roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiListByAuthorHandler.execute([]) - |> json_response!(200) - - author_id = context.author.id - blog_id = context.blog.id - - assert %{ - "limit" => 50, - "total_count" => 1, - "items" => [ - %{ - "author" => %{"id" => ^author_id}, - "id" => ^blog_id, - "name" => "elixir blog", - "published" => true - } - ] - } = resp - end - - test "returns a 404 if the parent is not found", context do - params = %{"id" => Ecto.UUID.generate()} - - user = %{id: context.author.id, roles: [:user]} - - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiListByAuthorHandler.execute([]) - |> json_response!(404) - end - end -end diff --git a/test/momo/api/list_decoder_test.exs b/test/momo/api/list_decoder_test.exs deleted file mode 100644 index 353989d9..00000000 --- a/test/momo/api/list_decoder_test.exs +++ /dev/null @@ -1,76 +0,0 @@ -defmodule Momo.Api.ListDecoderTest do - use Momo.DataCase - - alias Blogs.Publishing.Blog - - describe "list json api decoder" do - test "decodes empty no params" do - params = %{} - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert data == params - end - - test "decodes queries attributes" do - params = %{"query" => "name:elixir,published:true"} - - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert data.query.name == "elixir" - assert data.query.published == true - - params = %{"query" => "name:elixir,published:false"} - - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert data.query.name == "elixir" - assert data.query.published == false - - params = %{"query" => "published:false"} - - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert data.query.published == false - end - - test "transforms simple includes into ecto preloads" do - params = %{"include" => "posts,author"} - - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert data.preload == [:posts, :author] - end - - test "does not support complex includes into ecto preloads" do - params = %{"include" => "posts,comments.author"} - - assert {:error, %{"include" => ["no such field comments.author"]}} = - Blog.ApiListDecoder.decode(params) - end - - test "rejects unknown includes" do - params = %{"include" => "comment"} - - assert {:error, %{"include" => ["no such field comment"]}} = - Blog.ApiListDecoder.decode(params) - end - - test "decodes pagination params" do - params = %{"before" => "a", "after" => "b", "limit" => "1"} - - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert data.limit == 1 - assert data.before == "a" - assert data.after == "b" - end - - test "decodes sorting params" do - params = %{"sort" => "name:desc,published:asc"} - - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert data.sort == %{name: :desc, published: :asc} - end - - test "ignores sorting on unknown fields" do - params = %{"sort" => "body:desc"} - - assert {:ok, data} = Blog.ApiListDecoder.decode(params) - assert Enum.empty?(data.sort) - end - end -end diff --git a/test/momo/api/list_handler_test.exs b/test/momo/api/list_handler_test.exs deleted file mode 100644 index df2b6000..00000000 --- a/test/momo/api/list_handler_test.exs +++ /dev/null @@ -1,169 +0,0 @@ -defmodule Momo.Api.ListHandlerTest do - use Momo.DataCase - - alias Blogs.Publishing.{Blog, Comment} - - setup [:comments] - - describe "list handler" do - test "returns models", context do - params = %{} - - user = %{id: context.author.id, roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiListHandler.execute([]) - |> json_response!(200) - - author_id = context.author.id - blog_id = context.blog.id - - assert %{ - "limit" => 50, - "total_count" => 1, - "items" => [ - %{ - "author" => %{"id" => ^author_id}, - "id" => ^blog_id, - "name" => "elixir blog", - "published" => true, - "theme" => nil - } - ] - } = resp - end - - test "returns included parents", context do - params = %{"include" => "author"} - - user = %{id: context.author.id, roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiListHandler.execute([]) - |> json_response!(200) - - author_id = context.author.id - blog_id = context.blog.id - - assert %{ - "limit" => 50, - "total_count" => 1, - "items" => [ - %{ - "author" => %{"id" => ^author_id, "name" => "foo"}, - "id" => ^blog_id, - "name" => "elixir blog", - "published" => true - } - ] - } = resp - end - - test "supports query filters", context do - params = %{"include" => "author", "query" => "name:finance"} - - user = %{id: context.author.id, roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiListHandler.execute([]) - |> json_response!(200) - - assert %{ - "limit" => 50, - "total_count" => 0, - "items" => [] - } == resp - end - - test "supports pagination" do - params = %{"limit" => "1"} - user = %{roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Comment.ApiListHandler.execute([]) - |> json_response!(200) - - after_cursor = resp["after"] - assert resp["total_count"] > 1 - assert [c1] = resp["items"] - - assert after_cursor - - params = %{"limit" => "1", "after" => after_cursor} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Comment.ApiListHandler.execute([]) - |> json_response!(200) - - before_cursor = resp["before"] - assert [c2] = resp["items"] - refute c1 == c2 - - params = %{"limit" => "1", "before" => before_cursor} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Comment.ApiListHandler.execute([]) - |> json_response!(200) - - assert [c3] = resp["items"] - - assert c1 == c3 - end - - test "supports sorting" do - params = %{"sort" => "body:asc"} - user = %{roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Comment.ApiListHandler.execute([]) - |> json_response!(200) - - bodies = Enum.map(resp["items"], & &1["body"]) - assert bodies == Enum.sort(bodies) - - params = %{"sort" => "body:desc"} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Comment.ApiListHandler.execute([]) - |> json_response!(200) - - bodies2 = Enum.map(resp["items"], & &1["body"]) - assert Enum.reverse(bodies2) == bodies - end - - test "ignores sorting on unknown fields" do - params = %{"sort" => "name:asc"} - user = %{roles: [:user]} - - params - |> test_conn() - |> assign(:current_user, user) - |> Comment.ApiListHandler.execute([]) - |> json_response!(200) - end - end -end diff --git a/test/momo/api/read_decoder_test.exs b/test/momo/api/read_decoder_test.exs deleted file mode 100644 index 3a269995..00000000 --- a/test/momo/api/read_decoder_test.exs +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Momo.Api.ReadDecoderTest do - use Momo.DataCase - - alias Blogs.Publishing.Post - - describe "read api decoder" do - test "decodes the model id and preload" do - id = Ecto.UUID.generate() - params = %{"id" => id, "include" => "blog"} - - assert {:ok, data} = Post.ApiReadDecoder.decode(params) - assert data.id == id - assert data.preload == [:blog] - end - end -end diff --git a/test/momo/api/read_handler_test.exs b/test/momo/api/read_handler_test.exs deleted file mode 100644 index 05cd06f7..00000000 --- a/test/momo/api/read_handler_test.exs +++ /dev/null @@ -1,90 +0,0 @@ -defmodule Momo.Api.ReadHandlerTest do - use Momo.DataCase - - alias Blogs.Publishing.Blog - - setup [:comments] - - describe "read handler" do - test "does authorization", context do - params = %{ - "id" => context.blog.id - } - - guest = %{roles: [:guest]} - - params - |> test_conn() - |> assign(:current_user, guest) - |> Blog.ApiReadHandler.execute([]) - |> json_response!(403) - end - - test "returns not found codes" do - params = %{ - "id" => Ecto.UUID.generate() - } - - user = %{roles: [:user]} - - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiReadHandler.execute([]) - |> json_response!(404) - end - - test "returns models", context do - params = %{ - "id" => context.blog.id - } - - user = %{roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiReadHandler.execute([]) - |> json_response!(200) - - blog_id = context.blog.id - author_id = context.author.id - - assert %{ - "author" => %{"id" => ^author_id}, - "id" => ^blog_id, - "name" => "elixir blog", - "published" => true, - "theme" => nil - } = resp - end - - test "returns included parents", context do - params = %{ - "include" => "author", - "id" => context.blog.id - } - - user = %{id: context.author.id, roles: [:user]} - - resp = - params - |> test_conn() - |> assign(:current_user, user) - |> Blog.ApiReadHandler.execute([]) - |> json_response!(200) - - blog_id = context.blog.id - author_id = context.author.id - - assert %{ - "author" => %{"id" => ^author_id, "name" => "foo"}, - "id" => ^blog_id, - "name" => "elixir blog", - "published" => true, - "theme" => nil - } = resp - end - end -end diff --git a/test/momo/api/update_decoder_test.exs b/test/momo/api/update_decoder_test.exs deleted file mode 100644 index a032aa6f..00000000 --- a/test/momo/api/update_decoder_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Momo.Api.UpdateDecoderTest do - use Momo.DataCase - - alias Blogs.Publishing.Blog - - setup [:comments, :post_api_params] - - describe "update api decoder" do - test "only validates params that are present", context do - params = %{ - "id" => context.blog.id, - "name" => "blog name updated" - } - - assert {:ok, - %{ - id: context.blog.id, - name: "blog name updated" - }} == Blog.ApiUpdateDecoder.decode(params) - end - - test "validates relations", context do - params = %{ - "id" => context.blog.id, - "name" => "blog name updated", - "author" => %{"id" => context.author.id} - } - - assert {:ok, - %{ - id: context.blog.id, - name: "blog name updated", - author: context.author - }} == Blog.ApiUpdateDecoder.decode(params) - end - end -end diff --git a/test/momo/api/update_handler_test.exs b/test/momo/api/update_handler_test.exs deleted file mode 100644 index 44c97f92..00000000 --- a/test/momo/api/update_handler_test.exs +++ /dev/null @@ -1,52 +0,0 @@ -defmodule Momo.Api.UpdateHandlerTest do - use Momo.DataCase - - alias Blogs.Publishing.Blog - - setup [:comments] - - describe "update handler" do - test "does authorization", context do - params = %{ - "id" => context.blog.id, - "name" => "blog name updated" - } - - guest = %{roles: [:guest]} - - :post - |> conn("/", params) - |> assign(:current_user, guest) - |> Blog.ApiUpdateHandler.execute([]) - |> json_response!(403) - end - - test "updates attributes", context do - params = %{ - "id" => context.blog.id, - "name" => "blog name updated" - } - - author = %{roles: [:user], id: context.author.id} - - resp = - :post - |> conn("/", params) - |> assign(:current_user, author) - |> Blog.ApiUpdateHandler.execute([]) - |> json_response!(200) - - published = context.blog.published - id = context.blog.id - author_id = context.author.id - - assert %{ - "name" => "blog name updated", - "published" => ^published, - "id" => ^id, - "author" => %{"id" => ^author_id}, - "theme" => nil - } = resp - end - end -end diff --git a/test/momo/app_test.exs b/test/momo/app_test.exs index d94b05eb..faf7915c 100644 --- a/test/momo/app_test.exs +++ b/test/momo/app_test.exs @@ -1,20 +1,92 @@ defmodule Momo.AppTest do - use ExUnit.Case + use Momo.DataCase - alias Blogs.App + alias Blogs.Values.UserEmail + alias Blogs.Values.UserId describe "roles_from_context/1" do test "returns the roles from the context" do roles = [:admin] context = %{current_user: %{roles: roles}} - assert {:ok, roles} == App.roles_from_context(context) + assert {:ok, roles} == Blogs.App.roles_from_context(context) end test "returns an error if the path to the roles is not found in the context" do context = %{} - assert {:error, :no_such_roles_path} == App.roles_from_context(context) + assert {:error, :no_such_roles_path} == Blogs.App.roles_from_context(context) + end + end + + describe "shortest_path/2" do + test "evaluates the shortest to an attribute" do + assert [:body] == Blogs.App.get_shortest_path(:comment, :body) + end + + test "evaluates the shortest to an ancestor" do + assert [:author] == Blogs.App.get_shortest_path(:comment, :author) + end + + test "evaluates the shortest to an attribute in a parent" do + assert [:post, :locked] == Blogs.App.get_shortest_path(:comment, :locked) + end + + test "evaluates the shortest to an attribute in an ancestor" do + assert [:post, :blog, :public] == Blogs.App.get_shortest_path(:comment, :public) + end + end + + describe "paths/2" do + test "returns all paths between an model and an ancestor" do + assert [[:author], [:post, :author], [:post, :blog, :author]] == + Blogs.App.get_paths(:comment, :author) + end + end + + describe "map/3" do + test "maps input to output value using mappings" do + input = %{id: "1"} + assert {:ok, user_id} = Blogs.App.map(Map, UserId, input) + assert user_id.__struct__ == UserId + assert user_id.user_id == "1" + end + + test "maps multiple items when using mappings" do + inputs = [%{id: "1"}, %{id: "2"}] + assert {:ok, [user_id1, user_id2]} = Blogs.App.map(Map, UserId, inputs) + assert user_id1.__struct__ == UserId + assert user_id1.user_id == "1" + assert user_id2.__struct__ == UserId + assert user_id2.user_id == "2" + end + + test "validates the output when using mappings" do + input = %{id: 1} + assert {:error, reason} = Blogs.App.map(Map, UserId, input) + assert errors_on(reason) == %{user_id: ["is invalid"]} + end + + test "maps input to output value also when not using mappings" do + input = %{email: "foo@bar"} + assert {:ok, user_email} = Blogs.App.map(Map, UserEmail, input) + assert user_email.__struct__ == UserEmail + assert user_email.email == "foo@bar" + end + + test "maps multiple inputs when not using mappings" do + inputs = [%{email: "foo@bar"}, %{email: "bar@bar"}] + assert {:ok, [user_email1, user_email2]} = Blogs.App.map(Map, UserEmail, inputs) + assert user_email1.__struct__ == UserEmail + assert user_email1.email == "foo@bar" + assert user_email2.__struct__ == UserEmail + assert user_email2.email == "bar@bar" + end + + test "validate the output even if a mapping is not defined" do + input = %{email: 1} + assert {:error, reason} = Blogs.App.map(Map, UserEmail, input) + assert errors_on(reason) == %{email: ["is invalid"]} end end end diff --git a/test/momo/command_test.exs b/test/momo/command_test.exs index 908f6dab..bf45585d 100644 --- a/test/momo/command_test.exs +++ b/test/momo/command_test.exs @@ -1,10 +1,10 @@ defmodule Momo.CommandTest do use Momo.DataCase - alias Blogs.Accounts.Commands.{ExpireCredentials, RemindPassword, RegisterUser} - alias Blogs.Accounts.User - alias Blogs.Accounts.Values.UserId - alias Blogs.Accounts.Events.UserRegistered + alias Blogs.Commands.RegisterUser + alias Blogs.Commands.RemindPassword + alias Blogs.Models.User + alias Blogs.Events.UserRegistered describe "allowed?/1" do test "always allows if the command has no policies" do @@ -45,36 +45,66 @@ defmodule Momo.CommandTest do end describe "execute/2" do - test "returns a result and a list of events" do + test "applies authorization if the context is empty" do params = %User{id: uuid(), email: "test@gmail.com", external_id: uuid()} context = %{} - assert {:ok, user, [event]} = RegisterUser.execute(params, context) + assert {:error, :unauthorized} = RegisterUser.execute(params, context) + end + + test "applies authorization based on policies" do + params = %{email: "test@example.com", external_id: uuid(), id: uuid()} + context = %{current_user: %{roles: [:admin]}} + + assert {:error, :unauthorized} == RegisterUser.execute(params, context) + assert 0 == Blogs.Repo.aggregate(User, :count) + + refute_event_published(UserRegistered) + end + test "can skip authorization" do + params = %User{id: uuid(), email: "test@gmail.com", external_id: uuid()} + context = %{authorization: :skip} + + assert {:ok, user} = RegisterUser.execute(params, context) assert user.id == params.id assert user.email == params.email - assert %UserRegistered{} = event - assert event.user_id == user.id - assert event.registered_at == user.inserted_at + assert_event_published(UserRegistered) end - test "returns the input params by default" do - params = %UserId{user_id: uuid()} - context = %{} + test "does not publish events if explicit conditions are matched" do + params = %{email: "fake@gmail.com", external_id: uuid(), id: uuid()} + context = %{current_user: %{roles: [:guest]}} - assert {:ok, result, events} = RemindPassword.execute(params, context) - assert result == params - assert events == [] + assert {:ok, _user} = RegisterUser.execute(params, context) + refute_event_published(UserRegistered) end - test "returns one event per item returned" do - params = %UserId{user_id: uuid()} - context = %{} + test "accepts value structs as parameters" do + params = %User{email: "test@example.com", external_id: uuid(), id: uuid()} + context = %{current_user: %{roles: [:guest]}} + + assert {:ok, _user} = RegisterUser.execute(params, context) + refute_event_published(UserRegistered) + end + + test "accepts keyword lists as parameters" do + params = [email: "test@example.com", external_id: uuid(), id: uuid()] + context = %{current_user: %{roles: [:guest]}} + + assert {:ok, _user} = RegisterUser.execute(params, context) + refute_event_published(UserRegistered) + end + + test "rollbacks the transaction if the command is atomic and the handler fails" do + params = %{email: "foo@bar.com", external_id: uuid(), id: uuid()} + context = %{current_user: %{roles: [:guest]}} + + assert {:error, :invalid_email} = RegisterUser.execute(params, context) + assert 0 == Blogs.Repo.aggregate(User, :count) - assert {:ok, credentials, events} = ExpireCredentials.execute(params, context) - assert 2 == length(credentials) - assert 2 == length(events) + refute_event_published(UserRegistered) end end end diff --git a/test/momo/endpoint/api_test.exs b/test/momo/endpoint/api_test.exs deleted file mode 100644 index d9d67c84..00000000 --- a/test/momo/endpoint/api_test.exs +++ /dev/null @@ -1,171 +0,0 @@ -defmodule Momo.Endpoint.ApiTest do - use Momo.DataCase - - alias Blogs.Publishing.Comment - - setup [:comments] - - describe "an endpoint" do - test "routes json api read requests", context do - id = context.blog.id - - headers = %{ - "authorization" => "user " <> context.author.id - } - - resp = - "/api/publishing/blogs/#{id}" - |> get(headers: headers) - |> json_response!(200) - - author_id = context.author.id - blog_id = context.blog.id - - assert %{ - "name" => "elixir blog", - "published" => true, - "id" => ^blog_id, - "author" => %{"id" => ^author_id} - } = resp - end - - test "routes json api list requests", context do - headers = %{ - "authorization" => "user " <> context.author.id - } - - resp = - "/api/publishing/blogs" - |> get(headers: headers) - |> json_response!(200) - - author_id = context.author.id - blog_id = context.blog.id - - assert %{ - "limit" => 50, - "total_count" => 1, - "items" => [ - %{ - "name" => "elixir blog", - "published" => true, - "id" => ^blog_id, - "author" => %{"id" => ^author_id} - } - ] - } = resp - end - - test "routes json api list children requests", context do - headers = %{ - "authorization" => "user " <> context.author.id - } - - resp = - "/api/publishing/authors/#{context.author.id}/blogs?sort=name:desc" - |> get(headers: headers) - |> json_response!(200) - - author_id = context.author.id - blog_id = context.blog.id - - assert %{ - "limit" => 50, - "total_count" => 1, - "items" => [ - %{ - "name" => "elixir blog", - "published" => true, - "id" => ^blog_id, - "author" => %{"id" => ^author_id} - } - ] - } = resp - end - - test "routes json api list children requests with queries", context do - headers = %{ - "authorization" => "user " <> context.author.id - } - - resp = - "/api/publishing/authors/#{context.author.id}/blogs?query=published:false" - |> get(headers: headers) - |> json_response!(200) - - assert %{ - "limit" => 50, - "total_count" => 0, - "items" => [] - } == resp - end - - test "routes json api create requests", context do - headers = %{ - "authorization" => "user " <> context.author.id - } - - id = Ecto.UUID.generate() - - params = %{ - "id" => id, - "name" => "some other blog", - "published" => true, - "author" => %{"id" => context.author.id} - } - - resp = - "/api/publishing/blogs" - |> post(params, headers: headers) - |> json_response!(201) - - author_id = context.author.id - - assert %{ - "name" => "some other blog", - "published" => true, - "id" => ^id, - "author" => %{"id" => ^author_id}, - "theme" => nil - } = resp - end - - test "routes json api update requests", context do - headers = %{ - "authorization" => "user " <> context.author.id - } - - params = %{ - "name" => "updated blog name" - } - - resp = - "/api/publishing/blogs/#{context.blog.id}" - |> patch(params, headers: headers) - |> json_response!(200) - - author_id = context.author.id - blog_id = context.blog.id - - assert %{ - "name" => "updated blog name", - "published" => true, - "id" => ^blog_id, - "author" => %{"id" => ^author_id}, - "theme" => nil - } = resp - end - - test "routes json api delete requests", context do - headers = %{ - "authorization" => "user " <> context.author.id - } - - "/api/publishing/comments/#{context.comment1.id}" - |> delete(headers: headers) - |> json_response!(204) - - assert {:error, :not_found} == Comment.fetch(context.comment1.id) - end - end -end diff --git a/test/momo/enum_test.exs b/test/momo/enum_test.exs index 9245d8be..c82ed18f 100644 --- a/test/momo/enum_test.exs +++ b/test/momo/enum_test.exs @@ -1,7 +1,7 @@ defmodule Momo.EnumTest do use ExUnit.Case - alias Blogs.Accounts.Enums.CredentialType + alias Blogs.Enums.CredentialType describe "values/0" do test "returns all values" do diff --git a/test/momo/event_test.exs b/test/momo/event_test.exs index bc231115..3fa47eb3 100644 --- a/test/momo/event_test.exs +++ b/test/momo/event_test.exs @@ -1,8 +1,8 @@ defmodule Momo.EventTest do use ExUnit.Case - alias Blogs.Accounts.Events.UserRegistered - alias Blogs.Accounts.Events.UsersLocked + alias Blogs.Events.UserRegistered + alias Blogs.Events.UsersLocked describe "new/1" do test "creates event with valid required data" do diff --git a/test/momo/feature/create_action_test.exs b/test/momo/feature/create_action_test.exs deleted file mode 100644 index 52fa9158..00000000 --- a/test/momo/feature/create_action_test.exs +++ /dev/null @@ -1,76 +0,0 @@ -defmodule Momo.Feature.CreateActionTest do - use Momo.DataCase - - alias Blogs.Publishing - - setup [:comments, :current_user] - - describe "create action" do - test "requires an id", context do - params = %{id: uuid(), name: "author"} - ctx = guest(context).params - - assert {:ok, author} = Publishing.create_author(params, ctx) - assert author.id == params.id - assert author.name == params.name - end - - test "requires parent relations", context do - params = %{id: uuid(), name: "blog", published: true, author: context.author} - ctx = author(context).params - - assert {:ok, blog} = Publishing.create_blog(params, ctx) - assert blog.author_id == context.author.id - end - - test "creates children too", context do - author = context.author - - params = %{ - id: uuid(), - name: "an elixir blog", - published: true, - author: author, - posts: [ - %{ - id: uuid(), - title: "some comment", - author: author, - published: true, - locked: false, - deleted: false - } - ] - } - - ctx = author(context).params - - assert {:ok, _blog} = Publishing.create_blog(params, ctx) - end - - test "returns unique contraints as validation errors", context do - params = %{name: "blog", published: true, author: context.author} - ctx = author(context).params - - assert {:ok, _} = Publishing.create_blog(params, ctx) - assert {:error, error} = Publishing.create_blog(params, ctx) - - assert error - |> errors_on() - |> Map.values() - |> List.flatten() - |> Enum.member?("has already been taken") - end - - test "merges entries on conflict", context do - attrs = %{name: "science"} - ctx = guest(context).params - - assert {:ok, theme1} = Publishing.create_theme(attrs, ctx) - assert {:ok, theme2} = Publishing.create_theme(attrs, ctx) - - assert theme1.id == theme2.id - assert theme1.name == theme2.name - end - end -end diff --git a/test/momo/feature/delete_action_test.exs b/test/momo/feature/delete_action_test.exs deleted file mode 100644 index c4ee5749..00000000 --- a/test/momo/feature/delete_action_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Momo.Feature.DeleteActionTest do - use Momo.DataCase - - alias Blogs.Publishing - - setup [:comments, :current_user] - - describe "delete action" do - test "does not models that have children", %{params: params, blog: blog} do - assert {:error, changeset} = Publishing.delete_blog(blog, params) - assert "has children" in errors_on(changeset).blog_id - end - - test "deletes models that have no children", %{ - params: params, - blog: blog, - post: post, - comment1: c1, - comment2: c2, - comment3: c3 - } do - assert :ok = Publishing.Comment.delete(c1) - assert :ok = Publishing.Comment.delete(c2) - assert :ok = Publishing.Comment.delete(c3) - assert :ok = Publishing.Post.delete(post) - - assert :ok == Publishing.delete_blog(blog, params) - assert {:error, :not_found} = Publishing.read_blog(blog.id, params) - end - - test "refuses access", %{blog: blog} do - context = %{current_user: %{roles: [:guest]}} - - assert {:error, :forbidden} = Publishing.delete_blog(blog, context) - end - end -end diff --git a/test/momo/feature/paths_test.exs b/test/momo/feature/paths_test.exs deleted file mode 100644 index feba92f5..00000000 --- a/test/momo/feature/paths_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Momo.Feature.PathsTest do - use ExUnit.Case - - alias Blogs.Publishing - - describe "shortest_path/2" do - test "evaluates the shortest to an attribute" do - assert [:body] == Publishing.get_shortest_path(:comment, :body) - end - - test "evaluates the shortest to an ancestor" do - assert [:author] == Publishing.get_shortest_path(:comment, :author) - end - - test "evaluates the shortest to an attribute in a parent" do - assert [:post, :locked] == Publishing.get_shortest_path(:comment, :locked) - end - - test "evaluates the shortest to an attribute in an ancestor" do - assert [:post, :blog, :public] == Publishing.get_shortest_path(:comment, :public) - end - end - - describe "paths/2" do - test "returns all paths between an model and an ancestor" do - assert [[:author], [:post, :author], [:post, :blog, :author]] == - Publishing.get_paths(:comment, :author) - end - end -end diff --git a/test/momo/feature/read_action_test.exs b/test/momo/feature/read_action_test.exs deleted file mode 100644 index f2f930df..00000000 --- a/test/momo/feature/read_action_test.exs +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Momo.Feature.ReadActionTest do - use Momo.DataCase - - alias Blogs.{Accounts, Publishing} - - setup [:comments, :current_user] - - describe "read by id action" do - test "returns models", %{params: params, blog: blog} do - assert {:ok, _blog} = Publishing.read_blog(blog.id, params) - end - - test "refuses access", %{blog: blog} = context do - params = guest(context).params - - assert {:error, :forbidden} = Publishing.read_blog(blog.id, params) - end - - test "returns model with preloaded relations", %{blog: blog, author: author} do - assert {:ok, blog} = Publishing.read_blog(blog.id, %{preload: [:author]}) - assert blog.author == author - end - end - - describe "read by unique key" do - test "returns the model", %{params: params, user: user} do - assert {:ok, user} == Accounts.read_user_by_email(user.email, params) - end - - test "returns an error whent not found", %{params: params} do - assert {:error, :not_found} == Accounts.read_user_by_email("unknown@email", params) - end - end -end diff --git a/test/momo/feature/update_action_test.exs b/test/momo/feature/update_action_test.exs deleted file mode 100644 index 15e063d5..00000000 --- a/test/momo/feature/update_action_test.exs +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Momo.Feature.UpdateActionTest do - use Momo.DataCase - - alias Blogs.Publishing - - setup [:comments, :current_user] - - describe "update action" do - test "updates attributes", %{blog: blog} = context do - attrs = %{published: false} - ctx = author(context).params - - assert {:ok, updated_blog} = Publishing.update_blog(blog, attrs, ctx) - refute updated_blog.published - end - - test "refuses access", %{blog: blog} = context do - attrs = %{published: false} - ctx = other_user(context).params - - assert {:error, :forbidden} = Publishing.update_blog(blog, attrs, ctx) - end - end -end diff --git a/test/momo/feature_test.exs b/test/momo/feature_test.exs deleted file mode 100644 index 4e63423c..00000000 --- a/test/momo/feature_test.exs +++ /dev/null @@ -1,254 +0,0 @@ -defmodule Momo.FeatureTest do - use Momo.DataCase - - alias Blogs.Accounts - alias Blogs.Accounts.Events.UserRegistered - alias Blogs.Accounts.Onboarding - alias Blogs.Accounts.User - alias Blogs.Accounts.Values.UserId - alias Blogs.Accounts.Values.UserEmail - - describe "command functions" do - test "invoke the handler if the command is allowed" do - params = %{email: "test@example.com", external_id: uuid(), id: uuid()} - context = %{current_user: %{roles: [:guest]}} - - assert {:ok, _user} = Accounts.register_user(params, context) - refute_event_published(UserRegistered) - end - - test "publishes events if explicit conditions are matched" do - params = %{email: "test@gmail.com", external_id: uuid(), id: uuid()} - context = %{current_user: %{roles: [:guest]}} - - assert {:ok, _user} = Accounts.register_user(params, context) - assert_event_published(UserRegistered) - end - - test "does not publish events if explicit conditions are matched" do - params = %{email: "fake@gmail.com", external_id: uuid(), id: uuid()} - context = %{current_user: %{roles: [:guest]}} - - assert {:ok, _user} = Accounts.register_user(params, context) - refute_event_published(UserRegistered) - end - - test "accepts value structs as parameters" do - params = %User{email: "test@example.com", external_id: uuid(), id: uuid()} - context = %{current_user: %{roles: [:guest]}} - - assert {:ok, _user} = Accounts.register_user(params, context) - refute_event_published(UserRegistered) - end - - test "accepts keyword lists as parameters" do - params = [email: "test@example.com", external_id: uuid(), id: uuid()] - context = %{current_user: %{roles: [:guest]}} - - assert {:ok, _user} = Accounts.register_user(params, context) - refute_event_published(UserRegistered) - end - - test "do not call the handler if the command is not allowed" do - params = %{email: "test@example.com", external_id: uuid(), id: uuid()} - context = %{current_user: %{roles: [:admin]}} - - assert {:error, :unauthorized} == Accounts.register_user(params, context) - assert 0 == Blogs.Repo.aggregate(Accounts.User, :count) - - refute_event_published(UserRegistered) - end - - test "rollbacks the transaction if the handler fails" do - params = %{email: "foo@bar.com", external_id: uuid(), id: uuid()} - context = %{current_user: %{roles: [:guest]}} - - assert {:error, :invalid_email} = Accounts.register_user(params, context) - assert 0 == Blogs.Repo.aggregate(Accounts.User, :count) - - refute_event_published(UserRegistered) - end - end - - describe "query functions" do - test "returns nothing, if no roles are present in the context" do - {:ok, _} = - Accounts.User.create( - id: uuid(), - email: "foo@bar", - public: true, - external_id: uuid() - ) - - assert [] == Accounts.get_users() - end - - test "applies scopes" do - {:ok, _} = - Accounts.User.create( - id: uuid(), - email: "foo@bar", - public: true, - external_id: uuid() - ) - - {:ok, _} = - Accounts.User.create( - id: uuid(), - email: "bar@bar", - public: false, - external_id: uuid() - ) - - context = %{current_user: %{roles: [:guest]}} - assert [foo] = Accounts.get_users(context) - assert foo.email == "foo@bar" - end - - test "return validation errors on parameters" do - params = %{} - assert {:error, errors} = Accounts.get_user_by_email(params) - - assert errors_on(errors) == %{email: ["can't be blank"]} - end - - test "can returns a single item" do - {:ok, foo} = - Accounts.User.create( - id: uuid(), - email: "foo@bar", - public: true, - external_id: uuid() - ) - - {:ok, _} = - Accounts.User.create( - id: uuid(), - email: "bar@bar", - public: false, - external_id: uuid() - ) - - params = %{"email" => foo.email} - context = %{current_user: %{roles: [:user]}} - - assert {:ok, foo} = Accounts.get_user_by_email(params, context) - assert foo.email == "foo@bar" - end - - test "returns an error if the item is not found" do - params = %{"email" => "bar@bar"} - context = %{current_user: %{roles: [:user]}} - - assert {:error, :not_found} = Accounts.get_user_by_email(params, context) - end - - test "accept keyword lists as parameters" do - context = %{current_user: %{roles: [:user]}} - params = [email: "bar@bar"] - - assert {:error, :not_found} = Accounts.get_user_by_email(params, context) - end - - test "can execute custom queries on read models" do - assert [item] = Accounts.get_user_ids() - - assert is_struct(item) - assert item.__struct__ == UserId - assert item.user_id - end - - test "sorts results" do - assert {:ok, o1} = Onboarding.create(id: uuid(), user_id: uuid(), steps_pending: 1) - assert {:ok, o2} = Onboarding.create(id: uuid(), user_id: uuid(), steps_pending: 3) - - assert [^o2, ^o1] = Accounts.get_onboardings() - end - - test "preloads associations by default" do - {:ok, user} = - Accounts.User.create( - id: uuid(), - email: "foo@bar", - public: true, - external_id: uuid() - ) - - params = %{"email" => user.email} - context = %{current_user: %{roles: [:user]}} - - assert {:ok, user} = Accounts.get_user_by_email(params, context) - assert user.credentials == [] - end - - test "support lists of strings as parameters" do - {:ok, _} = - Accounts.User.create( - id: uuid(), - email: "foo@bar", - public: true, - external_id: uuid() - ) - - {:ok, _} = - Accounts.User.create( - id: uuid(), - email: "bar@bar", - public: false, - external_id: uuid() - ) - - context = %{current_user: %{roles: [:user]}} - params = [email: ["foo@bar", "bar@bar"]] - - assert users = Accounts.get_users_by_emails(params, context) - assert length(users) == 2 - end - end - - describe "map/3" do - test "maps input to output value using mappings" do - input = %{id: "1"} - assert {:ok, user_id} = Accounts.map(Map, UserId, input) - assert user_id.__struct__ == UserId - assert user_id.user_id == "1" - end - - test "maps multiple items when using mappings" do - inputs = [%{id: "1"}, %{id: "2"}] - assert {:ok, [user_id1, user_id2]} = Accounts.map(Map, UserId, inputs) - assert user_id1.__struct__ == UserId - assert user_id1.user_id == "1" - assert user_id2.__struct__ == UserId - assert user_id2.user_id == "2" - end - - test "validates the output when using mappings" do - input = %{id: 1} - assert {:error, reason} = Accounts.map(Map, UserId, input) - assert errors_on(reason) == %{user_id: ["is invalid"]} - end - - test "maps input to output value also when not using mappings" do - input = %{email: "foo@bar"} - assert {:ok, user_email} = Accounts.map(Map, UserEmail, input) - assert user_email.__struct__ == UserEmail - assert user_email.email == "foo@bar" - end - - test "maps multiple inputs when not using mappings" do - inputs = [%{email: "foo@bar"}, %{email: "bar@bar"}] - assert {:ok, [user_email1, user_email2]} = Accounts.map(Map, UserEmail, inputs) - assert user_email1.__struct__ == UserEmail - assert user_email1.email == "foo@bar" - assert user_email2.__struct__ == UserEmail - assert user_email2.email == "bar@bar" - end - - test "validate the output even if a mapping is not defined" do - input = %{email: 1} - assert {:error, reason} = Accounts.map(Map, UserEmail, input) - assert errors_on(reason) == %{email: ["is invalid"]} - end - end -end diff --git a/test/momo/flow_test.exs b/test/momo/flow_test.exs index b9e549e8..2321406b 100644 --- a/test/momo/flow_test.exs +++ b/test/momo/flow_test.exs @@ -1,8 +1,8 @@ defmodule Momo.FlowTest do use Momo.DataCase - alias Blogs.Accounts.Flows.Onboarding - alias Blogs.Accounts.Values.UserId + alias Blogs.Flows.Onboarding + alias Blogs.Values.UserId describe "execute/2" do test "Creates a model entry and enqueues jobs" do diff --git a/test/momo/mapping_test.exs b/test/momo/mapping_test.exs index 17adbc11..4db1365d 100644 --- a/test/momo/mapping_test.exs +++ b/test/momo/mapping_test.exs @@ -1,9 +1,9 @@ defmodule Momo.MappingTest do use ExUnit.Case - alias Blogs.Accounts.Mappings.UserRegisteredFromUser - alias Blogs.Accounts.User - alias Blogs.Accounts.Events.UserRegistered + alias Blogs.Mappings.UserRegisteredFromUser + alias Blogs.Models.User + alias Blogs.Events.UserRegistered describe "map/1" do test "copies data from a source value to a target value" do diff --git a/test/momo/migrations/alter_table_test.exs b/test/momo/migrations/alter_table_test.exs index 88e20cfc..7552a36b 100644 --- a/test/momo/migrations/alter_table_test.exs +++ b/test/momo/migrations/alter_table_test.exs @@ -10,7 +10,7 @@ defmodule Momo.Migrations.AlterTableTest do use Ecto.Migration def up do - create table(:themes, prefix: :publishing, primary_key: false) do + create table(:themes, prefix: nil, primary_key: false) do end end end @@ -18,7 +18,7 @@ defmodule Momo.Migrations.AlterTableTest do ] |> generate_migrations() |> assert_migrations([ - "alter(table(:themes, prefix: :publishing)) do", + "alter(table(:themes)) do", "add(:id, :binary_id, primary_key: true, null: false)", "add(:name, :string, null: false)" ]) @@ -31,7 +31,7 @@ defmodule Momo.Migrations.AlterTableTest do use Ecto.Migration def up do - create table(:themes, prefix: :publishing, primary_key: false) do + create table(:themes, prefix: nil, primary_key: false) do add(:id, :binary_id, primary_key: true, null: false) add(:name, :string, null: false) add(:closed, :boolean, null: true) @@ -42,7 +42,7 @@ defmodule Momo.Migrations.AlterTableTest do ] |> generate_migrations() |> assert_migrations([ - "alter(table(:themes, prefix: :publishing)) do", + "alter(table(:themes)) do", "remove(:closed)" ]) end @@ -54,9 +54,9 @@ defmodule Momo.Migrations.AlterTableTest do use Ecto.Migration def up do - create(table(:themes, prefix: :publishing, primary_key: false)) do + create(table(:themes, prefix: nil, primary_key: false)) do end - alter(table(:themes, prefix: :publishing)) do + alter(table(:themes, prefix: nil)) do add(:id, :binary_id, primary_key: true, null: false) add(:name, :string, null: false) end @@ -66,7 +66,7 @@ defmodule Momo.Migrations.AlterTableTest do ] |> generate_migrations() |> refute_migrations([ - "alter(table(:themes, prefix: :publishing)) do" + "alter(table(:themes, prefix: nil)) do" ]) end end diff --git a/test/momo/migrations/create_index_test.exs b/test/momo/migrations/create_index_test.exs index cae2070a..b12b924c 100644 --- a/test/momo/migrations/create_index_test.exs +++ b/test/momo/migrations/create_index_test.exs @@ -7,10 +7,10 @@ defmodule Momo.Migrations.CreateIndexTest do migration = generate_migrations() assert migration =~ - "create(\n unique_index(:blogs, [:author_id, :name],\n name: :blogs_author_id_name_idx,\n prefix: :publishing\n" + "create(unique_index(:blogs, [:author_id, :name], name: :blogs_author_id_name_idx)" assert migration =~ - "create(unique_index(:themes, [:name], name: :themes_name_idx, prefix: :publishing))" + "create(unique_index(:themes, [:name], name: :themes_name_idx" end end end diff --git a/test/momo/migrations/create_schema_test.exs b/test/momo/migrations/create_schema_test.exs index 4ed19e5e..37c20d44 100644 --- a/test/momo/migrations/create_schema_test.exs +++ b/test/momo/migrations/create_schema_test.exs @@ -3,9 +3,10 @@ defmodule Momo.Migrations.CreateSchemaTest do import MigrationHelper describe "migrations" do - test "create schemas for new features" do + test "create tables without schema prefixes" do migration = generate_migrations() - assert migration =~ "execute(\"CREATE SCHEMA accounts\")" + assert migration =~ "create(table(:users, primary_key: false))" + assert migration =~ "create(table(:credentials, primary_key: false))" end end end diff --git a/test/momo/migrations/create_table_test.exs b/test/momo/migrations/create_table_test.exs index ad04e15c..e4d5eb27 100644 --- a/test/momo/migrations/create_table_test.exs +++ b/test/momo/migrations/create_table_test.exs @@ -5,8 +5,8 @@ defmodule Momo.Migrations.CreateTableTest do describe "migrations" do test "create tables in the right context" do migrations = generate_migrations() - assert migrations =~ "create(table(:blogs, prefix: :publishing" - assert migrations =~ "create(table(:users, prefix: :accounts" + assert migrations =~ "create(table(:blogs" + assert migrations =~ "create(table(:users" end test "do not create tables if they already exist in the context" do @@ -16,9 +16,9 @@ defmodule Momo.Migrations.CreateTableTest do use Ecto.Migration def up do - create table(:blogs, prefix: :publishing, primary_key: false) do + create table(:blogs, prefix: nil, primary_key: false) do end - create table(:users, prefix: :accounts, primary_key: false) do + create table(:users, prefix: nil, primary_key: false) do end end end @@ -30,24 +30,6 @@ defmodule Momo.Migrations.CreateTableTest do refute migrations =~ "create(table(:users" end - test "do create tables if they don't exist in the context" do - existing = [ - """ - defmodule Momo.Migration.V1 do - use Ecto.Migration - - def up do - create table(:users, prefix: :other, primary_key: false) do - end - end - end - """ - ] - - migrations = generate_migrations(existing) - assert migrations =~ "create(table(:users, prefix: :accounts" - end - test "store timestamps as utc datetimes" do migrations = generate_migrations() assert migrations =~ "add(:published_at, :utc_datetime" diff --git a/test/momo/migrations/drop_index_test.exs b/test/momo/migrations/drop_index_test.exs index d27bce91..49e506ef 100644 --- a/test/momo/migrations/drop_index_test.exs +++ b/test/momo/migrations/drop_index_test.exs @@ -17,7 +17,7 @@ defmodule Momo.Migrations.DropIndexTest do ] |> generate_migrations() |> assert_migrations([ - "drop_if_exists(index(:blogs, [], name: :blogs_id_name_idx, prefix: :publishing))" + "drop_if_exists(index(:blogs, [], name: :blogs_id_name_idx))" ]) end diff --git a/test/momo/migrations/drop_schema_test.exs b/test/momo/migrations/drop_schema_test.exs deleted file mode 100644 index c2327132..00000000 --- a/test/momo/migrations/drop_schema_test.exs +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Momo.Migrations.DropSchemaTest do - use ExUnit.Case - import MigrationHelper - - describe "migrations" do - test "drops schemas for obsolete features" do - existing = [ - """ - defmodule Momo.Migration.V1 do - use Ecto.Migration - - def up do - execute("CREATE SCHEMA monitoring") - end - end - """ - ] - - migration = generate_migrations(existing) - assert migration =~ "execute(\"DROP SCHEMA monitoring\")" - end - end -end diff --git a/test/momo/migrations/drop_table_test.exs b/test/momo/migrations/drop_table_test.exs index a91ab5ce..22e71804 100644 --- a/test/momo/migrations/drop_table_test.exs +++ b/test/momo/migrations/drop_table_test.exs @@ -18,7 +18,7 @@ defmodule Momo.Migrations.DropTableTest do ] migrations = generate_migrations(existing) - assert migrations =~ "drop_if_exists(table(:emails, prefix: :notifications" + assert migrations =~ "drop_if_exists(table(:emails" end end end diff --git a/test/momo/model/attribute_test.exs b/test/momo/model/attribute_test.exs deleted file mode 100644 index 689872af..00000000 --- a/test/momo/model/attribute_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Momo.Model.AttributeTest do - use ExUnit.Case - - alias Blogs.Publishing.Blog - alias Momo.Model.Attribute - - describe "models" do - test "have a list of attributes" do - for attr <- Blog.attributes() do - assert {:ok, %Attribute{} = ^attr} = Blog.field(attr.name) - end - end - end -end diff --git a/test/momo/model/create_test.exs b/test/momo/model/create_test.exs deleted file mode 100644 index 114c1186..00000000 --- a/test/momo/model/create_test.exs +++ /dev/null @@ -1,67 +0,0 @@ -defmodule Momo.Model.CreateTest do - use Momo.DataCase - - alias Blogs.Accounts.User - alias Blogs.Publishing.Author - alias Blogs.Publishing.Theme - - describe "create function" do - test "creates models" do - attrs = %{ - "id" => Ecto.UUID.generate(), - "name" => "john" - } - - assert {:ok, author} = Author.create(attrs) - assert author.name == "john" - end - - test "supports attributes as keyword lists" do - assert {:ok, author} = Author.create(id: Ecto.UUID.generate(), name: "john") - assert author.name == "john" - end - - test "support attribute with atom keys" do - attrs = %{ - id: Ecto.UUID.generate(), - name: "john" - } - - assert {:ok, author} = Author.create(attrs) - assert author.name == "john" - end - - test "validates inclusion of attribute values" do - attrs = %{ - "id" => Ecto.UUID.generate(), - "name" => "other" - } - - assert {:error, changeset} = Theme.create(attrs) - assert errors_on(changeset) == %{name: ["is invalid"]} - end - - test "validates ids" do - attrs = %{ - external_id: "1", - email: "foo@bar.com", - id: "2" - } - - assert {:error, changeset} = User.create(attrs) - - assert errors_on(changeset) == %{ - external_id: ["is not a valid UUID"], - id: ["is not a valid UUID"] - } - end - end - - describe "create_many/2" do - test "creates many models at once" do - authors = [%{name: "a1", profile: "publisher"}, %{name: "a2", profile: "publisher"}] - - assert :ok = Author.create_many(authors) - end - end -end diff --git a/test/momo/model/key_test.exs b/test/momo/model/key_test.exs deleted file mode 100644 index 6e13af44..00000000 --- a/test/momo/model/key_test.exs +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Momo.Model.KeyTest do - use ExUnit.Case - - alias Blogs.Publishing.{Blog, Theme} - alias Blogs.Accounts.User - alias Momo.Model.{Attribute, Relation} - - describe "models" do - test "have keys as a combination of fields" do - assert [key] = Blog.keys() - assert Blog == key.model - assert [%Relation{name: :author}, %Attribute{name: :name}] = key.fields - assert key.unique? - end - - test "can have unique keys" do - assert [key] = Theme.keys() - assert key.unique? - end - - test "have primary keys of type binary_id" do - pk = User.primary_key() - assert :id == pk.name - assert :id == pk.kind - assert :binary_id == pk.storage - end - end -end diff --git a/test/momo/model/list_test.exs b/test/momo/model/list_test.exs deleted file mode 100644 index d2f2dfa4..00000000 --- a/test/momo/model/list_test.exs +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Momo.Model.ListTest do - use Momo.DataCase - - alias Blogs.Publishing.{Blog, Comment} - - import Ecto.Query - - setup [:comments] - - describe "list function" do - test "returns all blogs by default", %{blog: blog} do - assert page = Blog.list() - assert page.metadata.total_count == 1 - - assert [blog] == page.entries - end - - test "supports a blog queryable", %{blog: blog} do - query = from(b in Blog, where: b.name == ^blog.name) - assert page = Blog.list(query: query) - assert page.metadata.total_count == 1 - - assert [blog] == page.entries - - query = from(b in Blog, where: b.name != ^blog.name) - assert page = Blog.list(query: query) - assert page.metadata.total_count == 0 - - assert [] == page.entries - end - - test "supports preloads", %{post: post} do - assert page = Blog.list(preload: [:posts]) - - assert [blog] = page.entries - assert [post] == blog.posts - end - - test "supports pagination", %{comment1: c1, comment2: c2, comment3: c3} do - assert page = Comment.list(limit: 2) - assert page.metadata.total_count == 3 - assert cursor = page.metadata.after - assert [c1, c2] == page.entries - - assert page = Comment.list(limit: 2, after: cursor) - assert page.metadata.total_count == 3 - refute page.metadata.after - assert [c3] == page.entries - end - end -end diff --git a/test/momo/model/relation_test.exs b/test/momo/model/relation_test.exs deleted file mode 100644 index 23ce3baf..00000000 --- a/test/momo/model/relation_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Momo.Model.RelationTest do - use ExUnit.Case - - alias Blogs.Publishing.Blog - alias Momo.Model.Relation - - describe "models" do - test "have parent relations" do - for rel <- Blog.parents() do - assert {:ok, %Relation{} = ^rel} = Blog.field(rel.name) - end - end - end -end diff --git a/test/momo/model/virtual_test.exs b/test/momo/model/virtual_test.exs deleted file mode 100644 index 059a8e54..00000000 --- a/test/momo/model/virtual_test.exs +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Momo.Model.VirtualTest do - use ExUnit.Case - - alias Blogs.Notifications.Digest - - describe "models" do - test "can be virtual" do - assert Digest.virtual?() - end - - test "have a name" do - assert :digest == Digest.name() - end - end -end diff --git a/test/momo/model_test.exs b/test/momo/model_test.exs index 8f357fd4..eeaaab7a 100644 --- a/test/momo/model_test.exs +++ b/test/momo/model_test.exs @@ -1,19 +1,132 @@ defmodule Momo.ModelTest do use Momo.DataCase - alias Blogs.Accounts - alias Blogs.Accounts.User - alias Blogs.Accounts.Onboarding - alias Blogs.Accounts.Credential + alias Blogs + + alias Blogs.Models.{ + Author, + Blog, + Digest, + User, + Theme, + Onboarding, + Credential + } + + alias Blogs.Queries.GetOnboardings + + describe "name/0" do + test "return the atom representation of the model" do + assert :blog == Blog.name() + end + end - describe "describe field/1" do + describe "field/1" do test "finds built-in fields" do assert {:ok, field} = User.field(:inserted_at) assert field.name == :inserted_at end end + describe "attributes/0" do + test "return a list of attributes" do + for attr <- Blog.attributes() do + assert {:ok, %Momo.Model.Attribute{} = ^attr} = Blog.field(attr.name) + end + end + end + + describe "keys/0" do + test "returns composite keys" do + assert [key] = Blog.keys() + assert Blog == key.model + + assert [%Momo.Model.Relation{name: :author}, %Momo.Model.Attribute{name: :name}] = + key.fields + + assert key.unique? + end + + test "returns unique keys" do + assert [key] = Theme.keys() + assert key.unique? + end + end + + describe "primary_key/0" do + test "returns its name and type" do + pk = User.primary_key() + assert :id == pk.name + assert :id == pk.kind + assert :binary_id == pk.storage + end + end + + describe "parents/0" do + test "returns relations" do + for rel <- Blog.parents() do + assert {:ok, %Momo.Model.Relation{} = ^rel} = Blog.field(rel.name) + end + end + end + + describe "virtual?/0" do + test "returns whether the model is managed" do + assert Digest.virtual?() + end + end + describe "create/1" do + test "creates models" do + attrs = %{ + "id" => Ecto.UUID.generate(), + "name" => "john" + } + + assert {:ok, author} = Author.create(attrs) + assert author.name == "john" + end + + test "supports attributes as keyword lists" do + assert {:ok, author} = Author.create(id: Ecto.UUID.generate(), name: "john") + assert author.name == "john" + end + + test "support attribute with atom keys" do + attrs = %{ + id: Ecto.UUID.generate(), + name: "john" + } + + assert {:ok, author} = Author.create(attrs) + assert author.name == "john" + end + + test "validates inclusion of attribute values" do + attrs = %{ + "id" => Ecto.UUID.generate(), + "name" => "other" + } + + assert {:error, changeset} = Theme.create(attrs) + assert errors_on(changeset) == %{name: ["is invalid"]} + end + + test "validates ids" do + attrs = %{ + external_id: "1", + email: "foo@bar.com", + id: "2" + } + + assert {:error, changeset} = User.create(attrs) + + assert errors_on(changeset) == %{ + external_id: ["is not a valid UUID"], + id: ["is not a valid UUID"] + } + end + test "allows timestamps to be manually modified" do two_days_ago = DateTime.utc_now() |> DateTime.add(-2 * 24 * 3600, :second) @@ -72,11 +185,16 @@ defmodule Momo.ModelTest do test "merges records when the unique key involves a parent relation" do assert {:ok, user} = - Blogs.Accounts.create_user(email: "foo@bar", external_id: uuid(), public: true) + User.create( + id: uuid(), + email: "foo@bar", + external_id: uuid(), + public: true + ) assert {:ok, cred1} = - Accounts.create_credential( - user: user, + Credential.create( + user_id: user.id, name: "password", value: "foo", enabled: true, @@ -84,8 +202,8 @@ defmodule Momo.ModelTest do ) assert {:ok, cred2} = - Accounts.create_credential( - user: user, + Credential.create( + user_id: user.id, name: "password", value: "bar", enabled: false, @@ -97,7 +215,7 @@ defmodule Momo.ModelTest do assert cred2.value == "bar" refute cred2.enabled - assert {:ok, ^cred2} = Blogs.Accounts.Credential.fetch(cred1.id) + assert {:ok, ^cred2} = Blogs.Models.Credential.fetch(cred1.id) end test "executes computations on fields" do @@ -108,33 +226,44 @@ defmodule Momo.ModelTest do end describe "create_many/1" do + test "creates many models at once" do + authors = [%{name: "a1", profile: "publisher"}, %{name: "a2", profile: "publisher"}] + + assert :ok = Author.create_many(authors) + end + test "merges records on conflict when the strategy is set" do user_id = uuid() - o1 = [ + onboarding1 = %{ id: uuid(), user_id: user_id, steps_pending: 2 - ] + } - o2 = [ + onboarding2 = %{ id: uuid(), user_id: user_id, steps_pending: 3 - ] + } - assert :ok = Onboarding.create_many([o1]) - assert :ok = Onboarding.create_many([o2]) + assert :ok = Onboarding.create_many([onboarding1]) + assert :ok = Onboarding.create_many([onboarding2]) - assert [onboarding] = Accounts.get_onboardings() + assert [onboarding] = GetOnboardings.execute() assert onboarding.steps_pending == 3 end end describe "read functions" do test "preload children relation when the option is set" do - assert {:ok, user} = - Accounts.create_user(email: "foo@bar", external_id: uuid(), public: true) + {:ok, user} = + User.create( + id: uuid(), + email: "foo@bar", + external_id: uuid(), + public: true + ) assert {:ok, user} = User.fetch(user.id) assert user.credentials == [] @@ -144,12 +273,17 @@ defmodule Momo.ModelTest do end test "do not preload relations by default" do - assert {:ok, user} = - Accounts.create_user(email: "foo@bar", external_id: uuid(), public: true) + {:ok, user} = + User.create( + id: uuid(), + email: "foo@bar", + external_id: uuid(), + public: true + ) assert {:ok, credential} = - Accounts.create_credential( - user: user, + Credential.create( + user_id: user.id, name: "password", value: "bar", enabled: false, @@ -166,8 +300,13 @@ defmodule Momo.ModelTest do describe "plain_map/1" do test "turns a model into a plain map with string keys" do - assert {:ok, user} = - Accounts.create_user(email: "foo@bar", external_id: uuid(), public: true) + {:ok, user} = + User.create( + id: uuid(), + email: "foo@bar", + external_id: uuid(), + public: true + ) assert %{ "email" => user.email, diff --git a/test/momo/query_builder_test.exs b/test/momo/query_builder_test.exs index fe39d07f..5100d5a5 100644 --- a/test/momo/query_builder_test.exs +++ b/test/momo/query_builder_test.exs @@ -3,7 +3,7 @@ defmodule Momo.QueryBuilderTest do alias Momo.QueryBuilder - alias Blogs.Publishing.{Author, Blog, Post} + alias Blogs.Models.{Author, Blog, Post} import Ecto.Query @@ -11,7 +11,7 @@ defmodule Momo.QueryBuilderTest do @select "SELECT p0.\"id\", p0.\"title\", p0.\"published_at\", p0.\"locked\", " <> "p0.\"published\", p0.\"deleted\", p0.\"blog_id\", p0.\"author_id\", p0.\"inserted_at\", " <> - "p0.\"updated_at\" FROM \"publishing\".\"posts\" AS p0" + "p0.\"updated_at\" FROM \"posts\" AS p0" test "supports no filters" do sql = @@ -75,7 +75,7 @@ defmodule Momo.QueryBuilderTest do |> to_sql() assert sql == - "SELECT p0.\"id\", p0.\"title\", p0.\"published_at\", p0.\"locked\", p0.\"published\", p0.\"deleted\", p0.\"blog_id\", p0.\"author_id\", p0.\"inserted_at\", p0.\"updated_at\" FROM \"publishing\".\"posts\" AS p0 WHERE (((p0.\"published\" = $1) AND (p0.\"locked\" = $2)) AND (p0.\"deleted\" = $3)) ORDER BY p0.\"id\", p0.\"inserted\" DESC" + "SELECT p0.\"id\", p0.\"title\", p0.\"published_at\", p0.\"locked\", p0.\"published\", p0.\"deleted\", p0.\"blog_id\", p0.\"author_id\", p0.\"inserted_at\", p0.\"updated_at\" FROM \"posts\" AS p0 WHERE (((p0.\"published\" = $1) AND (p0.\"locked\" = $2)) AND (p0.\"deleted\" = $3)) ORDER BY p0.\"id\", p0.\"inserted\" DESC" end test "combines 'and' and 'or'" do @@ -148,8 +148,8 @@ defmodule Momo.QueryBuilderTest do assert sql == @select <> - " INNER JOIN \"publishing\".\"blogs\" AS b1 ON p0.\"blog_id\" = b1.\"id\"" <> - " INNER JOIN \"publishing\".\"authors\" AS a2 ON b1.\"author_id\" = a2.\"id\"" + " INNER JOIN \"blogs\" AS b1 ON p0.\"blog_id\" = b1.\"id\"" <> + " INNER JOIN \"authors\" AS a2 ON b1.\"author_id\" = a2.\"id\"" end test "supports left joins" do @@ -163,7 +163,7 @@ defmodule Momo.QueryBuilderTest do assert sql == @select <> - " LEFT OUTER JOIN \"publishing\".\"blogs\" AS b1 ON p0.\"blog_id\" = b1.\"id\"" <> - " LEFT OUTER JOIN \"publishing\".\"authors\" AS a2 ON b1.\"author_id\" = a2.\"id\"" + " LEFT OUTER JOIN \"blogs\" AS b1 ON p0.\"blog_id\" = b1.\"id\"" <> + " LEFT OUTER JOIN \"authors\" AS a2 ON b1.\"author_id\" = a2.\"id\"" end end diff --git a/test/momo/query_test.exs b/test/momo/query_test.exs index 178c7ec1..a091091d 100644 --- a/test/momo/query_test.exs +++ b/test/momo/query_test.exs @@ -1,9 +1,12 @@ defmodule Momo.QueryTest do use Momo.DataCase - alias Blogs.Accounts.User + alias Blogs.Models.{ + Onboarding, + User + } - alias Blogs.Accounts.Queries.{ + alias Blogs.Queries.{ GetOnboardings, GetUsers, GetUserByEmail, @@ -11,6 +14,8 @@ defmodule Momo.QueryTest do GetUserIds } + alias Blogs.Values.UserId + alias Momo.Query describe "scope/1" do @@ -66,10 +71,145 @@ defmodule Momo.QueryTest do describe "execute/1" do test "is used when queries have no params" do + assert [item] = GetUserIds.execute() + + assert is_struct(item) + assert item.user_id + end + + test "returns nothing, if no roles are present in the context" do + {:ok, _} = + User.create( + id: uuid(), + email: "foo@bar", + public: true, + external_id: uuid() + ) + + assert [] == GetUsers.execute() + end + + test "applies scopes" do + {:ok, _} = + User.create( + id: uuid(), + email: "foo@bar", + public: true, + external_id: uuid() + ) + + {:ok, _} = + User.create( + id: uuid(), + email: "bar@bar", + public: false, + external_id: uuid() + ) + + context = %{current_user: %{roles: [:guest]}} + assert [foo] = GetUsers.execute(context) + assert foo.email == "foo@bar" + end + + test "return validation errors on invalid parameters" do + params = %{} context = %{} - assert [item] = GetUserIds.execute(context) + + assert {:error, %Ecto.Changeset{} = errors} = GetUserByEmail.execute(params, context) + assert errors_on(errors) == %{email: ["can't be blank"]} + end + + test "can returns a single item" do + {:ok, foo} = + User.create( + id: uuid(), + email: "foo@bar", + public: true, + external_id: uuid() + ) + + {:ok, _} = + User.create( + id: uuid(), + email: "bar@bar", + public: false, + external_id: uuid() + ) + + params = %{"email" => foo.email} + context = %{current_user: %{roles: [:user]}} + + assert {:ok, foo} = GetUserByEmail.execute(params, context) + assert foo.email == "foo@bar" + end + + test "returns an error if the item is not found" do + params = %{"email" => "bar@bar"} + context = %{current_user: %{roles: [:user]}} + + assert {:error, :not_found} = GetUserByEmail.execute(params, context) + end + + test "accept keyword lists as parameters" do + context = %{current_user: %{roles: [:user]}} + params = [email: "bar@bar"] + + assert {:error, :not_found} = GetUserByEmail.execute(params, context) + end + + test "can execute custom queries on read models" do + assert [item] = GetUserIds.execute() + + assert is_struct(item) + assert item.__struct__ == UserId assert item.user_id end + + test "sorts results" do + assert {:ok, o1} = Onboarding.create(id: uuid(), user_id: uuid(), steps_pending: 1) + assert {:ok, o2} = Onboarding.create(id: uuid(), user_id: uuid(), steps_pending: 3) + assert [^o2, ^o1] = GetOnboardings.execute() + end + + test "preloads associations by default" do + {:ok, user} = + User.create( + id: uuid(), + email: "foo@bar", + public: true, + external_id: uuid() + ) + + params = %{"email" => user.email} + context = %{current_user: %{roles: [:user]}} + + assert {:ok, user} = GetUserByEmail.execute(params, context) + assert user.credentials == [] + end + + test "support lists of strings as parameters" do + {:ok, _} = + User.create( + id: uuid(), + email: "foo@bar", + public: true, + external_id: uuid() + ) + + {:ok, _} = + User.create( + id: uuid(), + email: "bar@bar", + public: false, + external_id: uuid() + ) + + context = %{current_user: %{roles: [:user]}} + params = [email: ["foo@bar", "bar@bar"]] + + assert users = GetUsersByEmails.execute(params, context) + assert length(users) == 2 + end end describe "apply_filters/2" do diff --git a/test/momo/scope_test.exs b/test/momo/scope_test.exs index d2c0b126..4036955b 100644 --- a/test/momo/scope_test.exs +++ b/test/momo/scope_test.exs @@ -1,9 +1,8 @@ defmodule Momo.ScopeTest do use ExUnit.Case - alias Blogs.Accounts.Scopes.SelfAndNotLocked - alias Blogs.Publishing.{Blog, Post, Comment, Author} - + alias Blogs.Scopes.SelfAndNotLocked + alias Blogs.Models.{Blog, Post, Comment, Author} alias Momo.Scope alias Momo.Scope.Expression @@ -12,7 +11,7 @@ defmodule Momo.ScopeTest do scope do same do - path "published" + path("published") true end end @@ -23,7 +22,7 @@ defmodule Momo.ScopeTest do scope do same do - path "**.published" + path("**.published") true end end @@ -34,7 +33,7 @@ defmodule Momo.ScopeTest do scope do is_true do - path "published" + path("published") end end end @@ -44,7 +43,7 @@ defmodule Momo.ScopeTest do scope do is_true do - path "locked" + path("locked") end end end @@ -240,7 +239,7 @@ defmodule Momo.ScopeTest do scope do same do - path "blog.published" + path("blog.published") true end end @@ -249,7 +248,7 @@ defmodule Momo.ScopeTest do builder = Scope.query_builder(Post, IsBlogPublished) assert builder.joins == [ - {:join, {Blogs.Publishing.Blog, :post_blog, :id}, {:post, :blog_id}} + {:join, {Blogs.Models.Blog, :post_blog, :id}, {:post, :blog_id}} ] assert builder.filters == [{{:post_blog, :published}, :eq, true}] @@ -261,7 +260,7 @@ defmodule Momo.ScopeTest do scope do same do - path "blog.theme.name" + path("blog.theme.name") "Science" end end @@ -271,8 +270,7 @@ defmodule Momo.ScopeTest do assert builder.joins == [ {:join, {Blog, :post_blog, :id}, {:post, :blog_id}}, - {:left_join, {Blogs.Publishing.Theme, :post_blog_theme, :id}, - {:post_blog, :theme_id}} + {:left_join, {Blogs.Models.Theme, :post_blog_theme, :id}, {:post_blog, :theme_id}} ] assert builder.filters == [{{:post_blog_theme, :name}, :eq, "Science"}] @@ -284,7 +282,7 @@ defmodule Momo.ScopeTest do scope do same do - path "blog.author.name" + path("blog.author.name") "John" end end @@ -306,7 +304,7 @@ defmodule Momo.ScopeTest do scope do same do - path "post.author.name" + path("post.author.name") "John" end end @@ -317,7 +315,7 @@ defmodule Momo.ScopeTest do scope do same do - path "post.blog.author.name" + path("post.blog.author.name") "John" end end @@ -358,7 +356,7 @@ defmodule Momo.ScopeTest do scope do same do - path "blog" + path("blog") 1 end end @@ -374,7 +372,7 @@ defmodule Momo.ScopeTest do scope do same do - path "posts" + path("posts") 1 end end @@ -395,7 +393,7 @@ defmodule Momo.ScopeTest do scope do same do - path "posts.published" + path("posts.published") true end end diff --git a/test/momo/ui_test.exs b/test/momo/ui_test.exs index dc67bca2..e68b95cd 100644 --- a/test/momo/ui_test.exs +++ b/test/momo/ui_test.exs @@ -14,8 +14,20 @@ defmodule Momo.Ui.UiTest do assert [ {:get, "/admin/"}, {:get, "/"}, - {:get, "/blogs"} + {:get, "/blogs/:id"} ] == routes end + + test "routes requests" do + assert visit("/admin") =~ "Admin Page" + assert visit("/blogs/1") =~ "Welcome to my Blog 1" + assert visit("/blogs/2") =~ "No such blog" + assert visit("/") =~ "It works!" + end + end + + defp visit(path) do + conn = :get |> new_conn(path) |> Blogs.Ui.call() + conn.resp_body end end diff --git a/test/momo/value_test.exs b/test/momo/value_test.exs index b04a2888..5db3ffb8 100644 --- a/test/momo/value_test.exs +++ b/test/momo/value_test.exs @@ -2,8 +2,8 @@ defmodule Momo.ValueTest do use ExUnit.Case import Momo.ErrorsHelper - alias Blogs.Accounts.Values.UserEmails - alias Blogs.Accounts.Values.UserId + alias Blogs.Values.UserEmails + alias Blogs.Values.UserId describe "values" do test "don't have an id" do diff --git a/test/support/blogs/accounts.ex b/test/support/blogs/accounts.ex deleted file mode 100644 index eb8078ab..00000000 --- a/test/support/blogs/accounts.ex +++ /dev/null @@ -1,56 +0,0 @@ -defmodule Blogs.Accounts do - @moduledoc false - use Momo.Feature - - feature do - models do - Blogs.Accounts.Credential - Blogs.Accounts.Onboarding - Blogs.Accounts.User - end - - commands do - Blogs.Accounts.Commands.ExpireCredentials - Blogs.Accounts.Commands.RegisterUser - Blogs.Accounts.Commands.RemindPassword - Blogs.Accounts.Commands.EnableUser - Blogs.Accounts.Commands.SendWelcomeEmail - Blogs.Accounts.Commands.RequestFeedback - end - - queries do - Blogs.Accounts.Queries.GetOnboardings - Blogs.Accounts.Queries.GetUsers - Blogs.Accounts.Queries.GetUserByEmail - Blogs.Accounts.Queries.GetUsersByEmails - Blogs.Accounts.Queries.GetUserIds - end - - scopes do - Blogs.Accounts.Scopes.Self - end - - events do - Blogs.Accounts.Events.CredentialsExpired - Blogs.Accounts.Events.PasswordRemindedSent - Blogs.Accounts.Events.UserOnboarded - Blogs.Accounts.Events.UserRegistered - Blogs.Accounts.Events.UsersLocked - end - - flows do - Blogs.Accounts.Flows.Onboarding - end - - subscriptions do - Blogs.Accounts.Subscriptions.UserRegistrations - Blogs.Accounts.Subscriptions.UserOnboardings - end - - mappings do - Blogs.Accounts.Mappings.UserRegisteredFromUser - Blogs.Accounts.Mappings.UserIdFromMap - Blogs.Accounts.Mappings.CredentialExpiredFromCredential - end - end -end diff --git a/test/support/blogs/accounts/commands/enamble_user.ex b/test/support/blogs/accounts/commands/enamble_user.ex deleted file mode 100644 index dc3afccf..00000000 --- a/test/support/blogs/accounts/commands/enamble_user.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Blogs.Accounts.Commands.EnableUser do - @moduledoc false - use Momo.Command - - alias Blogs.Accounts.Values.UserId - - command params: UserId do - end - - def handle(_user, _context), do: :ok -end diff --git a/test/support/blogs/accounts/commands/register_user.ex b/test/support/blogs/accounts/commands/register_user.ex deleted file mode 100644 index 2162e7ce..00000000 --- a/test/support/blogs/accounts/commands/register_user.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Blogs.Accounts.Commands.RegisterUser do - @moduledoc false - - use Momo.Command - - alias Blogs.Accounts.User - alias Blogs.Accounts.Events.UserRegistered - alias Blogs.Accounts.Expressions.IsGmailAccount - alias Blogs.Accounts.Expressions.LooksFake - - command params: User, atomic: true do - policy role: :guest - - publish event: UserRegistered, if: IsGmailAccount, unless: LooksFake - end - - def handle(%{email: "foo@bar.com"}, _context), do: {:error, :invalid_email} - def handle(user, context), do: Blogs.Accounts.create_user(user, context) -end diff --git a/test/support/blogs/accounts/commands/remind_password.ex b/test/support/blogs/accounts/commands/remind_password.ex deleted file mode 100644 index 47cc48a3..00000000 --- a/test/support/blogs/accounts/commands/remind_password.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Blogs.Accounts.Commands.RemindPassword do - @moduledoc false - - use Momo.Command - - alias Blogs.Accounts.Scopes.SelfAndNotLocked - alias Blogs.Accounts.Values.UserId - - command params: UserId do - policy role: :user, scope: SelfAndNotLocked - end - - def handle(_params, _context), do: :ok -end diff --git a/test/support/blogs/accounts/commands/request_feedback.ex b/test/support/blogs/accounts/commands/request_feedback.ex deleted file mode 100644 index fd6c8ce3..00000000 --- a/test/support/blogs/accounts/commands/request_feedback.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule Blogs.Accounts.Commands.RequestFeedback do - @moduledoc false - use Momo.Command - alias Blogs.Accounts.Values.UserId - - command params: UserId do - end - - def handle(_user, _context), do: :ok -end diff --git a/test/support/blogs/accounts/commands/send_welcome_email.ex b/test/support/blogs/accounts/commands/send_welcome_email.ex deleted file mode 100644 index 1b5f5c05..00000000 --- a/test/support/blogs/accounts/commands/send_welcome_email.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Blogs.Accounts.Commands.SendWelcomeEmail do - @moduledoc false - use Momo.Command - - alias Blogs.Accounts.Values.UserId - - command params: UserId do - end - - def handle(_user, _context), do: :ok -end diff --git a/test/support/blogs/accounts/flows/onboarding.ex b/test/support/blogs/accounts/flows/onboarding.ex deleted file mode 100644 index 5eccf3f7..00000000 --- a/test/support/blogs/accounts/flows/onboarding.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Blogs.Accounts.Flows.Onboarding do - @moduledoc false - use Momo.Flow - - alias Blogs.Accounts.Commands.SendWelcomeEmail - alias Blogs.Accounts.Commands.EnableUser - - alias Blogs.Accounts.Onboarding - alias Blogs.Accounts.Values.UserId - alias Blogs.Accounts.Events.UserOnboarded - - flow model: Onboarding, params: UserId, publish: UserOnboarded do - steps do - SendWelcomeEmail - EnableUser - end - end -end diff --git a/test/support/blogs/accounts/queries/get_user_by_email.ex b/test/support/blogs/accounts/queries/get_user_by_email.ex deleted file mode 100644 index 1f90e318..00000000 --- a/test/support/blogs/accounts/queries/get_user_by_email.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Blogs.Accounts.Queries.GetUserByEmail do - @moduledoc false - use Momo.Query - - alias Blogs.Accounts.User - alias Blogs.Accounts.Values.UserEmail - - query params: UserEmail, returns: User do - policy role: :user - end -end diff --git a/test/support/blogs/accounts/scopes/self.ex b/test/support/blogs/accounts/scopes/self.ex deleted file mode 100644 index 549fbae8..00000000 --- a/test/support/blogs/accounts/scopes/self.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Blogs.Accounts.Scopes.Self do - @moduledoc false - use Momo.Scope - - scope do - same do - path "user.id" - path "current_user.id" - end - end -end diff --git a/test/support/blogs/accounts/scopes/self_and_not_locked.ex b/test/support/blogs/accounts/scopes/self_and_not_locked.ex deleted file mode 100644 index 9fd6db47..00000000 --- a/test/support/blogs/accounts/scopes/self_and_not_locked.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Blogs.Accounts.Scopes.SelfAndNotLocked do - @moduledoc false - use Momo.Scope - - scope do - all do - Blogs.Accounts.Scopes.Self - Blogs.Accounts.Scopes.NotLocked - end - end -end diff --git a/test/support/blogs/accounts/subscriptions/user_onboardings.ex b/test/support/blogs/accounts/subscriptions/user_onboardings.ex deleted file mode 100644 index 1fb2877f..00000000 --- a/test/support/blogs/accounts/subscriptions/user_onboardings.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Blogs.Accounts.Subscriptions.UserOnboardings do - @moduledoc false - use Momo.Subscription - - subscription( - on: Blogs.Accounts.Events.UserOnboarded, - perform: Blogs.Accounts.Commands.RequestFeedback - ) -end diff --git a/test/support/blogs/accounts/subscriptions/user_registrations.ex b/test/support/blogs/accounts/subscriptions/user_registrations.ex deleted file mode 100644 index cc926693..00000000 --- a/test/support/blogs/accounts/subscriptions/user_registrations.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Blogs.Accounts.Subscriptions.UserRegistrations do - @moduledoc false - use Momo.Subscription - - subscription( - on: Blogs.Accounts.Events.UserRegistered, - perform: Blogs.Accounts.Flows.Onboarding - ) -end diff --git a/test/support/blogs/api.ex b/test/support/blogs/api.ex index 1b7db0bd..11058487 100644 --- a/test/support/blogs/api.ex +++ b/test/support/blogs/api.ex @@ -8,7 +8,7 @@ defmodule Blogs.Api do end features do - Blogs.Publishing + Blogs end end end diff --git a/test/support/blogs/app.ex b/test/support/blogs/app.ex index 56d41d51..bb22e9c4 100644 --- a/test/support/blogs/app.ex +++ b/test/support/blogs/app.ex @@ -3,10 +3,69 @@ defmodule Blogs.App do use Momo.App, otp_app: :momo app roles: "current_user.roles" do - features do - Blogs.Accounts - Blogs.Notifications - Blogs.Publishing + models do + Blogs.Models.User + Blogs.Models.Credential + Blogs.Models.Onboarding + Blogs.Models.Digest + Blogs.Models.Author + Blogs.Models.Blog + Blogs.Models.Comment + Blogs.Models.Post + Blogs.Models.Theme + end + + commands do + Blogs.Commands.EnableUser + Blogs.Commands.ExpireCredentials + Blogs.Commands.RegisterUser + Blogs.Commands.RemindPassword + Blogs.Commands.RequestFeedback + Blogs.Commands.SendWelcomeEmail + end + + queries do + Blogs.Queries.GetOnboardings + Blogs.Queries.GetUserByEmail + Blogs.Queries.GetUserIds + Blogs.Queries.GetUsers + Blogs.Queries.GetUsersByEmails + end + + events do + Blogs.Events.CredentialExpired + Blogs.Events.PasswordRemindedSent + Blogs.Events.UserOnboarded + Blogs.Events.UserRegistered + Blogs.Events.UsersLocked + end + + flows do + Blogs.Flows.Onboarding + end + + subscriptions do + Blogs.Subscriptions.UserOnboardings + Blogs.Subscriptions.UserRegistrations + end + + mappings do + Blogs.Mappings.CredentialExpiredFromCredential + Blogs.Mappings.UserIdFromMap + Blogs.Mappings.UserRegisteredFromUser + end + + values do + Blogs.Values.UserEmail + Blogs.Values.UserEmails + Blogs.Values.UserId + end + + scopes do + Blogs.Scopes.IsPublic + Blogs.Scopes.NotLocked + Blogs.Scopes.Self + Blogs.Scopes.SelfAndNotLocked end end end diff --git a/test/support/blogs/commands/enable_user.ex b/test/support/blogs/commands/enable_user.ex new file mode 100644 index 00000000..e5330c7b --- /dev/null +++ b/test/support/blogs/commands/enable_user.ex @@ -0,0 +1,6 @@ +defmodule Blogs.Commands.EnableUser do + @moduledoc false + use Momo.Command + + command params: Blogs.Values.UserId, handler: Blogs.Handlers.EnableUser +end diff --git a/test/support/blogs/commands/expire_credentials.ex b/test/support/blogs/commands/expire_credentials.ex new file mode 100644 index 00000000..9e837239 --- /dev/null +++ b/test/support/blogs/commands/expire_credentials.ex @@ -0,0 +1,15 @@ +defmodule Blogs.Commands.ExpireCredentials do + @moduledoc false + use Momo.Command + + alias Blogs.Values.UserId + alias Blogs.Models.Credential + alias Blogs.Events.CredentialExpired + + command params: UserId, + returns: Credential, + many: true, + handler: Blogs.Handlers.ExpireCredentials do + publish event: CredentialExpired + end +end diff --git a/test/support/blogs/commands/register_user.ex b/test/support/blogs/commands/register_user.ex new file mode 100644 index 00000000..87fbb479 --- /dev/null +++ b/test/support/blogs/commands/register_user.ex @@ -0,0 +1,16 @@ +defmodule Blogs.Commands.RegisterUser do + @moduledoc false + + use Momo.Command + + alias Blogs.Models.User + alias Blogs.Events.UserRegistered + alias Blogs.Expressions.IsGmailAccount + alias Blogs.Expressions.LooksFake + + command params: User, atomic: true, handler: Blogs.Handlers.RegisterUser do + policy role: :guest + + publish event: UserRegistered, if: IsGmailAccount, unless: LooksFake + end +end diff --git a/test/support/blogs/commands/remind_password.ex b/test/support/blogs/commands/remind_password.ex new file mode 100644 index 00000000..552d8b54 --- /dev/null +++ b/test/support/blogs/commands/remind_password.ex @@ -0,0 +1,11 @@ +defmodule Blogs.Commands.RemindPassword do + @moduledoc false + use Momo.Command + + alias Blogs.Scopes.SelfAndNotLocked + alias Blogs.Values.UserId + + command params: UserId, handler: Blogs.Handlers.RemindPassword do + policy role: :user, scope: SelfAndNotLocked + end +end diff --git a/test/support/blogs/commands/request_feedback.ex b/test/support/blogs/commands/request_feedback.ex new file mode 100644 index 00000000..5f9a6c12 --- /dev/null +++ b/test/support/blogs/commands/request_feedback.ex @@ -0,0 +1,6 @@ +defmodule Blogs.Commands.RequestFeedback do + @moduledoc false + use Momo.Command + + command params: Blogs.Values.UserId, handler: Blogs.Handlers.RequestFeedback +end diff --git a/test/support/blogs/commands/send_welcome_email.ex b/test/support/blogs/commands/send_welcome_email.ex new file mode 100644 index 00000000..6b7953e7 --- /dev/null +++ b/test/support/blogs/commands/send_welcome_email.ex @@ -0,0 +1,6 @@ +defmodule Blogs.Commands.SendWelcomeEmail do + @moduledoc false + use Momo.Command + + command params: Blogs.Values.UserId, handler: Blogs.Handlers.SendWelcomeEmail +end diff --git a/test/support/blogs/accounts/computations/onboarding_state.ex b/test/support/blogs/computations/onboarding_state.ex similarity index 74% rename from test/support/blogs/accounts/computations/onboarding_state.ex rename to test/support/blogs/computations/onboarding_state.ex index eff173f7..7bd80578 100644 --- a/test/support/blogs/accounts/computations/onboarding_state.ex +++ b/test/support/blogs/computations/onboarding_state.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Computations.OnboardingState do +defmodule Blogs.Computations.OnboardingState do @moduledoc false def execute(onboarding, _context) do diff --git a/test/support/blogs/accounts/enums/credential_type.ex b/test/support/blogs/enums/credential_type.ex similarity index 69% rename from test/support/blogs/accounts/enums/credential_type.ex rename to test/support/blogs/enums/credential_type.ex index 30de83e2..aaacb139 100644 --- a/test/support/blogs/accounts/enums/credential_type.ex +++ b/test/support/blogs/enums/credential_type.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Enums.CredentialType do +defmodule Blogs.Enums.CredentialType do @moduledoc false use Momo.Enum diff --git a/test/support/blogs/publishing/enums/theme.ex b/test/support/blogs/enums/theme.ex similarity index 75% rename from test/support/blogs/publishing/enums/theme.ex rename to test/support/blogs/enums/theme.ex index 90838b8d..76309ec0 100644 --- a/test/support/blogs/publishing/enums/theme.ex +++ b/test/support/blogs/enums/theme.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Publishing.Enums.Theme do +defmodule Blogs.Enums.Theme do @moduledoc false use Momo.Enum diff --git a/test/support/blogs/accounts/events/credential_expired.ex b/test/support/blogs/events/credential_expired.ex similarity index 69% rename from test/support/blogs/accounts/events/credential_expired.ex rename to test/support/blogs/events/credential_expired.ex index d0a3b3eb..bb05b73e 100644 --- a/test/support/blogs/accounts/events/credential_expired.ex +++ b/test/support/blogs/events/credential_expired.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Events.CredentialExpired do +defmodule Blogs.Events.CredentialExpired do @moduledoc false use Momo.Event diff --git a/test/support/blogs/accounts/events/password_reminded_sent.ex b/test/support/blogs/events/password_reminded_sent.ex similarity index 69% rename from test/support/blogs/accounts/events/password_reminded_sent.ex rename to test/support/blogs/events/password_reminded_sent.ex index 5318fc4a..dcbf31cf 100644 --- a/test/support/blogs/accounts/events/password_reminded_sent.ex +++ b/test/support/blogs/events/password_reminded_sent.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Events.PasswordRemindedSent do +defmodule Blogs.Events.PasswordRemindedSent do @moduledoc false use Momo.Event diff --git a/test/support/blogs/accounts/events/user_onboarded.ex b/test/support/blogs/events/user_onboarded.ex similarity index 67% rename from test/support/blogs/accounts/events/user_onboarded.ex rename to test/support/blogs/events/user_onboarded.ex index 37811955..4efee230 100644 --- a/test/support/blogs/accounts/events/user_onboarded.ex +++ b/test/support/blogs/events/user_onboarded.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Events.UserOnboarded do +defmodule Blogs.Events.UserOnboarded do @moduledoc false use Momo.Event diff --git a/test/support/blogs/accounts/events/user_registered.ex b/test/support/blogs/events/user_registered.ex similarity index 77% rename from test/support/blogs/accounts/events/user_registered.ex rename to test/support/blogs/events/user_registered.ex index 4ab1bfc2..fe3ee45a 100644 --- a/test/support/blogs/accounts/events/user_registered.ex +++ b/test/support/blogs/events/user_registered.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Events.UserRegistered do +defmodule Blogs.Events.UserRegistered do @moduledoc false use Momo.Event diff --git a/test/support/blogs/accounts/events/users_locked.ex b/test/support/blogs/events/users_locked.ex similarity index 69% rename from test/support/blogs/accounts/events/users_locked.ex rename to test/support/blogs/events/users_locked.ex index 7f582ab6..dd3b2530 100644 --- a/test/support/blogs/accounts/events/users_locked.ex +++ b/test/support/blogs/events/users_locked.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Events.UsersLocked do +defmodule Blogs.Events.UsersLocked do @moduledoc false use Momo.Event diff --git a/test/support/blogs/accounts/expressions/is_gmail_account.ex b/test/support/blogs/expressions/is_gmail_account.ex similarity index 64% rename from test/support/blogs/accounts/expressions/is_gmail_account.ex rename to test/support/blogs/expressions/is_gmail_account.ex index 08a5c920..619a1330 100644 --- a/test/support/blogs/accounts/expressions/is_gmail_account.ex +++ b/test/support/blogs/expressions/is_gmail_account.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Expressions.IsGmailAccount do +defmodule Blogs.Expressions.IsGmailAccount do @moduledoc false def execute(user, _context), do: String.ends_with?(user.email, "gmail.com") diff --git a/test/support/blogs/accounts/expressions/looks_fake.ex b/test/support/blogs/expressions/looks_fake.ex similarity index 65% rename from test/support/blogs/accounts/expressions/looks_fake.ex rename to test/support/blogs/expressions/looks_fake.ex index a4a440bb..9b6d48f6 100644 --- a/test/support/blogs/accounts/expressions/looks_fake.ex +++ b/test/support/blogs/expressions/looks_fake.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Expressions.LooksFake do +defmodule Blogs.Expressions.LooksFake do @moduledoc false def execute(user, _context), do: String.contains?(user.email, "fake") diff --git a/test/support/blogs/flows/onboarding.ex b/test/support/blogs/flows/onboarding.ex new file mode 100644 index 00000000..3f3cab0b --- /dev/null +++ b/test/support/blogs/flows/onboarding.ex @@ -0,0 +1,18 @@ +defmodule Blogs.Flows.Onboarding do + @moduledoc false + use Momo.Flow + + alias Blogs.Commands.SendWelcomeEmail + alias Blogs.Commands.EnableUser + + alias Blogs.Models.Onboarding + alias Blogs.Values.UserId + alias Blogs.Events.UserOnboarded + + flow model: Onboarding, params: UserId, publish: UserOnboarded do + steps do + SendWelcomeEmail + EnableUser + end + end +end diff --git a/test/support/blogs/handlers/enable_user.ex b/test/support/blogs/handlers/enable_user.ex new file mode 100644 index 00000000..c8ec2025 --- /dev/null +++ b/test/support/blogs/handlers/enable_user.ex @@ -0,0 +1,5 @@ +defmodule Blogs.Handlers.EnableUser do + @moduledoc false + + def execute(_user, _context), do: :ok +end diff --git a/test/support/blogs/accounts/commands/expire_credentials.ex b/test/support/blogs/handlers/expire_credentials.ex similarity index 57% rename from test/support/blogs/accounts/commands/expire_credentials.ex rename to test/support/blogs/handlers/expire_credentials.ex index dfdf938d..5b0729fb 100644 --- a/test/support/blogs/accounts/commands/expire_credentials.ex +++ b/test/support/blogs/handlers/expire_credentials.ex @@ -1,16 +1,9 @@ -defmodule Blogs.Accounts.Commands.ExpireCredentials do +defmodule Blogs.Handlers.ExpireCredentials do @moduledoc false - use Momo.Command - alias Blogs.Accounts.Values.UserId - alias Blogs.Accounts.Credential - alias Blogs.Accounts.Events.CredentialExpired + alias Blogs.Models.Credential - command params: UserId, returns: Credential, many: true do - publish event: CredentialExpired - end - - def handle(params, _context) do + def execute(params, _context) do credentials = [ %Credential{ id: Ecto.UUID.generate(), diff --git a/test/support/blogs/handlers/register_user.ex b/test/support/blogs/handlers/register_user.ex new file mode 100644 index 00000000..833d849e --- /dev/null +++ b/test/support/blogs/handlers/register_user.ex @@ -0,0 +1,7 @@ +defmodule Blogs.Handlers.RegisterUser do + @moduledoc false + alias Blogs.Models.User + + def execute(%{email: "foo@bar.com"}, _context), do: {:error, :invalid_email} + def execute(user, _context), do: User.create(user) +end diff --git a/test/support/blogs/handlers/remind_password.ex b/test/support/blogs/handlers/remind_password.ex new file mode 100644 index 00000000..184de619 --- /dev/null +++ b/test/support/blogs/handlers/remind_password.ex @@ -0,0 +1,5 @@ +defmodule Blogs.Handlers.RemindPassword do + @moduledoc false + + def execute(_user, _context), do: :ok +end diff --git a/test/support/blogs/handlers/request_feedback.ex b/test/support/blogs/handlers/request_feedback.ex new file mode 100644 index 00000000..7628c9bb --- /dev/null +++ b/test/support/blogs/handlers/request_feedback.ex @@ -0,0 +1,5 @@ +defmodule Blogs.Handlers.RequestFeedback do + @moduledoc false + + def execute(_user, _context), do: :ok +end diff --git a/test/support/blogs/handlers/send_welcome_email.ex b/test/support/blogs/handlers/send_welcome_email.ex new file mode 100644 index 00000000..b9a3d9af --- /dev/null +++ b/test/support/blogs/handlers/send_welcome_email.ex @@ -0,0 +1,5 @@ +defmodule Blogs.Handlers.SendWelcomeEmail do + @moduledoc false + + def execute(_user, _context), do: :ok +end diff --git a/test/support/blogs/accounts/mappings/credential_expired_from_credential.ex b/test/support/blogs/mappings/credential_expired_from_credential.ex similarity index 51% rename from test/support/blogs/accounts/mappings/credential_expired_from_credential.ex rename to test/support/blogs/mappings/credential_expired_from_credential.ex index c1dbce9f..9d060d58 100644 --- a/test/support/blogs/accounts/mappings/credential_expired_from_credential.ex +++ b/test/support/blogs/mappings/credential_expired_from_credential.ex @@ -1,9 +1,9 @@ -defmodule Blogs.Accounts.Mappings.CredentialExpiredFromCredential do +defmodule Blogs.Mappings.CredentialExpiredFromCredential do @moduledoc false use Momo.Mapping - alias Blogs.Accounts.Model.Credential - alias Blogs.Accounts.Events.CredentialExpired + alias Blogs.Models.Credential + alias Blogs.Events.CredentialExpired mapping from: Credential, to: CredentialExpired do field :user_id, path: "user.id" diff --git a/test/support/blogs/accounts/mappings/user_id_from_map.ex b/test/support/blogs/mappings/user_id_from_map.ex similarity index 56% rename from test/support/blogs/accounts/mappings/user_id_from_map.ex rename to test/support/blogs/mappings/user_id_from_map.ex index 1f6b113d..1d1bc0c3 100644 --- a/test/support/blogs/accounts/mappings/user_id_from_map.ex +++ b/test/support/blogs/mappings/user_id_from_map.ex @@ -1,8 +1,8 @@ -defmodule Blogs.Accounts.Mappings.UserIdFromMap do +defmodule Blogs.Mappings.UserIdFromMap do @moduledoc false use Momo.Mapping - alias Blogs.Accounts.Values.UserId + alias Blogs.Values.UserId mapping from: Map, to: UserId do field :user_id, path: "id" diff --git a/test/support/blogs/accounts/mappings/user_registered_from_user.ex b/test/support/blogs/mappings/user_registered_from_user.ex similarity index 56% rename from test/support/blogs/accounts/mappings/user_registered_from_user.ex rename to test/support/blogs/mappings/user_registered_from_user.ex index 114f8e26..fb24761b 100644 --- a/test/support/blogs/accounts/mappings/user_registered_from_user.ex +++ b/test/support/blogs/mappings/user_registered_from_user.ex @@ -1,9 +1,9 @@ -defmodule Blogs.Accounts.Mappings.UserRegisteredFromUser do +defmodule Blogs.Mappings.UserRegisteredFromUser do @moduledoc false use Momo.Mapping - alias Blogs.Accounts.User - alias Blogs.Accounts.Events.UserRegistered + alias Blogs.Models.User + alias Blogs.Events.UserRegistered mapping from: User, to: UserRegistered do field :user_id, path: "id" diff --git a/test/support/blogs/publishing/author.ex b/test/support/blogs/models/author.ex similarity index 64% rename from test/support/blogs/publishing/author.ex rename to test/support/blogs/models/author.ex index 0f847a0b..0c25f8be 100644 --- a/test/support/blogs/publishing/author.ex +++ b/test/support/blogs/models/author.ex @@ -1,12 +1,10 @@ -defmodule Blogs.Publishing.Author do +defmodule Blogs.Models.Author do @moduledoc false use Momo.Model - alias Blogs.Publishing.Blog - model do attribute :name, kind: :string attribute :profile, kind: :string, default: "publisher" - has_many Blog + has_many Blogs.Models.Blog end end diff --git a/test/support/blogs/publishing/blog.ex b/test/support/blogs/models/blog.ex similarity index 62% rename from test/support/blogs/publishing/blog.ex rename to test/support/blogs/models/blog.ex index dbdfad3b..6393bf07 100644 --- a/test/support/blogs/publishing/blog.ex +++ b/test/support/blogs/models/blog.ex @@ -1,16 +1,14 @@ -defmodule Blogs.Publishing.Blog do +defmodule Blogs.Models.Blog do @moduledoc false use Momo.Model - alias Blogs.Publishing.{Author, Post, Theme} - model do attribute :name, kind: :string attribute :published, kind: :boolean, required: true, default: false attribute :public, kind: :boolean, required: false, default: false - belongs_to Author - belongs_to Theme, required: false - has_many Post + belongs_to Blogs.Models.Author + belongs_to Blogs.Models.Theme, required: false + has_many Blogs.Models.Post unique fields: [:author, :name] end end diff --git a/test/support/blogs/models/comment.ex b/test/support/blogs/models/comment.ex new file mode 100644 index 00000000..7ba47884 --- /dev/null +++ b/test/support/blogs/models/comment.ex @@ -0,0 +1,10 @@ +defmodule Blogs.Models.Comment do + @moduledoc false + use Momo.Model + + model do + attribute :body, kind: :string + belongs_to Blogs.Models.Post + belongs_to Blogs.Models.Author + end +end diff --git a/test/support/blogs/accounts/models/credential.ex b/test/support/blogs/models/credential.ex similarity index 57% rename from test/support/blogs/accounts/models/credential.ex rename to test/support/blogs/models/credential.ex index 2ed202eb..34437515 100644 --- a/test/support/blogs/accounts/models/credential.ex +++ b/test/support/blogs/models/credential.ex @@ -1,15 +1,14 @@ -defmodule Blogs.Accounts.Credential do +defmodule Blogs.Models.Credential do use Momo.Model - alias Blogs.Accounts.Enums.CredentialType - model do attribute :name, kind: :string - attribute :type, kind: :integer, in: CredentialType + attribute :type, kind: :integer, in: Blogs.Enums.CredentialType + attribute :value, kind: :string attribute :enabled, kind: :boolean - belongs_to Blogs.Accounts.User + belongs_to Blogs.Models.User unique fields: [:user, :name] do on_conflict strategy: :merge diff --git a/test/support/blogs/notifications/digest.ex b/test/support/blogs/models/digest.ex similarity index 79% rename from test/support/blogs/notifications/digest.ex rename to test/support/blogs/models/digest.ex index 9336d8d1..e2aa66c2 100644 --- a/test/support/blogs/notifications/digest.ex +++ b/test/support/blogs/models/digest.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Notifications.Digest do +defmodule Blogs.Models.Digest do @moduledoc false use Momo.Model diff --git a/test/support/blogs/accounts/models/onboarding.ex b/test/support/blogs/models/onboarding.ex similarity index 88% rename from test/support/blogs/accounts/models/onboarding.ex rename to test/support/blogs/models/onboarding.ex index b3cff0b1..3c9dbd30 100644 --- a/test/support/blogs/accounts/models/onboarding.ex +++ b/test/support/blogs/models/onboarding.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Onboarding do +defmodule Blogs.Models.Onboarding do @moduledoc false use Momo.Model diff --git a/test/support/blogs/publishing/post.ex b/test/support/blogs/models/post.ex similarity index 71% rename from test/support/blogs/publishing/post.ex rename to test/support/blogs/models/post.ex index e491e819..cad6aa96 100644 --- a/test/support/blogs/publishing/post.ex +++ b/test/support/blogs/models/post.ex @@ -1,17 +1,15 @@ -defmodule Blogs.Publishing.Post do +defmodule Blogs.Models.Post do @moduledoc false use Momo.Model - alias Blogs.Publishing.{Author, Blog, Comment} - model do attribute :title, kind: :string attribute :published_at, kind: :datetime, required: false attribute :locked, kind: :boolean, required: true, default: false attribute :published, kind: :boolean, required: true, default: false attribute :deleted, kind: :boolean, required: true, default: false - belongs_to Blog - belongs_to Author - has_many Comment + belongs_to Blogs.Models.Blog + belongs_to Blogs.Models.Author + has_many Blogs.Models.Comment end end diff --git a/test/support/blogs/publishing/theme.ex b/test/support/blogs/models/theme.ex similarity index 52% rename from test/support/blogs/publishing/theme.ex rename to test/support/blogs/models/theme.ex index 2dab590b..67aa8466 100644 --- a/test/support/blogs/publishing/theme.ex +++ b/test/support/blogs/models/theme.ex @@ -1,11 +1,9 @@ -defmodule Blogs.Publishing.Theme do +defmodule Blogs.Models.Theme do @moduledoc false use Momo.Model - alias Blogs.Publishing.Enums.Theme - model do - attribute :name, kind: :string, in: Theme + attribute :name, kind: :string, in: Blogs.Enums.Theme unique fields: [:name] do on_conflict strategy: :merge diff --git a/test/support/blogs/accounts/models/user.ex b/test/support/blogs/models/user.ex similarity index 68% rename from test/support/blogs/accounts/models/user.ex rename to test/support/blogs/models/user.ex index 48396517..9ac92c95 100644 --- a/test/support/blogs/accounts/models/user.ex +++ b/test/support/blogs/models/user.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.User do +defmodule Blogs.Models.User do use Momo.Model model do @@ -8,6 +8,6 @@ defmodule Blogs.Accounts.User do unique fields: [:email] - has_many Blogs.Accounts.Credential, preloaded: true + has_many Blogs.Models.Credential, preloaded: true end end diff --git a/test/support/blogs/notifications.ex b/test/support/blogs/notifications.ex deleted file mode 100644 index 7eaf3f86..00000000 --- a/test/support/blogs/notifications.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule Blogs.Notifications do - @moduledoc false - use Momo.Feature - - feature do - models do - Blogs.Notifications.Digest - end - end -end diff --git a/test/support/blogs/publishing.ex b/test/support/blogs/publishing.ex deleted file mode 100644 index bd1d099f..00000000 --- a/test/support/blogs/publishing.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Blogs.Publishing do - @moduledoc false - use Momo.Feature - - feature do - scopes do - Blogs.Scopes - end - - models do - Blogs.Publishing.Author - Blogs.Publishing.Blog - Blogs.Publishing.Comment - Blogs.Publishing.Post - Blogs.Publishing.Theme - end - end -end diff --git a/test/support/blogs/publishing/comment.ex b/test/support/blogs/publishing/comment.ex deleted file mode 100644 index 9b4677df..00000000 --- a/test/support/blogs/publishing/comment.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Blogs.Publishing.Comment do - @moduledoc false - use Momo.Model - - alias Blogs.Publishing.{Author, Post} - - model do - attribute :body, kind: :string - belongs_to Post - belongs_to Author - end -end diff --git a/test/support/blogs/accounts/queries/get_onboardings.ex b/test/support/blogs/queries/get_onboardings.ex similarity index 61% rename from test/support/blogs/accounts/queries/get_onboardings.ex rename to test/support/blogs/queries/get_onboardings.ex index df58aa6b..ca600021 100644 --- a/test/support/blogs/accounts/queries/get_onboardings.ex +++ b/test/support/blogs/queries/get_onboardings.ex @@ -1,8 +1,8 @@ -defmodule Blogs.Accounts.Queries.GetOnboardings do +defmodule Blogs.Queries.GetOnboardings do @moduledoc false use Momo.Query - alias Blogs.Accounts.Onboarding + alias Blogs.Models.Onboarding query returns: Onboarding, many: true do sort by: :steps_pending, direction: :desc diff --git a/test/support/blogs/queries/get_user_by_email.ex b/test/support/blogs/queries/get_user_by_email.ex new file mode 100644 index 00000000..540db46a --- /dev/null +++ b/test/support/blogs/queries/get_user_by_email.ex @@ -0,0 +1,11 @@ +defmodule Blogs.Queries.GetUserByEmail do + @moduledoc false + use Momo.Query + + alias Blogs.Models.User + alias Blogs.Values.UserEmail + + query params: UserEmail, returns: User do + policy role: :user + end +end diff --git a/test/support/blogs/accounts/queries/get_user_ids.ex b/test/support/blogs/queries/get_user_ids.ex similarity index 69% rename from test/support/blogs/accounts/queries/get_user_ids.ex rename to test/support/blogs/queries/get_user_ids.ex index 5f4b2337..9a870c71 100644 --- a/test/support/blogs/accounts/queries/get_user_ids.ex +++ b/test/support/blogs/queries/get_user_ids.ex @@ -1,8 +1,8 @@ -defmodule Blogs.Accounts.Queries.GetUserIds do +defmodule Blogs.Queries.GetUserIds do @moduledoc false use Momo.Query - alias Blogs.Accounts.Values.UserId + alias Blogs.Values.UserId query returns: UserId, many: true, custom: true do policy role: :user diff --git a/test/support/blogs/accounts/queries/get_users.ex b/test/support/blogs/queries/get_users.ex similarity index 52% rename from test/support/blogs/accounts/queries/get_users.ex rename to test/support/blogs/queries/get_users.ex index 89f0d97d..dc5c6114 100644 --- a/test/support/blogs/accounts/queries/get_users.ex +++ b/test/support/blogs/queries/get_users.ex @@ -1,9 +1,9 @@ -defmodule Blogs.Accounts.Queries.GetUsers do +defmodule Blogs.Queries.GetUsers do @moduledoc false use Momo.Query - alias Blogs.Accounts.User - alias Blogs.Accounts.Scopes.IsPublic + alias Blogs.Models.User + alias Blogs.Scopes.IsPublic query returns: User, many: true do policy role: :guest, scope: IsPublic diff --git a/test/support/blogs/accounts/queries/get_users_by_emails.ex b/test/support/blogs/queries/get_users_by_emails.ex similarity index 51% rename from test/support/blogs/accounts/queries/get_users_by_emails.ex rename to test/support/blogs/queries/get_users_by_emails.ex index eb887202..105c322c 100644 --- a/test/support/blogs/accounts/queries/get_users_by_emails.ex +++ b/test/support/blogs/queries/get_users_by_emails.ex @@ -1,9 +1,9 @@ -defmodule Blogs.Accounts.Queries.GetUsersByEmails do +defmodule Blogs.Queries.GetUsersByEmails do @moduledoc false use Momo.Query - alias Blogs.Accounts.User - alias Blogs.Accounts.Values.UserEmails + alias Blogs.Models.User + alias Blogs.Values.UserEmails query params: UserEmails, returns: User, many: true do policy role: :user diff --git a/test/support/blogs/accounts/scopes/is_public.ex b/test/support/blogs/scopes/is_public.ex similarity index 55% rename from test/support/blogs/accounts/scopes/is_public.ex rename to test/support/blogs/scopes/is_public.ex index 18fd2ba7..afe5363e 100644 --- a/test/support/blogs/accounts/scopes/is_public.ex +++ b/test/support/blogs/scopes/is_public.ex @@ -1,10 +1,10 @@ -defmodule Blogs.Accounts.Scopes.IsPublic do +defmodule Blogs.Scopes.IsPublic do @moduledoc false use Momo.Scope scope do is_true do - path "public" + path("public") end end end diff --git a/test/support/blogs/accounts/scopes/not_locked.ex b/test/support/blogs/scopes/not_locked.ex similarity index 53% rename from test/support/blogs/accounts/scopes/not_locked.ex rename to test/support/blogs/scopes/not_locked.ex index 339a48e1..dea48a2e 100644 --- a/test/support/blogs/accounts/scopes/not_locked.ex +++ b/test/support/blogs/scopes/not_locked.ex @@ -1,10 +1,10 @@ -defmodule Blogs.Accounts.Scopes.NotLocked do +defmodule Blogs.Scopes.NotLocked do @moduledoc false use Momo.Scope scope do is_false do - path "user.locked" + path("user.locked") end end end diff --git a/test/support/blogs/scopes/self.ex b/test/support/blogs/scopes/self.ex new file mode 100644 index 00000000..06371992 --- /dev/null +++ b/test/support/blogs/scopes/self.ex @@ -0,0 +1,11 @@ +defmodule Blogs.Scopes.Self do + @moduledoc false + use Momo.Scope + + scope do + same do + path("user.id") + path("current_user.id") + end + end +end diff --git a/test/support/blogs/scopes/self_and_not_locked.ex b/test/support/blogs/scopes/self_and_not_locked.ex new file mode 100644 index 00000000..a13463f6 --- /dev/null +++ b/test/support/blogs/scopes/self_and_not_locked.ex @@ -0,0 +1,11 @@ +defmodule Blogs.Scopes.SelfAndNotLocked do + @moduledoc false + use Momo.Scope + + scope do + all do + Blogs.Scopes.Self + Blogs.Scopes.NotLocked + end + end +end diff --git a/test/support/blogs/subscriptions/user_onboardings.ex b/test/support/blogs/subscriptions/user_onboardings.ex new file mode 100644 index 00000000..7278ab69 --- /dev/null +++ b/test/support/blogs/subscriptions/user_onboardings.ex @@ -0,0 +1,9 @@ +defmodule Blogs.Subscriptions.UserOnboardings do + @moduledoc false + use Momo.Subscription + + subscription( + on: Blogs.Events.UserOnboarded, + perform: Blogs.Commands.RequestFeedback + ) +end diff --git a/test/support/blogs/subscriptions/user_registrations.ex b/test/support/blogs/subscriptions/user_registrations.ex new file mode 100644 index 00000000..9d6551c2 --- /dev/null +++ b/test/support/blogs/subscriptions/user_registrations.ex @@ -0,0 +1,9 @@ +defmodule Blogs.Subscriptions.UserRegistrations do + @moduledoc false + use Momo.Subscription + + subscription( + on: Blogs.Events.UserRegistered, + perform: Blogs.Flows.Onboarding + ) +end diff --git a/test/support/blogs/ui.ex b/test/support/blogs/ui.ex index b33f26fc..03f52383 100644 --- a/test/support/blogs/ui.ex +++ b/test/support/blogs/ui.ex @@ -4,8 +4,8 @@ defmodule Blogs.Ui do ui do namespaces do - Blogs.Ui.Namespaces.Root Blogs.Ui.Namespaces.Admin + Blogs.Ui.Namespaces.Root end end end diff --git a/test/support/blogs/ui/routes/blog.ex b/test/support/blogs/ui/routes/blog.ex index d6aa6d9c..552c14e8 100644 --- a/test/support/blogs/ui/routes/blog.ex +++ b/test/support/blogs/ui/routes/blog.ex @@ -2,9 +2,9 @@ defmodule Blogs.Ui.Routes.Blog do @moduledoc false use Momo.Ui.Route - route "/blogs" do + route "/blogs/:id" do view Blogs.Ui.Views.Blog - view Blogs.Ui.Actions.BlogNotFound, for: "not_found" + view Blogs.Ui.Views.BlogNotFound, for: "not_found" end def execute(%{"id" => "1"}), do: %{"id" => "1", "name" => "Blog"} diff --git a/test/support/blogs/ui/views/admin.ex b/test/support/blogs/ui/views/admin.ex new file mode 100644 index 00000000..04a64179 --- /dev/null +++ b/test/support/blogs/ui/views/admin.ex @@ -0,0 +1,16 @@ +defmodule Blogs.Ui.Views.Admin do + @moduledoc false + use Momo.Ui.View + + view do + html do + head do + title "My Blog" + end + + body do + h1 "Admin Page" + end + end + end +end diff --git a/test/support/blogs/ui/views/blog.ex b/test/support/blogs/ui/views/blog.ex index 1f72499e..ac1fa61c 100644 --- a/test/support/blogs/ui/views/blog.ex +++ b/test/support/blogs/ui/views/blog.ex @@ -9,7 +9,7 @@ defmodule Blogs.Ui.Views.Blog do end body do - h1 "Welcome to my Blog" + h1 "Welcome to my Blog {{ id }}" end end end diff --git a/test/support/blogs/accounts/values/user_email.ex b/test/support/blogs/values/user_email.ex similarity index 66% rename from test/support/blogs/accounts/values/user_email.ex rename to test/support/blogs/values/user_email.ex index 9984b7eb..74e7f8bf 100644 --- a/test/support/blogs/accounts/values/user_email.ex +++ b/test/support/blogs/values/user_email.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Values.UserEmail do +defmodule Blogs.Values.UserEmail do @moduledoc false use Momo.Value diff --git a/test/support/blogs/accounts/values/user_emails.ex b/test/support/blogs/values/user_emails.ex similarity index 68% rename from test/support/blogs/accounts/values/user_emails.ex rename to test/support/blogs/values/user_emails.ex index de759a01..412eb5b8 100644 --- a/test/support/blogs/accounts/values/user_emails.ex +++ b/test/support/blogs/values/user_emails.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Values.UserEmails do +defmodule Blogs.Values.UserEmails do @moduledoc false use Momo.Value diff --git a/test/support/blogs/accounts/values/user_id.ex b/test/support/blogs/values/user_id.ex similarity index 68% rename from test/support/blogs/accounts/values/user_id.ex rename to test/support/blogs/values/user_id.ex index fcee8f0b..59febf64 100644 --- a/test/support/blogs/accounts/values/user_id.ex +++ b/test/support/blogs/values/user_id.ex @@ -1,4 +1,4 @@ -defmodule Blogs.Accounts.Values.UserId do +defmodule Blogs.Values.UserId do @moduledoc false use Momo.Value diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index be4526d3..90b6b57d 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -1,8 +1,7 @@ defmodule Momo.Fixtures do @moduledoc false - alias Blogs.Accounts - alias Blogs.Publishing + alias Blogs.Models.{User, Author, Blog, Post, Comment} @doc "A convenience function to generate uuids in tests" def uuid, do: Ecto.UUID.generate() @@ -42,17 +41,17 @@ defmodule Momo.Fixtures do def comments(context) do {:ok, user} = - Accounts.User.create( + User.create( id: Ecto.UUID.generate(), email: "foo@bar", public: true, external_id: uuid() ) - {:ok, author} = Publishing.Author.create(id: user.id, name: "foo") + {:ok, author} = Author.create(id: user.id, name: "foo") {:ok, blog} = - Publishing.Blog.create( + Blog.create( id: uuid(), published: true, author_id: author.id, @@ -60,7 +59,7 @@ defmodule Momo.Fixtures do ) {:ok, post} = - Publishing.Post.create( + Post.create( id: uuid(), author_id: author.id, blog_id: blog.id, @@ -72,7 +71,7 @@ defmodule Momo.Fixtures do ) {:ok, comment1} = - Publishing.Comment.create( + Comment.create( id: uuid(), post_id: post.id, author_id: author.id, @@ -81,7 +80,7 @@ defmodule Momo.Fixtures do ) {:ok, comment2} = - Publishing.Comment.create( + Comment.create( id: uuid(), post_id: post.id, author_id: author.id, @@ -90,7 +89,7 @@ defmodule Momo.Fixtures do ) {:ok, comment3} = - Publishing.Comment.create( + Comment.create( id: uuid(), post_id: post.id, author_id: author.id, diff --git a/test/support/migration_helper.ex b/test/support/migration_helper.ex index 251fd6bb..7966638d 100644 --- a/test/support/migration_helper.ex +++ b/test/support/migration_helper.ex @@ -11,10 +11,9 @@ defmodule MigrationHelper do @doc false def generate_migrations(existing \\ []) do app = :momo |> Application.fetch_env!(Momo) |> Keyword.fetch!(:app) - features = app.features() existing - |> Migrations.missing(features) + |> Migrations.missing(app) |> Migration.encode() |> Migration.format() |> Enum.join("")