From e3301a51396ad4c118e36c53416f1e535b6b20db Mon Sep 17 00:00:00 2001 From: Zamith Date: Wed, 11 Nov 2020 15:00:37 +0000 Subject: [PATCH 1/3] Adds multitenancy support Why: * When building an admin dashboard is not always true that: 1. There is only one repo 2. The repo is not dynamic This change addresses the need by: * Adding a resource level repo config that can be set when creating the resources list, to allow for multiple repos. It defaults to the ecto repo config in the env. * Adding a new configurable function called `set_dynamic_repo`, which takes the connection as based on that it will set the dynamic repo. Since the dynamic repo is set per process, this is done on every controller action, so that we can ensure it is set properly. An example of this could be by setting a cookie with the tenant, which a plug can assign to the connection and have the `set_dynamic_repo` use that to decide what to do. * Adding a new form of extension called navigation extras, which allows adding html to the top navigation. This can be used by, for example, having a select with the multiple tenants --- lib/kaffy/resource_callbacks.ex | 12 +++--- lib/kaffy/resource_form.ex | 38 ++++++++++++------- lib/kaffy/resource_query.ex | 21 +++++----- lib/kaffy/utils.ex | 26 ++++++++++++- .../controllers/resource_controller.ex | 9 +++++ lib/kaffy_web/templates/layout/app.html.eex | 6 ++- lib/kaffy_web/templates/resource/new.html.eex | 2 +- .../templates/resource/show.html.eex | 2 +- 8 files changed, 83 insertions(+), 33 deletions(-) diff --git a/lib/kaffy/resource_callbacks.ex b/lib/kaffy/resource_callbacks.ex index 551ed0ad..962ea7ed 100644 --- a/lib/kaffy/resource_callbacks.ex +++ b/lib/kaffy/resource_callbacks.ex @@ -5,7 +5,7 @@ defmodule Kaffy.ResourceCallbacks do def create_callbacks(conn, resource, changes) do changeset = Kaffy.ResourceAdmin.create_changeset(resource, changes) - repo = Kaffy.Utils.repo() + repo = Kaffy.Utils.repo(resource) repo.transaction(fn -> result = @@ -28,7 +28,7 @@ defmodule Kaffy.ResourceCallbacks do {:ok, entry} else {:error, :not_found} -> - Kaffy.Utils.repo().insert(changeset) + Kaffy.Utils.repo(resource).insert(changeset) unexpected_error -> {:error, unexpected_error} @@ -37,7 +37,7 @@ defmodule Kaffy.ResourceCallbacks do def update_callbacks(conn, resource, entry, changes) do changeset = Kaffy.ResourceAdmin.update_changeset(resource, entry, changes) - repo = Kaffy.Utils.repo() + repo = Kaffy.Utils.repo(resource) repo.transaction(fn -> result = @@ -60,7 +60,7 @@ defmodule Kaffy.ResourceCallbacks do {:ok, entry} else {:error, :not_found} -> - Kaffy.Utils.repo().update(changeset) + Kaffy.Utils.repo(resource).update(changeset) unexpected_error -> {:error, unexpected_error} @@ -68,7 +68,7 @@ defmodule Kaffy.ResourceCallbacks do end def delete_callbacks(conn, resource, entry) do - repo = Kaffy.Utils.repo() + repo = Kaffy.Utils.repo(resource) repo.transaction(fn -> result = @@ -163,7 +163,7 @@ defmodule Kaffy.ResourceCallbacks do {:ok, entry} else {:error, :not_found} -> - Kaffy.Utils.repo().delete(changeset) + Kaffy.Utils.repo(resource).delete(changeset) unexpected_error -> {:error, unexpected_error} diff --git a/lib/kaffy/resource_form.ex b/lib/kaffy/resource_form.ex index dfbe4d2d..e66a1b3d 100644 --- a/lib/kaffy/resource_form.ex +++ b/lib/kaffy/resource_form.ex @@ -29,13 +29,13 @@ defmodule Kaffy.ResourceForm do ) true -> - build_html_input(resource[:schema], form, field, type, []) + build_html_input(resource, resource[:schema], form, field, type, []) end end - def form_field(changeset, form, field, opts \\ []) + def form_field(resource, changeset, form, field, opts \\ []) - def form_field(changeset, form, {field, options}, opts) do + def form_field(resource, changeset, form, {field, options}, opts) do options = options || %{} type = @@ -62,16 +62,24 @@ defmodule Kaffy.ResourceForm do select(form, field, choices, class: "custom-select") true -> - build_html_input(changeset.data, form, field, type, opts, permission == :readonly) + build_html_input( + resource, + changeset.data, + form, + field, + type, + opts, + permission == :readonly + ) end end - def form_field(changeset, form, field, opts) do + def form_field(resource, changeset, form, field, opts) do type = Kaffy.ResourceSchema.field_type(changeset.data.__struct__, field) - build_html_input(changeset.data, form, field, type, opts) + build_html_input(resource, changeset.data, form, field, type, opts) end - defp build_html_input(schema, form, field, type, opts, readonly \\ false) do + defp build_html_input(resource, schema, form, field, type, opts, readonly \\ false) do data = schema {conn, opts} = Keyword.pop(opts, :conn) opts = Keyword.put(opts, :readonly, readonly) @@ -91,7 +99,7 @@ defmodule Kaffy.ResourceForm do [ [ form_label(fp, f), - form_field(embed_changeset, fp, f, class: "form-control") + form_field(resource, embed_changeset, fp, f, class: "form-control") ] | all ] @@ -104,13 +112,13 @@ defmodule Kaffy.ResourceForm do :id -> case Kaffy.ResourceSchema.primary_key(schema) == [field] do true -> text_input(form, field, opts) - false -> text_or_assoc(conn, schema, form, field, opts) + false -> text_or_assoc(conn, resource, form, field, opts) end :binary_id -> case Kaffy.ResourceSchema.primary_key(schema) == [field] do true -> text_input(form, field, opts) - false -> text_or_assoc(conn, schema, form, field, opts) + false -> text_or_assoc(conn, resource, form, field, opts) end :string -> @@ -240,7 +248,9 @@ defmodule Kaffy.ResourceForm do ] end - defp text_or_assoc(conn, schema, form, field, opts) do + defp text_or_assoc(conn, resource, form, field, opts) do + schema = resource[:schema] + actual_assoc = Enum.filter(Kaffy.ResourceSchema.associations(schema), fn a -> Kaffy.ResourceSchema.association(schema, a).owner_key == field @@ -291,7 +301,7 @@ defmodule Kaffy.ResourceForm do end false -> - options = Kaffy.Utils.repo().all(assoc) + options = Kaffy.Utils.repo(resource).all(assoc) fields = Kaffy.ResourceSchema.fields(assoc) @@ -356,7 +366,7 @@ defmodule Kaffy.ResourceForm do defp build_changeset_value(value), do: to_string(value) - def kaffy_input(conn, changeset, form, field, options) do + def kaffy_input(conn, resource, changeset, form, field, options) do ft = Kaffy.ResourceSchema.field_type(changeset.data.__struct__, field) case Kaffy.Utils.is_module(ft) && Keyword.has_key?(ft.__info__(:functions), :render_form) do @@ -371,7 +381,7 @@ defmodule Kaffy.ResourceForm do label_tag = if ft != :boolean, do: form_label(form, {field, options}), else: "" field_tag = - form_field(changeset, form, {field, options}, + form_field(resource, changeset, form, {field, options}, class: "form-control #{error_class}", conn: conn ) diff --git a/lib/kaffy/resource_query.ex b/lib/kaffy/resource_query.ex index e2a3995e..56c57f83 100644 --- a/lib/kaffy/resource_query.ex +++ b/lib/kaffy/resource_query.ex @@ -26,10 +26,10 @@ defmodule Kaffy.ResourceQuery do ) custom_query = Kaffy.ResourceAdmin.custom_index_query(conn, resource, paged) - current_page = Kaffy.Utils.repo().all(custom_query) + current_page = Kaffy.Utils.repo(resource).all(custom_query) do_cache = if search == "" and Enum.empty?(filtered_fields), do: true, else: false - all_count = cached_total_count(schema, do_cache, all) + all_count = cached_total_count(resource, do_cache, all) {all_count, current_page} end @@ -48,7 +48,7 @@ defmodule Kaffy.ResourceQuery do schema = resource[:schema] query = from(s in schema, where: s.id == ^id) custom_query = Kaffy.ResourceAdmin.custom_show_query(conn, resource, query) - Kaffy.Utils.repo().one(custom_query) + Kaffy.Utils.repo(resource).one(custom_query) end def fetch_list(_, [""]), do: [] @@ -57,13 +57,15 @@ defmodule Kaffy.ResourceQuery do schema = resource[:schema] from(s in schema, where: s.id in ^ids) - |> Kaffy.Utils.repo().all() + |> Kaffy.Utils.repo(resource).all() end - def total_count(schema, do_cache, query) do + def total_count(resource, do_cache, query) do + schema = resource[:schema] + result = from(s in query, select: fragment("count(*)")) - |> Kaffy.Utils.repo().one() + |> Kaffy.Utils.repo(resource).one() if do_cache and result > 100_000 do Kaffy.Cache.Client.add_cache(schema, "count", result, 600) @@ -72,10 +74,11 @@ defmodule Kaffy.ResourceQuery do result end - def cached_total_count(schema, false, query), do: total_count(schema, false, query) + def cached_total_count(resource, false, query), do: total_count(resource, false, query) - def cached_total_count(schema, do_cache, query) do - Kaffy.Cache.Client.get_cache(schema, "count") || total_count(schema, do_cache, query) + def cached_total_count(resource, do_cache, query) do + Kaffy.Cache.Client.get_cache(resource[:schema], "count") || + total_count(resource, do_cache, query) end defp get_filter_fields(params, resource) do diff --git a/lib/kaffy/utils.ex b/lib/kaffy/utils.ex index 5542273e..045aac71 100644 --- a/lib/kaffy/utils.ex +++ b/lib/kaffy/utils.ex @@ -50,6 +50,14 @@ defmodule Kaffy.Utils do end end + @doc """ + Returns the repo module specified by the resource + """ + @spec repo(String.t()) :: atom() + def repo(resource) do + resource[:repo] || repo() + end + @doc """ Returns the version of the provided app. @@ -121,6 +129,14 @@ defmodule Kaffy.Utils do end end + @spec full_resources(Plug.Conn.t()) :: atom() | pid() | nil + def set_dynamic_repo(conn) do + case env(:set_dynamic_repo) do + f when is_function(f) -> f.(conn) + _ -> nil + end + end + @doc """ Returns a list of contexts as atoms. @@ -340,7 +356,15 @@ defmodule Kaffy.Utils do end end) - %{stylesheets: stylesheets, javascripts: javascripts} + navigation_extras = + Enum.map(exts, fn ext -> + case function_exported?(ext, :navigation_extras, 1) do + true -> ext.navigation_extras(conn) + false -> [] + end + end) + + %{stylesheets: stylesheets, javascripts: javascripts, navigation_extras: navigation_extras} end defp env(key, default \\ nil) do diff --git a/lib/kaffy_web/controllers/resource_controller.ex b/lib/kaffy_web/controllers/resource_controller.ex index 2d84c5f3..9fe8122f 100644 --- a/lib/kaffy_web/controllers/resource_controller.ex +++ b/lib/kaffy_web/controllers/resource_controller.ex @@ -15,6 +15,7 @@ defmodule KaffyWeb.ResourceController do "pick" => _field } = params ) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) case can_proceed?(my_resource, conn) do @@ -52,6 +53,7 @@ defmodule KaffyWeb.ResourceController do end def index(conn, %{"context" => context, "resource" => resource} = params) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) case can_proceed?(my_resource, conn) do @@ -89,6 +91,7 @@ defmodule KaffyWeb.ResourceController do end def show(conn, %{"context" => context, "resource" => resource, "id" => id}) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) schema = my_resource[:schema] resource_name = Kaffy.ResourceAdmin.singular_name(my_resource) @@ -121,6 +124,7 @@ defmodule KaffyWeb.ResourceController do end def update(conn, %{"context" => context, "resource" => resource, "id" => id} = params) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) schema = my_resource[:schema] params = Kaffy.ResourceParams.decode_map_fields(resource, schema, params) @@ -200,6 +204,7 @@ defmodule KaffyWeb.ResourceController do end def new(conn, %{"context" => context, "resource" => resource}) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) resource_name = Kaffy.ResourceAdmin.singular_name(my_resource) @@ -222,6 +227,7 @@ defmodule KaffyWeb.ResourceController do end def create(conn, %{"context" => context, "resource" => resource} = params) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) params = Kaffy.ResourceParams.decode_map_fields(resource, my_resource[:schema], params) changes = Map.get(params, resource, %{}) @@ -281,6 +287,7 @@ defmodule KaffyWeb.ResourceController do end def delete(conn, %{"context" => context, "resource" => resource, "id" => id}) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) case can_proceed?(my_resource, conn) do @@ -318,6 +325,7 @@ defmodule KaffyWeb.ResourceController do "id" => id, "action_key" => action_key }) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) entry = Kaffy.ResourceQuery.fetch_resource(conn, my_resource, id) actions = Kaffy.ResourceAdmin.resource_actions(my_resource, conn) @@ -342,6 +350,7 @@ defmodule KaffyWeb.ResourceController do conn, %{"context" => context, "resource" => resource, "action_key" => action_key} = params ) do + Kaffy.Utils.set_dynamic_repo(conn) my_resource = Kaffy.Utils.get_resource(conn, context, resource) ids = Map.get(params, "ids", "") |> String.split(",") entries = Kaffy.ResourceQuery.fetch_list(my_resource, ids) diff --git a/lib/kaffy_web/templates/layout/app.html.eex b/lib/kaffy_web/templates/layout/app.html.eex index 1aa5308c..b479209f 100644 --- a/lib/kaffy_web/templates/layout/app.html.eex +++ b/lib/kaffy_web/templates/layout/app.html.eex @@ -26,7 +26,7 @@ logo <% end %> -