diff --git a/lib/kaffy/resource_admin.ex b/lib/kaffy/resource_admin.ex index 031aaedf..2c294502 100644 --- a/lib/kaffy/resource_admin.ex +++ b/lib/kaffy/resource_admin.ex @@ -2,6 +2,8 @@ defmodule Kaffy.ResourceAdmin do alias Kaffy.ResourceSchema alias Kaffy.Utils + @default_id_separator ":" + @moduledoc """ ResourceAdmin modules should be created for every schema you want to customize/configure in Kaffy. @@ -148,7 +150,7 @@ defmodule Kaffy.ResourceAdmin do @doc """ `ordering/1` takes a schema and returns how the entries should be ordered. - If `ordering/1` is not defined, Kaffy will return `[desc: :id]`. + If `ordering/1` is not defined, Kaffy will return `[desc: primary_key]`, or the first field of the primary key, if it's a composite. ## Examples @@ -159,7 +161,10 @@ defmodule Kaffy.ResourceAdmin do ``` """ def ordering(resource) do - Utils.get_assigned_value_or_default(resource, :ordering, desc: :id) + schema = resource[:schema] + [order_key | _] = ResourceSchema.primary_keys(schema) + + Utils.get_assigned_value_or_default(resource, :ordering, desc: order_key) end @doc """ @@ -331,6 +336,70 @@ defmodule Kaffy.ResourceAdmin do Utils.get_assigned_value_or_default(resource, :plural_name, default) end + @doc """ + `serialize_id/2` takes a schema and record and must return a string to be used in the URL and form values. + + If `serialize_id/2` is not defined, Kaffy will concatenate multiple primary keys with `":"` as a separator. + + Examples: + + ```elixir + # Default method with fixed keys + def serialize_id(_schema, record) do + Enum.join([record.post_id, record.tag_id], ":") + end + + # ETF + def serialize_id(_schema, record) do + {record.post_id, record.tag_id} + |> :erlang.term_to_binary() + |> Base.url_encode64() + end + ``` + """ + def serialize_id(resource, entry) do + schema = resource[:schema] + default = schema + |> ResourceSchema.primary_keys() + |> Enum.map_join(@default_id_separator, &Map.get(entry, &1)) + + Utils.get_assigned_value_or_default(resource, :serialize_id, default, [entry]) + end + + @doc """ + `deserialize_id/2` takes a schema and serialized id and must return a complete + keyword list in the form of [{:primary_key, value}, ...]. + + If `deserialize_id/2` is not defined, Kaffy will split multiple primary keys with `":"` as a separator. + + Examples: + + ```elixir + # Default method with fixed keys + def deserialize_id(_schema, serialized_id) do + Enum.zip([:post_id, :tag_id], String.split(serialized_id, ":")) + end + + # Deserialize from ETF + def deserialize_id(_schema, serialized_id) do + {product_id, tag_id} = serialized_id + |> Base.url_decode64!() + |> :erlang.binary_to_term() + + [product_id: product_id, tag_id: tag_id] + end + ``` + """ + def deserialize_id(resource, id) do + schema = resource[:schema] + id_list = String.split(id, @default_id_separator) + default = schema + |> ResourceSchema.primary_keys() + |> Enum.zip(id_list) + + Utils.get_assigned_value_or_default(resource, :deserialize_id, default, [id]) + end + def resource_actions(resource, conn) do Utils.get_assigned_value_or_default(resource, :resource_actions, nil, [conn], false) end diff --git a/lib/kaffy/resource_form.ex b/lib/kaffy/resource_form.ex index 4452456f..d2354fc9 100644 --- a/lib/kaffy/resource_form.ex +++ b/lib/kaffy/resource_form.ex @@ -47,8 +47,14 @@ defmodule Kaffy.ResourceForm do opts end + # Check if any primary key fields are nil + is_create_event = changeset.data.__struct__ + |> Kaffy.ResourceSchema.primary_keys() + |> Enum.map(&Map.get(changeset.data, &1)) + |> Enum.any?(&is_nil/1) + permission = - case is_nil(changeset.data.id) do + case is_create_event do true -> Map.get(options, :create, :editable) false -> Map.get(options, :update, :editable) end @@ -115,13 +121,13 @@ defmodule Kaffy.ResourceForm do textarea(form, field, [value: value, rows: 4, placeholder: "JSON Content"] ++ opts) :id -> - case Kaffy.ResourceSchema.primary_key(schema) == [field] do + case field in Kaffy.ResourceSchema.primary_keys(schema) do true -> text_input(form, field, opts) false -> text_or_assoc(conn, schema, form, field, opts) end :binary_id -> - case Kaffy.ResourceSchema.primary_key(schema) == [field] do + case field in Kaffy.ResourceSchema.primary_keys(schema) do true -> text_input(form, field, opts) false -> text_or_assoc(conn, schema, form, field, opts) end diff --git a/lib/kaffy/resource_query.ex b/lib/kaffy/resource_query.ex index c2a03973..298d134d 100644 --- a/lib/kaffy/resource_query.ex +++ b/lib/kaffy/resource_query.ex @@ -52,7 +52,9 @@ defmodule Kaffy.ResourceQuery do def fetch_resource(conn, resource, id) do schema = resource[:schema] - query = from(s in schema, where: s.id == ^id) + + id_filter = Kaffy.ResourceAdmin.deserialize_id(resource, id) + query = from(s in schema, where: ^id_filter) case Kaffy.ResourceAdmin.custom_show_query(conn, resource, query) do {custom_query, opts} -> Kaffy.Utils.repo().one(custom_query, opts) @@ -65,8 +67,13 @@ defmodule Kaffy.ResourceQuery do def fetch_list(resource, ids) do schema = resource[:schema] - from(s in schema, where: s.id in ^ids) - |> Kaffy.Utils.repo().all() + primary_keys = Kaffy.ResourceSchema.primary_keys(schema) + ids = Enum.map(ids, &Kaffy.ResourceAdmin.deserialize_id(resource, &1)) + + case build_list_query(schema, primary_keys, ids) do + {:error, error_msg} -> {:error, error_msg} + query -> Kaffy.Utils.repo().all(query) + end end def total_count(schema, do_cache, query, opts \\ []) @@ -149,6 +156,21 @@ defmodule Kaffy.ResourceQuery do {query, limited_query} end + defp build_list_query(_schema, [], _key_pairs) do + {:error, "No private keys. List action not supported."} + end + + defp build_list_query(schema, [primary_key], ids) do + ids = Enum.map(ids, fn [{_key, id}] -> id end) + from(s in schema, where: field(s, ^primary_key) in ^ids) + end + + defp build_list_query(schema, _composite_key, key_pairs) do + Enum.reduce(key_pairs, schema, fn pair, query_acc -> + from query_acc, or_where: ^pair + end) + end + defp build_filtered_fields_query(query, []), do: query defp build_filtered_fields_query(query, [filter | rest]) do diff --git a/lib/kaffy/resource_schema.ex b/lib/kaffy/resource_schema.ex index 57ab23bd..ede7f90d 100644 --- a/lib/kaffy/resource_schema.ex +++ b/lib/kaffy/resource_schema.ex @@ -1,7 +1,7 @@ defmodule Kaffy.ResourceSchema do @moduledoc false - def primary_key(schema) do + def primary_keys(schema) do schema.__schema__(:primary_key) end @@ -26,7 +26,10 @@ defmodule Kaffy.ResourceSchema do end def form_fields(schema) do - to_be_removed = fields_to_be_removed(schema) ++ [:id, :inserted_at, :updated_at] + to_be_removed = + fields_to_be_removed(schema) ++ + primary_keys(schema) ++ + [:inserted_at, :updated_at] Keyword.drop(fields(schema), to_be_removed) end @@ -35,7 +38,9 @@ defmodule Kaffy.ResourceSchema do fields_to_be_removed(schema) ++ get_has_many_associations(schema) ++ get_has_one_assocations(schema) ++ - get_many_to_many_associations(schema) ++ [:id, :inserted_at, :updated_at] + get_many_to_many_associations(schema) ++ + primary_keys(schema) ++ + [:inserted_at, :updated_at] Keyword.drop(fields(schema), to_be_removed) end diff --git a/lib/kaffy_web/controllers/resource_controller.ex b/lib/kaffy_web/controllers/resource_controller.ex index ccc9bf88..e501153a 100644 --- a/lib/kaffy_web/controllers/resource_controller.ex +++ b/lib/kaffy_web/controllers/resource_controller.ex @@ -349,11 +349,14 @@ defmodule KaffyWeb.ResourceController do action_record = get_action_record(actions, action_key) kaffy_inputs = Map.get(params, "kaffy-input", %{}) - result = - case Map.get(action_record, :inputs, []) do - [] -> action_record.action.(conn, entries) - _ -> action_record.action.(conn, entries, kaffy_inputs) - end + result = case entries do + {:error, error_msg} -> {:error, error_msg} + entries -> + case Map.get(action_record, :inputs, []) do + [] -> action_record.action.(conn, entries) + _ -> action_record.action.(conn, entries, kaffy_inputs) + end + end case result do :ok -> @@ -391,6 +394,9 @@ defmodule KaffyWeb.ResourceController do end defp redirect_to_resource(conn, context, resource, entry) do + my_resource = Kaffy.Utils.get_resource(conn, context, resource) + id = Kaffy.ResourceAdmin.serialize_id(my_resource, entry) + redirect(conn, to: Kaffy.Utils.router().kaffy_resource_path( @@ -398,7 +404,7 @@ defmodule KaffyWeb.ResourceController do :show, context, resource, - entry.id + id ) ) end diff --git a/lib/kaffy_web/templates/resource/_table.html.eex b/lib/kaffy_web/templates/resource/_table.html.eex index 2abcc2f8..e81de33d 100644 --- a/lib/kaffy_web/templates/resource/_table.html.eex +++ b/lib/kaffy_web/templates/resource/_table.html.eex @@ -4,17 +4,17 @@
- <%= for entry <- @entries do %> + <%= for {entry, index} <- Enum.with_index(@entries) do %>